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 pub stdout: Option<String>,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum SessionErrorKind {
44 Other,
45 Unsolvable,
46 Printed(i32),
51}
52
53impl SessionError {
54 pub fn new(message: impl Into<String>) -> Self {
55 Self {
56 message: message.into(),
57 kind: SessionErrorKind::Other,
58 stdout: None,
59 }
60 }
61}
62
63impl From<PoolError> for SessionError {
64 fn from(e: PoolError) -> SessionError {
65 SessionError::new(e.0)
66 }
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub struct RepoConfig {
72 pub key: RepoKey,
73 pub definition: Value,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum RepoKey {
78 Named(String),
79 Indexed(u64),
80}
81
82pub fn merge_repositories(current: &mut Vec<RepoConfig>, new: &Value) {
85 static PACKAGIST: OnceLock<Regex> = OnceLock::new();
86 let entries: Vec<(RepoKey, Value)> = match new {
87 Value::Array(list) => list
88 .iter()
89 .enumerate()
90 .map(|(i, v)| (RepoKey::Indexed(i as u64), v.clone()))
91 .collect(),
92 Value::Object(map) => map
93 .iter()
94 .map(|(k, v)| {
95 let key = match k.parse::<u64>() {
96 Ok(i) if i.to_string() == *k => RepoKey::Indexed(i),
97 _ => RepoKey::Named(k.clone()),
98 };
99 (key, v.clone())
100 })
101 .collect(),
102 _ => return,
103 };
104 if entries.is_empty() {
105 return;
106 }
107 current.reverse();
108 let disable = |current: &mut Vec<RepoConfig>, name: &str| {
111 let key = match name.parse::<u64>() {
112 Ok(i) if i.to_string() == name => RepoKey::Indexed(i),
113 _ => RepoKey::Named(name.to_owned()),
114 };
115 if current.iter().any(|r| r.key == key) {
116 current.retain(|r| r.key != key);
117 } else if name == "packagist" {
118 current.retain(|r| r.key != RepoKey::Named("packagist.org".into()));
119 }
120 };
121 for (key, repository) in entries.into_iter().rev() {
122 if repository == Value::Bool(false) {
123 let name = match &key {
124 RepoKey::Named(n) => n.clone(),
125 RepoKey::Indexed(i) => i.to_string(),
126 };
127 disable(current, &name);
128 continue;
129 }
130 if let Some(obj) = repository.as_object() {
131 if obj.len() == 1 && obj.values().next() == Some(&Value::Bool(false)) {
132 disable(current, obj.keys().next().map(String::as_str).unwrap_or(""));
133 continue;
134 }
135 }
136 if repository.get("type").and_then(Value::as_str) == Some("composer") {
137 if let Some(url) = repository.get("url").and_then(Value::as_str) {
138 let re = regex(
139 &PACKAGIST,
140 r"^https?://(?:[a-z0-9-.]+\.)?packagist.org(/|$)",
141 false,
142 );
143 if re.is_match(url.as_bytes()).unwrap_or(false) {
144 disable(current, "packagist.org");
145 }
146 }
147 }
148 match key {
149 RepoKey::Indexed(i) => {
150 if current.iter().any(|r| r.key == RepoKey::Indexed(i)) {
151 let next = current
152 .iter()
153 .filter_map(|r| match r.key {
154 RepoKey::Indexed(j) => Some(j + 1),
155 _ => None,
156 })
157 .max()
158 .unwrap_or(0);
159 current.push(RepoConfig {
160 key: RepoKey::Indexed(next),
161 definition: repository,
162 });
163 } else {
164 current.push(RepoConfig {
165 key: RepoKey::Indexed(i),
166 definition: repository,
167 });
168 }
169 }
170 RepoKey::Named(name) => {
171 let name = if name == "packagist" {
172 "packagist.org".to_owned()
173 } else {
174 name
175 };
176 let key = RepoKey::Named(name);
177 if let Some(slot) = current.iter_mut().find(|r| r.key == key) {
178 slot.definition = repository;
179 } else {
180 current.push(RepoConfig {
181 key,
182 definition: repository,
183 });
184 }
185 }
186 }
187 }
188 current.reverse();
189}
190
191#[derive(Debug, Clone, Default)]
193pub struct MergedConfig {
194 pub repositories: Vec<RepoConfig>,
195 pub platform: Map<String, Value>,
197 pub policy: crate::policy_config::RawPolicyConfig,
199}
200
201impl MergedConfig {
202 pub fn load(
205 manifest: &Value,
206 composer_home: Option<&Path>,
207 ) -> Result<MergedConfig, SessionError> {
208 let mut cfg = MergedConfig {
209 repositories: vec![RepoConfig {
210 key: RepoKey::Named("packagist.org".into()),
211 definition: serde_json::json!({"type": "composer", "url": "https://repo.packagist.org"}),
212 }],
213 platform: Map::new(),
214 policy: crate::policy_config::RawPolicyConfig::default(),
215 };
216 if let Some(home) = composer_home {
217 let global = home.join("config.json");
218 if global.is_file() {
219 let text = std::fs::read_to_string(&global)
220 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
221 let v: Value = serde_json::from_str(&text)
222 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
223 cfg.merge(&v);
224 }
225 }
226 cfg.merge(manifest);
227 Ok(cfg)
228 }
229
230 fn merge(&mut self, config: &Value) {
231 if let Some(platform) = config.get("config").and_then(|c| c.get("platform")) {
234 self.platform = platform.as_object().cloned().unwrap_or_default();
235 }
236 if let Some(repos) = config.get("repositories") {
237 merge_repositories(&mut self.repositories, repos);
238 }
239 if let Some(cfg) = config.get("config").and_then(Value::as_object) {
240 self.policy.merge(cfg);
241 }
242 }
243}
244
245pub struct SolveReport {
248 pub transaction: LockTransaction,
249 pub decisions: Vec<i64>,
251 pub rules: usize,
253 pub learned: usize,
255}
256
257#[derive(Debug, Clone, Default)]
260pub struct UpdateOptions {
261 pub allow_list: Vec<String>,
263 pub transitive: Option<crate::pool::UpdateMode>,
264 pub no_blocking: bool,
266 pub root_patch: Option<crate::root::RootPatch>,
269 pub merged: Option<crate::merge_plugin::Merged>,
273 pub temporary_requirements: Vec<(String, String)>,
277}
278
279impl UpdateOptions {
280 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
282 let mut allow_list: Vec<String> = Vec::new();
283 for p in packages {
284 let l = p.to_lowercase();
285 if !allow_list.contains(&l) {
286 allow_list.push(l);
287 }
288 }
289 UpdateOptions {
290 allow_list,
291 transitive: Some(transitive),
292 ..Default::default()
293 }
294 }
295}
296
297fn temporary_constraints(
303 reqs: &[(String, String)],
304 root: &mut RootPackage,
305) -> Result<BTreeMap<String, crate::pool::TemporaryConstraint>, SessionError> {
306 if reqs.is_empty() {
307 return Ok(BTreeMap::new());
308 }
309 let pairs: Vec<(String, String)> = reqs.to_vec();
310 crate::root::extract_references(&pairs, &mut root.references);
311 let minimum = root.minimum_stability.clone();
312 crate::root::extract_stability_flags(&pairs, &minimum, &mut root.stability_flags);
313 let root_requires: Vec<(String, String, Constraint)> = root
317 .all_requires()
318 .iter()
319 .map(|l| {
320 (
321 l.target.clone(),
322 l.pretty_constraint.clone(),
323 l.constraint.clone(),
324 )
325 })
326 .collect();
327 let fail = |message: String, stdout: Option<String>| SessionError {
328 message,
329 kind: SessionErrorKind::Printed(1),
330 stdout,
331 };
332 let mut out: BTreeMap<String, crate::pool::TemporaryConstraint> = BTreeMap::new();
333 for (name, text) in &pairs {
334 let name = name.to_lowercase();
335 let parsed = crate::pool::TemporaryConstraint {
336 pretty: text.clone(),
337 constraint: crate::constraint::parse_constraints(text)
338 .map_err(|e| SessionError::new(format!("{name}: {e}")))?
339 .constraint,
340 };
341 if name.contains('*') {
342 let re = crate::pool::package_name_regexp(&name);
343 for (target, pretty, constraint) in &root_requires {
344 if !re.is_match(target.as_bytes()).unwrap_or(false) {
345 continue;
346 }
347 out.insert(target.clone(), parsed.clone());
348 if !crate::intervals::have_intersections(&parsed.constraint, constraint) {
349 return Err(fail(
350 format!(
351 "The temporary constraint \"{text}\" for \"{name}\" matching \"{target}\" must be a subset of the constraint in your composer.json ({pretty})"
352 ),
353 None,
354 ));
355 }
356 }
357 } else {
358 out.insert(name.clone(), parsed.clone());
359 if let Some((_, pretty, constraint)) = root_requires.iter().find(|(t, _, _)| *t == name)
360 {
361 if !crate::intervals::have_intersections(&parsed.constraint, constraint) {
362 return Err(fail(
363 format!(
364 "The temporary constraint \"{text}\" for \"{name}\" must be a subset of the constraint in your composer.json ({pretty})"
365 ),
366 Some(format!(
367 "Run `composer require {name}` or `composer require {name}:{text}` instead to replace the constraint"
368 )),
369 ));
370 }
371 }
372 }
373 }
374 Ok(out)
375}
376
377#[derive(Debug, Clone)]
380pub struct FlexSession {
381 pub symfony_require: String,
382 pub symfony: Constraint,
383 pub versions: crate::flex_filter::FlexVersions,
384}
385
386pub struct UpdateSession {
387 pub flex: Option<FlexSession>,
389 pub arena: Vec<Package>,
390 pub root: RootPackage,
391 pub fixed_root: usize,
393 pub fixed_root_alias: Option<usize>,
394 pub platform: Vec<usize>,
395 pub locked: Option<Vec<usize>>,
396 pub set: RepositorySet,
397 pub request: Request,
398 pub config: MergedConfig,
399 pub dev_mode: bool,
400 pub prefer_stable: bool,
402 pub prefer_lowest: bool,
403 pub php_version: String,
406 pub policy_config: crate::policy_config::PolicyConfig,
408 pub ini_files: Vec<String>,
411 pub loaded_extensions: BTreeSet<String>,
413 pub installer_dev_mode: bool,
416}
417
418impl UpdateSession {
419 pub fn prepare(
420 project_dir: &Path,
421 composer_home: Option<&Path>,
422 dev_mode: bool,
423 ) -> Result<UpdateSession, SessionError> {
424 Self::prepare_with(project_dir, composer_home, dev_mode, None)
425 }
426
427 pub fn prepare_with(
429 project_dir: &Path,
430 composer_home: Option<&Path>,
431 dev_mode: bool,
432 http: Option<HttpTransports>,
433 ) -> Result<UpdateSession, SessionError> {
434 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
435 }
436
437 pub fn prepare_full(
440 project_dir: &Path,
441 composer_home: Option<&Path>,
442 dev_mode: bool,
443 http: Option<HttpTransports>,
444 cache_repo_dir: Option<&Path>,
445 ) -> Result<UpdateSession, SessionError> {
446 Self::prepare_update(
447 project_dir,
448 composer_home,
449 dev_mode,
450 http,
451 cache_repo_dir,
452 &UpdateOptions::default(),
453 )
454 }
455
456 pub fn prepare_update(
459 project_dir: &Path,
460 composer_home: Option<&Path>,
461 dev_mode: bool,
462 http: Option<HttpTransports>,
463 cache_repo_dir: Option<&Path>,
464 options: &UpdateOptions,
465 ) -> Result<UpdateSession, SessionError> {
466 let partial_update = !options.allow_list.is_empty();
467 let manifest_path = project_dir.join("composer.json");
468 let manifest_text = std::fs::read_to_string(&manifest_path)
469 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
470 let manifest: Value = serde_json::from_str(&manifest_text)
471 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
472 let config = MergedConfig::load(&manifest, composer_home)?;
473 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
474 .map_err(|e| SessionError::new(e.0))?;
475 policy_config
476 .apply_no_blocking(options.no_blocking)
477 .map_err(|e| SessionError::new(e.0))?;
478 let mut root =
479 RootPackage::load(&manifest, project_dir).map_err(|e| SessionError::new(e.0))?;
480 if let Some(merged) = &options.merged {
481 let flags = std::mem::take(&mut root.stability_flags);
488 root = RootPackage::load(&merged.manifest, project_dir)
489 .map_err(|e| SessionError::new(e.0))?;
490 root.stability_flags = flags;
491 for include in &merged.include_links {
494 crate::root::merge_plugin_stability_flags(
495 &mut root.stability_flags,
496 &root.minimum_stability,
497 &include.require,
498 );
499 }
500 for include in &merged.include_links {
501 crate::root::merge_plugin_stability_flags(
502 &mut root.stability_flags,
503 &root.minimum_stability,
504 &include.require_dev,
505 );
506 }
507 }
508 if let Some(patch) = &options.root_patch {
509 root.apply_patch(patch)
510 .map_err(|e| SessionError::new(e.0))?;
511 }
512 let temporary_constraints =
516 temporary_constraints(&options.temporary_requirements, &mut root)?;
517 let probed = probe().map_err(|e| SessionError::new(e.0))?;
518 let php_version = probed
522 .iter()
523 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
524 .and_then(|p| p.get("version").and_then(Value::as_str))
525 .map(|v| {
526 v.split(['.', '-', '+'])
527 .take(3)
528 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
529 .collect::<Vec<_>>()
530 .join(".")
531 })
532 .unwrap_or_default();
533 let platform_pkgs =
534 platform_packages(&probed, &config.platform).map_err(|e| SessionError::new(e.0))?;
535
536 let mut arena: Vec<Package> = Vec::new();
537
538 let mut fixed = root.package.clone();
540 fixed.requires = Default::default();
541 fixed.dev_requires = Default::default();
542 arena.push(fixed);
543 let fixed_root = 0;
544 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
545 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
546 arena.push(a);
547 arena.len() - 1
548 });
549 let root_members: Vec<usize> = fixed_root_alias
550 .into_iter()
551 .chain(std::iter::once(fixed_root))
552 .collect();
553
554 let mut platform: Vec<usize> = Vec::new();
555 for p in platform_pkgs {
556 arena.push(p);
557 platform.push(arena.len() - 1);
558 }
559
560 let lock_path = project_dir.join("composer.lock");
564 let lock: Option<Value> = if lock_path.is_file() {
565 let text = std::fs::read_to_string(&lock_path)
566 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
567 match serde_json::from_str::<Value>(&text) {
568 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
569 Ok(_) => None,
570 Err(e) => {
571 if partial_update {
572 return Err(SessionError::new(format!(
573 "\"{}\" does not contain valid JSON\n{e}",
574 lock_path.display()
575 )));
576 }
577 None
578 }
579 }
580 } else {
581 None
582 };
583 let locked = match &lock {
584 Some(v) => Some(locked_repository(v, &mut arena).map_err(|e| SessionError::new(e.0))?),
585 None => None,
586 };
587
588 let mut stability_flags = root.stability_flags.clone();
591 let root_version = match fixed_root_alias {
592 Some(a) => arena[a].version.clone(),
593 None => arena[fixed_root].version.clone(),
594 };
595 stability_flags.insert(
596 root.package.name.clone(),
597 stability_rank(parse_stability(&root_version)),
598 );
599
600 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
605 let requires = match &root.branch_alias {
606 Some((normalized, pretty)) => {
607 let aliased = root.package.alias(0, normalized, pretty);
608 let mut all = aliased.requires.clone();
609 for l in aliased.dev_requires.iter() {
610 all.insert(l.clone());
611 }
612 all
613 }
614 None => root.all_requires(),
615 };
616 for link in requires.iter() {
617 root_requires.insert(&link.target, link.constraint.clone());
618 }
619
620 let mut set = RepositorySet::new(
621 &root.minimum_stability,
622 stability_flags,
623 &root.aliases,
624 root.references.clone(),
625 root_requires,
626 temporary_constraints,
627 );
628 set.add_repository(Repository::Root(root_members));
629 set.add_repository(Repository::Platform(platform.clone()));
630 for repo in &config.repositories {
631 let origin = Origin::Repository(set.repositories.len());
632 set.add_repository(open_repository(
633 repo,
634 http.as_ref(),
635 cache_repo_dir,
636 project_dir,
637 origin,
638 &mut arena,
639 )?);
640 }
641 if let Some(ids) = &locked {
642 set.add_repository(Repository::Locked(ids.clone()));
643 }
644
645 if partial_update && locked.is_none() {
648 return Err(SessionError {
649 message: "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.".to_owned(),
650 kind: SessionErrorKind::Printed(3),
651 stdout: None,
652 });
653 }
654 let mut request = Request::new(locked.clone());
655 if partial_update {
656 request.set_update_allow_list(
657 options.allow_list.clone(),
658 options
659 .transitive
660 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
661 );
662 }
663 if let Some(a) = fixed_root_alias {
664 request.fix_package(a);
665 }
666 request.fix_package(fixed_root);
667 for &p in &platform {
668 let provided = arena[fixed_root]
669 .provides
670 .get(&arena[p].name)
671 .map(|l| l.constraint.clone());
672 let provided_here = provided
673 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
674 if !provided_here {
675 request.fix_package(p);
676 }
677 }
678 for link in requires.iter() {
679 request.require_name_pretty(
680 &link.target,
681 Some(link.constraint.clone()),
682 &link.pretty_constraint,
683 )?;
684 }
685
686 Ok(UpdateSession {
687 flex: None,
688 arena,
689 root,
690 fixed_root,
691 fixed_root_alias,
692 platform,
693 locked,
694 set,
695 request,
696 config,
697 dev_mode,
698 prefer_stable: false,
699 prefer_lowest: false,
700 php_version,
701 policy_config,
702 ini_files: crate::platform::ini_files(&probed),
703 loaded_extensions: crate::platform::loaded_extensions(&probed),
704 installer_dev_mode: true,
705 })
706 }
707
708 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
709 let pre_pool = self.flex.as_ref().map(|f| {
710 let mut root_constraints = BTreeMap::new();
713 for link in self
714 .root
715 .package
716 .requires
717 .0
718 .iter()
719 .chain(self.root.package.dev_requires.0.iter())
720 {
721 root_constraints
722 .entry(link.target.clone())
723 .or_insert_with(|| link.constraint.clone());
724 }
725 crate::pool::PrePoolFilter {
726 symfony_require: f.symfony_require.clone(),
727 symfony: f.symfony.clone(),
728 root_constraints,
729 versions: f.versions.clone(),
730 }
731 });
732 Ok(self
733 .set
734 .create_pool(&mut self.request, &mut self.arena, pre_pool.as_ref())?)
735 }
736
737 pub fn policy(&self) -> DefaultPolicy {
741 let mut policy = DefaultPolicy::new(
742 self.prefer_stable || self.root.prefer_stable,
743 self.prefer_lowest,
744 None,
745 );
746 if self.flex.is_some() {
747 policy.prefer_dev_over_prerelease = true;
748 }
749 policy
750 }
751
752 pub fn extract_dev_packages(
756 &mut self,
757 transaction: &mut LockTransaction,
758 policy: &mut DefaultPolicy,
759 filter: &PlatformRequirementFilter,
760 ) -> Result<(), SessionError> {
761 if self.root.package.dev_requires.is_empty() {
762 return Ok(());
763 }
764 let dumps: Vec<Value> = transaction
767 .new_lock_packages(&self.arena, false)
768 .iter()
769 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
770 .collect();
771 let result_ids =
772 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
773 .map_err(|e| SessionError::new(e.0))?;
774 let mut members: Vec<usize> = Vec::new();
777 members.extend(self.fixed_root_alias);
778 members.push(self.fixed_root);
779 members.extend(self.platform.iter().copied());
780 members.extend(result_ids);
781 let mut pool_packages: Vec<usize> = Vec::new();
782 for idx in members {
783 pool_packages.push(idx);
784 let (name, version) = (
785 self.arena[idx].name.clone(),
786 self.arena[idx].version.clone(),
787 );
788 if let Some((alias, alias_normalized)) = self
789 .set
790 .root_aliases
791 .get(&name)
792 .and_then(|m| m.get(&version))
793 {
794 let mut base = idx;
795 while let Some(b) = self.arena[base].alias_of {
796 base = b;
797 }
798 let mut a = self.arena[base].alias(base, alias_normalized, alias);
799 a.root_package_alias = true;
800 a.origin = Origin::Detached;
801 self.arena.push(a);
802 pool_packages.push(self.arena.len() - 1);
803 }
804 }
805 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
806 let mut request = Request::new(None);
808 if let Some(a) = self.fixed_root_alias {
809 request.fix_package(a);
810 }
811 request.fix_package(self.fixed_root);
812 for &p in &self.platform {
813 let provided = self.arena[self.fixed_root]
814 .provides
815 .get(&self.arena[p].name)
816 .map(|l| l.constraint.clone());
817 let provided_here = provided.is_some_and(|c| {
818 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
819 });
820 if !provided_here {
821 request.fix_package(p);
822 }
823 }
824 let requires = match &self.root.branch_alias {
825 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
826 None => self.root.package.requires.clone(),
827 };
828 for link in requires.iter() {
829 request.require_name_pretty(
830 &link.target,
831 Some(link.constraint.clone()),
832 &link.pretty_constraint,
833 )?;
834 }
835 let mut solver = Solver::new(&pool, &self.arena);
836 let non_dev = match solver.solve(&request, policy, filter) {
837 Ok(t) => t,
838 Err(SolveError::Problems(problems)) => {
839 drop(solver);
840 let pretty = {
843 let mut ctx = crate::problem::MessageContext {
844 arena: &mut self.arena,
845 set: &self.set,
846 pool: &pool,
847 request: &request,
848 is_verbose: false,
849 ini_files: &self.ini_files,
850 loaded_extensions: &self.loaded_extensions,
851 };
852 crate::problem::pretty_string(&mut ctx, &problems, true)
853 };
854 return Err(SessionError {
855 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}"),
856 kind: SessionErrorKind::Unsolvable,
857 stdout: None,
858 });
859 }
860 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
861 };
862 transaction.set_non_dev_packages(&self.arena, &non_dev);
863 Ok(())
864 }
865
866 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
868 let mut out = Map::new();
869 for l in links.iter() {
870 if crate::platform::is_platform_package(&l.target) {
871 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
872 }
873 }
874 out
875 }
876
877 pub fn lock_json(
879 &self,
880 transaction: &LockTransaction,
881 manifest_text: &str,
882 ) -> Result<Value, SessionError> {
883 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
884 .map_err(|e| SessionError::new(e.to_string()))?;
885 let packages = lock_packages(
886 &self.arena,
887 &transaction.new_lock_packages(&self.arena, false),
888 )
889 .map_err(SessionError::new)?;
890 let packages_dev = lock_packages(
891 &self.arena,
892 &transaction.new_lock_packages(&self.arena, true),
893 )
894 .map_err(SessionError::new)?;
895 let (requires, dev_requires) = match &self.root.branch_alias {
896 Some((normalized, pretty)) => {
897 let a = self.root.package.alias(0, normalized, pretty);
898 (a.requires, a.dev_requires)
899 }
900 None => (
901 self.root.package.requires.clone(),
902 self.root.package.dev_requires.clone(),
903 ),
904 };
905 Ok(lock_data(LockInput {
906 content_hash: &content_hash,
907 packages,
908 packages_dev: Some(packages_dev),
909 platform: Self::platform_requirements(&requires),
910 platform_dev: Self::platform_requirements(&dev_requires),
911 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
912 minimum_stability: &self.root.minimum_stability,
913 stability_flags: &self.root.stability_flags,
914 prefer_stable: self.prefer_stable || self.root.prefer_stable,
915 prefer_lowest: self.prefer_lowest,
916 platform_overrides: &self.config.platform,
917 }))
918 }
919
920 pub fn update(
923 &mut self,
924 manifest_text: &str,
925 filter: &PlatformRequirementFilter,
926 ) -> Result<(Value, SolveReport), SessionError> {
927 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
928 let t = std::time::Instant::now();
929 let lap = |label: &str, t: &std::time::Instant| {
930 if trace {
931 eprintln!(
932 "trace: {label:<22} {:>7.1} ms",
933 t.elapsed().as_secs_f64() * 1000.0
934 );
935 }
936 };
937 let mut policy = self.policy();
938 let pool = self.create_filtered_pool()?;
939 if let Some(notice) = &pool.flex_notice {
943 eprintln!("{notice}");
944 }
945 eprintln!("Updating dependencies");
946 for w in &pool.warnings {
947 eprintln!("{w}");
948 }
949 lap("pool", &t);
950 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
951 pool
952 } else {
953 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
954 };
955 lap("optimize", &t);
956 let mut report = match self.solve(&pool, &mut policy, filter) {
957 Ok(r) => r,
958 Err(SolveError::Problems(problems)) => {
959 let pretty = self.pretty_problems(&pool, &problems, false);
962 let mut message = format!(
963 "Your requirements could not be resolved to an installable set of packages.\n{pretty}"
964 );
965 if !self.installer_dev_mode {
966 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.");
967 }
968 return Err(SessionError {
969 message,
970 kind: SessionErrorKind::Unsolvable,
971 stdout: None,
972 });
973 }
974 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
975 };
976 lap("solve", &t);
977 for &idx in &report.transaction.all {
981 crate::lockfile::validate_package(&self.arena[idx]).map_err(SessionError::new)?;
982 }
983 drop(pool);
984 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
985 self.extract_dev_packages(&mut transaction, &mut policy, filter)?;
986 lap("extract dev", &t);
987 let lock = self.lock_json(&transaction, manifest_text)?;
988 lap("lock data", &t);
989 report.transaction = transaction;
990 Ok((lock, report))
991 }
992
993 pub fn pretty_problems(
996 &mut self,
997 pool: &Pool,
998 problems: &[crate::solver::SolvedProblem],
999 is_dev_extraction: bool,
1000 ) -> String {
1001 let mut ctx = crate::problem::MessageContext {
1002 arena: &mut self.arena,
1003 set: &self.set,
1004 pool,
1005 request: &self.request,
1006 is_verbose: false,
1007 ini_files: &self.ini_files,
1008 loaded_extensions: &self.loaded_extensions,
1009 };
1010 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
1011 }
1012
1013 pub fn find_packages_for_require(
1020 &mut self,
1021 name: &str,
1022 ignore_stability: bool,
1023 ) -> Result<Vec<usize>, SessionError> {
1024 let name = name.to_lowercase();
1025 let mut acceptable = BTreeMap::new();
1026 let min = crate::version::stability_rank(&self.root.minimum_stability);
1027 for st in ["stable", "RC", "beta", "alpha", "dev"] {
1028 let rank = crate::version::stability_rank(st);
1029 if ignore_stability || rank <= min {
1030 acceptable.insert(st.to_owned(), rank);
1031 }
1032 }
1033 let flags = BTreeMap::new();
1034 let already = BTreeMap::new();
1035 let map = vec![(name.clone(), Constraint::MatchAll)];
1036 let mut found: Vec<usize> = Vec::new();
1037 let (_, ids) = crate::pool::array_repository_load_packages(
1038 &self.platform,
1039 &map,
1040 &acceptable,
1041 &flags,
1042 &already,
1043 &self.arena,
1044 );
1045 found.extend(ids);
1046 for (i, repo) in self.set.repositories.iter().enumerate() {
1047 if let Repository::Composer(repo) = repo {
1048 let (_, ids) = repo
1049 .load_packages(
1050 &map,
1051 &acceptable,
1052 &flags,
1053 &already,
1054 Origin::Repository(i),
1055 &mut self.arena,
1056 )
1057 .map_err(|e| SessionError::new(e.0))?;
1058 found.extend(ids);
1059 }
1060 }
1061 Ok(found)
1062 }
1063
1064 pub fn create_optimized_pool(
1065 &mut self,
1066 policy: &mut DefaultPolicy,
1067 ) -> Result<Pool, SessionError> {
1068 let pool = self.create_filtered_pool()?;
1069 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
1070 return Ok(pool);
1071 }
1072 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
1073 }
1074
1075 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
1078 let mut pool = self.create_pool()?;
1079 let before = pool.len();
1080 let flex_notice = pool.flex_notice.take();
1083 let mut warnings = Vec::new();
1084 pool = crate::pool_filters::security_advisory_filter(
1085 pool,
1086 &self.arena,
1087 &self.set.repositories,
1088 &self.request,
1089 &self.policy_config,
1090 &mut warnings,
1091 )
1092 .map_err(|e| SessionError::new(e.0))?;
1093 pool = crate::pool_filters::filter_list_filter(
1094 pool,
1095 &self.arena,
1096 &self.set.repositories,
1097 &self.request,
1098 &self.policy_config,
1099 "update",
1100 &mut warnings,
1101 )
1102 .map_err(|e| SessionError::new(e.0))?;
1103 pool.warnings.extend(warnings);
1104 pool.flex_notice = flex_notice;
1105 if std::env::var_os("VIVACITY_TRACE").is_some() {
1106 eprintln!(
1107 "trace: policy filters {before} → {} package versions ({} removed by lists)",
1108 pool.len(),
1109 pool.filter_list_removed
1110 .values()
1111 .map(Vec::len)
1112 .sum::<usize>()
1113 );
1114 }
1115 Ok(pool)
1116 }
1117
1118 pub fn solve(
1121 &self,
1122 pool: &Pool,
1123 policy: &mut DefaultPolicy,
1124 filter: &PlatformRequirementFilter,
1125 ) -> Result<SolveReport, SolveError> {
1126 let mut solver = Solver::new(pool, &self.arena);
1127 let transaction = solver.solve(&self.request, policy, filter)?;
1128 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
1129 Ok(SolveReport {
1130 learned: solver
1131 .rules
1132 .ids_of_type(crate::rule::RuleType::Learned)
1133 .len(),
1134 rules: solver.rule_set_size(),
1135 decisions,
1136 transaction,
1137 })
1138 }
1139}
1140
1141pub fn install_policy_problems(
1147 project_dir: &Path,
1148 composer_home: Option<&Path>,
1149 http: Option<HttpTransports>,
1150 cache_repo_dir: Option<&Path>,
1151 with_dev: bool,
1152 no_blocking: bool,
1153) -> Result<(Vec<String>, Vec<String>), SessionError> {
1154 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
1155 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1156 let manifest: Value = serde_json::from_str(&manifest_text)
1157 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1158 let config = MergedConfig::load(&manifest, composer_home)?;
1159 let mut policy = crate::policy_config::PolicyConfig::from_raw(&config.policy)
1160 .map_err(|e| SessionError::new(e.0))?;
1161 policy
1162 .apply_no_blocking(no_blocking)
1163 .map_err(|e| SessionError::new(e.0))?;
1164 if !policy.malware_blocks("install") {
1165 return Ok((Vec::new(), Vec::new()));
1166 }
1167 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
1168 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1169 let lock: Value = serde_json::from_str(&lock_text)
1170 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1171 let mut repositories: Vec<Repository> = Vec::new();
1174 for repo in &config.repositories {
1175 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
1176 continue;
1177 }
1178 repositories.push(open_repository_with(
1181 repo,
1182 http.as_ref(),
1183 cache_repo_dir,
1184 true,
1185 )?);
1186 }
1187 let mut arena: Vec<Package> = Vec::new();
1188 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
1189 .map_err(|e| SessionError::new(e.0))?;
1190 let mut request = Request::new(Some(locked.clone()));
1191 for &idx in &locked {
1192 request.fix_locked_package(idx);
1193 }
1194 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
1195 let mut warnings = Vec::new();
1196 let pool = crate::pool_filters::filter_list_filter(
1197 pool,
1198 &arena,
1199 &repositories,
1200 &request,
1201 &policy,
1202 "install",
1203 &mut warnings,
1204 )
1205 .map_err(|e| SessionError::new(e.0))?;
1206 let problems: Vec<String> = locked
1207 .iter()
1208 .map(|&idx| &arena[idx])
1209 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
1210 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
1211 .collect();
1212 Ok((problems, warnings))
1213}
1214
1215fn open_repository(
1219 repo: &RepoConfig,
1220 http: Option<&HttpTransports>,
1221 cache_repo_dir: Option<&Path>,
1222 project_dir: &Path,
1223 origin: Origin,
1224 arena: &mut Vec<Package>,
1225) -> Result<Repository, SessionError> {
1226 if repo.definition.get("type").and_then(Value::as_str) == Some("path") {
1227 if repo.definition.get("only").is_some()
1228 || repo.definition.get("exclude").is_some()
1229 || repo.definition.get("canonical").is_some()
1230 {
1231 return Err(SessionError::new(format!(
1232 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1233 key_string(&repo.key)
1234 )));
1235 }
1236 let path_repo = crate::path_repo::open(&repo.definition, project_dir, origin, arena)
1237 .map_err(|e| SessionError::new(e.0))?;
1238 return Ok(Repository::Path(path_repo));
1239 }
1240 open_repository_with(repo, http, cache_repo_dir, false)
1241}
1242
1243fn open_repository_with(
1247 repo: &RepoConfig,
1248 http: Option<&HttpTransports>,
1249 cache_repo_dir: Option<&Path>,
1250 for_policies: bool,
1251) -> Result<Repository, SessionError> {
1252 let def = &repo.definition;
1253 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1254 SessionError::new(format!(
1255 "Repository \"{}\" ({def}) must have a type defined",
1256 key_string(&repo.key)
1257 ))
1258 })?;
1259 if kind != "composer" {
1260 return Err(SessionError::new(format!(
1261 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1262 key_string(&repo.key)
1263 )));
1264 }
1265 if !for_policies
1266 && (def.get("only").is_some()
1267 || def.get("exclude").is_some()
1268 || def.get("canonical").is_some())
1269 {
1270 return Err(SessionError::new(format!(
1271 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1272 key_string(&repo.key)
1273 )));
1274 }
1275 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
1276 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
1277 })?;
1278 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
1279 Box::new(FileTransport)
1280 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
1281 match http {
1282 Some(h) => Box::new(HttpTransport {
1283 fetch: h.0.clone(),
1284 fetch_many: h.1.clone(),
1285 post: h.2.clone(),
1286 }),
1287 None => {
1288 return Err(SessionError::new(format!(
1289 "remote composer repositories need a network transport ({url})"
1290 )))
1291 }
1292 }
1293 } else {
1294 return Err(SessionError::new(format!(
1295 "unsupported repository url scheme ({url})"
1296 )));
1297 };
1298 let mut repo = ComposerRepository::open(url, transport).map_err(|e| SessionError::new(e.0))?;
1299 if let Some(options) = def.get("options") {
1300 repo.options = options.clone();
1301 }
1302 repo.set_user_filter(def.get("filter"))
1303 .map_err(|e| SessionError::new(e.0))?;
1304 if for_policies {
1305 repo.set_name_filter(def.get("only"), def.get("exclude"))
1306 .map_err(|e| SessionError::new(e.0))?;
1307 }
1308 if let Some(dir) = cache_repo_dir {
1309 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1310 }
1311 Ok(Repository::Composer(Box::new(repo)))
1312}
1313
1314fn key_string(key: &RepoKey) -> String {
1315 match key {
1316 RepoKey::Named(n) => n.clone(),
1317 RepoKey::Indexed(i) => i.to_string(),
1318 }
1319}
1320
1321#[cfg(test)]
1322mod tests {
1323 use super::*;
1324 use serde_json::json;
1325
1326 fn names(repos: &[RepoConfig]) -> Vec<String> {
1327 repos.iter().map(|r| key_string(&r.key)).collect()
1328 }
1329
1330 #[test]
1331 fn merges_repositories_like_composer_config() {
1332 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1333 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1334 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1336 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1337 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1339 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1340 cfg.merge(
1343 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1344 );
1345 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1346 assert_eq!(cfg.repositories[0].definition["url"], "a");
1347 assert_eq!(cfg.repositories[1].definition["url"], "b");
1348 assert_eq!(
1349 cfg.repositories[2].definition["url"],
1350 "https://packages.drupal.org/8"
1351 );
1352 }
1353
1354 #[test]
1355 fn packagist_url_disables_default() {
1356 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1357 cfg.merge(
1358 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1359 );
1360 assert_eq!(names(&cfg.repositories), vec!["0"]);
1361 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1362 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1363 assert!(cfg.repositories.is_empty());
1364 }
1365}