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