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 pub parts: Vec<(vivacity_core::io::Verbosity, String)>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SessionErrorKind {
49 Other,
50 Unsolvable,
51 Transport,
54 Printed(i32),
59}
60
61impl SessionError {
62 pub fn new(message: impl Into<String>) -> Self {
63 Self {
64 message: message.into(),
65 kind: SessionErrorKind::Other,
66 stdout: None,
67 parts: Vec::new(),
68 }
69 }
70}
71
72impl From<PoolError> for SessionError {
73 fn from(e: PoolError) -> SessionError {
74 let transport = e.is_transport();
75 SessionError {
76 message: e.0,
77 kind: if transport {
78 SessionErrorKind::Transport
79 } else {
80 SessionErrorKind::Other
81 },
82 stdout: None,
83 parts: Vec::new(),
84 }
85 }
86}
87
88macro_rules! session_error_from {
91 ($($ty:path),+ $(,)?) => {
92 $(impl From<$ty> for SessionError {
93 fn from(e: $ty) -> SessionError {
94 SessionError::new(e.0)
95 }
96 })+
97 };
98}
99
100session_error_from!(
101 crate::loader::LoadError,
102 crate::platform::PlatformError,
103 crate::policy_config::PolicyError,
104 crate::root::RootError,
105);
106
107impl From<crate::pool_filters::FilterError> for SessionError {
109 fn from(e: crate::pool_filters::FilterError) -> SessionError {
110 SessionError {
111 kind: if e.1 == crate::repository::RepoErrorKind::Transport {
112 SessionErrorKind::Transport
113 } else {
114 SessionErrorKind::Other
115 },
116 message: e.0,
117 stdout: None,
118 parts: Vec::new(),
119 }
120 }
121}
122
123impl From<crate::repository::RepoError> for SessionError {
128 fn from(e: crate::repository::RepoError) -> SessionError {
129 let transport = e.is_transport();
130 SessionError {
131 message: e.0,
132 kind: if transport {
133 SessionErrorKind::Transport
134 } else {
135 SessionErrorKind::Other
136 },
137 stdout: None,
138 parts: Vec::new(),
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
145pub struct RepoConfig {
146 pub key: RepoKey,
147 pub definition: Value,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum RepoKey {
152 Named(String),
153 Indexed(u64),
154}
155
156pub fn merge_repositories(current: &mut Vec<RepoConfig>, new: &Value) {
159 static PACKAGIST: OnceLock<Regex> = OnceLock::new();
160 let entries: Vec<(RepoKey, Value)> = match new {
161 Value::Array(list) => list
162 .iter()
163 .enumerate()
164 .map(|(i, v)| (RepoKey::Indexed(i as u64), v.clone()))
165 .collect(),
166 Value::Object(map) => map
167 .iter()
168 .map(|(k, v)| {
169 let key = match k.parse::<u64>() {
170 Ok(i) if i.to_string() == *k => RepoKey::Indexed(i),
171 _ => RepoKey::Named(k.clone()),
172 };
173 (key, v.clone())
174 })
175 .collect(),
176 _ => return,
177 };
178 if entries.is_empty() {
179 return;
180 }
181 current.reverse();
182 let disable = |current: &mut Vec<RepoConfig>, name: &str| {
185 let key = match name.parse::<u64>() {
186 Ok(i) if i.to_string() == name => RepoKey::Indexed(i),
187 _ => RepoKey::Named(name.to_owned()),
188 };
189 if current.iter().any(|r| r.key == key) {
190 current.retain(|r| r.key != key);
191 } else if name == "packagist" {
192 current.retain(|r| r.key != RepoKey::Named("packagist.org".into()));
193 }
194 };
195 for (key, repository) in entries.into_iter().rev() {
196 if repository == Value::Bool(false) {
197 let name = match &key {
198 RepoKey::Named(n) => n.clone(),
199 RepoKey::Indexed(i) => i.to_string(),
200 };
201 disable(current, &name);
202 continue;
203 }
204 if let Some(obj) = repository.as_object() {
205 if obj.len() == 1 && obj.values().next() == Some(&Value::Bool(false)) {
206 disable(current, obj.keys().next().map(String::as_str).unwrap_or(""));
207 continue;
208 }
209 }
210 if repository.get("type").and_then(Value::as_str) == Some("composer") {
211 if let Some(url) = repository.get("url").and_then(Value::as_str) {
212 let re = regex(
213 &PACKAGIST,
214 r"^https?://(?:[a-z0-9-.]+\.)?packagist.org(/|$)",
215 false,
216 );
217 if re.is_match(url.as_bytes()).unwrap_or(false) {
218 disable(current, "packagist.org");
219 }
220 }
221 }
222 match key {
223 RepoKey::Indexed(i) => {
224 if current.iter().any(|r| r.key == RepoKey::Indexed(i)) {
225 let next = current
226 .iter()
227 .filter_map(|r| match r.key {
228 RepoKey::Indexed(j) => Some(j + 1),
229 _ => None,
230 })
231 .max()
232 .unwrap_or(0);
233 current.push(RepoConfig {
234 key: RepoKey::Indexed(next),
235 definition: repository,
236 });
237 } else {
238 current.push(RepoConfig {
239 key: RepoKey::Indexed(i),
240 definition: repository,
241 });
242 }
243 }
244 RepoKey::Named(name) => {
245 let name = if name == "packagist" {
246 "packagist.org".to_owned()
247 } else {
248 name
249 };
250 let key = RepoKey::Named(name);
251 if let Some(slot) = current.iter_mut().find(|r| r.key == key) {
252 slot.definition = repository;
253 } else {
254 current.push(RepoConfig {
255 key,
256 definition: repository,
257 });
258 }
259 }
260 }
261 }
262 current.reverse();
263}
264
265#[derive(Debug, Clone, Default)]
267pub struct MergedConfig {
268 pub repositories: Vec<RepoConfig>,
269 pub platform: Map<String, Value>,
271 pub policy: crate::policy_config::RawPolicyConfig,
273}
274
275impl MergedConfig {
276 pub fn load(
279 manifest: &Value,
280 composer_home: Option<&Path>,
281 ) -> Result<MergedConfig, SessionError> {
282 let mut cfg = MergedConfig {
283 repositories: vec![RepoConfig {
284 key: RepoKey::Named("packagist.org".into()),
285 definition: serde_json::json!({"type": "composer", "url": "https://repo.packagist.org"}),
286 }],
287 platform: Map::new(),
288 policy: crate::policy_config::RawPolicyConfig::default(),
289 };
290 if let Some(home) = composer_home {
291 let global = home.join("config.json");
292 if global.is_file() {
293 let text = std::fs::read_to_string(&global)
294 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
295 let v: Value = serde_json::from_str(&text)
296 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
297 cfg.merge(&v);
298 }
299 }
300 cfg.merge(manifest);
301 Ok(cfg)
302 }
303
304 fn merge(&mut self, config: &Value) {
305 if let Some(platform) = config.get("config").and_then(|c| c.get("platform")) {
308 self.platform = platform.as_object().cloned().unwrap_or_default();
309 }
310 if let Some(repos) = config.get("repositories") {
311 merge_repositories(&mut self.repositories, repos);
312 }
313 if let Some(cfg) = config.get("config").and_then(Value::as_object) {
314 self.policy.merge(cfg);
315 }
316 }
317}
318
319pub struct SolveReport {
322 pub transaction: LockTransaction,
323 pub decisions: Vec<i64>,
325 pub rules: usize,
327 pub learned: usize,
329 pub events: Vec<(vivacity_core::io::Verbosity, String)>,
335}
336
337#[derive(Debug, Clone, Default)]
340pub struct UpdateOptions {
341 pub allow_list: Vec<String>,
343 pub transitive: Option<crate::pool::UpdateMode>,
344 pub no_blocking: bool,
346 pub root_patch: Option<crate::root::RootPatch>,
349 pub merged: Option<crate::merge_plugin::Merged>,
353 pub temporary_requirements: Vec<(String, String)>,
357}
358
359impl UpdateOptions {
360 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
362 let mut allow_list: Vec<String> = Vec::new();
363 for p in packages {
364 let l = p.to_lowercase();
365 if !allow_list.contains(&l) {
366 allow_list.push(l);
367 }
368 }
369 UpdateOptions {
370 allow_list,
371 transitive: Some(transitive),
372 ..Default::default()
373 }
374 }
375}
376
377fn temporary_constraints(
383 reqs: &[(String, String)],
384 root: &mut RootPackage,
385) -> Result<BTreeMap<String, crate::pool::TemporaryConstraint>, SessionError> {
386 if reqs.is_empty() {
387 return Ok(BTreeMap::new());
388 }
389 let pairs: Vec<(String, String)> = reqs.to_vec();
390 crate::root::extract_references(&pairs, &mut root.references);
391 let minimum = root.minimum_stability.clone();
392 crate::root::extract_stability_flags(&pairs, &minimum, &mut root.stability_flags);
393 let root_requires: Vec<(String, String, Constraint)> = root
397 .all_requires()
398 .iter()
399 .map(|l| {
400 (
401 l.target.clone(),
402 l.pretty_constraint.clone(),
403 l.constraint.clone(),
404 )
405 })
406 .collect();
407 let fail = |message: String, stdout: Option<String>| SessionError {
408 message,
409 kind: SessionErrorKind::Printed(1),
410 stdout,
411 parts: Vec::new(),
412 };
413 let mut out: BTreeMap<String, crate::pool::TemporaryConstraint> = BTreeMap::new();
414 for (name, text) in &pairs {
415 let name = name.to_lowercase();
416 let parsed = crate::pool::TemporaryConstraint {
417 pretty: text.clone(),
418 constraint: crate::constraint::parse_constraints(text)
419 .map_err(|e| SessionError::new(format!("{name}: {e}")))?
420 .constraint,
421 };
422 if name.contains('*') {
423 let re = crate::pool::package_name_regexp(&name);
424 for (target, pretty, constraint) in &root_requires {
425 if !re.is_match(target.as_bytes()).unwrap_or(false) {
426 continue;
427 }
428 out.insert(target.clone(), parsed.clone());
429 if !crate::intervals::have_intersections(&parsed.constraint, constraint) {
430 return Err(fail(
431 format!(
432 "The temporary constraint \"{text}\" for \"{name}\" matching \"{target}\" must be a subset of the constraint in your composer.json ({pretty})"
433 ),
434 None,
435 ));
436 }
437 }
438 } else {
439 out.insert(name.clone(), parsed.clone());
440 if let Some((_, pretty, constraint)) = root_requires.iter().find(|(t, _, _)| *t == name)
441 {
442 if !crate::intervals::have_intersections(&parsed.constraint, constraint) {
443 return Err(fail(
444 format!(
445 "The temporary constraint \"{text}\" for \"{name}\" must be a subset of the constraint in your composer.json ({pretty})"
446 ),
447 Some(format!(
448 "Run `composer require {name}` or `composer require {name}:{text}` instead to replace the constraint"
449 )),
450 ));
451 }
452 }
453 }
454 }
455 Ok(out)
456}
457
458#[derive(Debug, Clone)]
461pub struct FlexSession {
462 pub symfony_require: String,
463 pub symfony: Constraint,
464 pub versions: crate::flex_filter::FlexVersions,
465}
466
467pub struct UpdateSession {
468 pub flex: Option<FlexSession>,
470 pub arena: Vec<Package>,
471 pub root: RootPackage,
472 pub fixed_root: usize,
474 pub fixed_root_alias: Option<usize>,
475 pub platform: Vec<usize>,
476 pub locked: Option<Vec<usize>>,
477 pub set: RepositorySet,
478 pub request: Request,
479 pub config: MergedConfig,
480 pub dev_mode: bool,
481 pub prefer_stable: bool,
483 pub prefer_lowest: bool,
484 pub php_version: String,
487 pub policy_config: crate::policy_config::PolicyConfig,
489 pub ini_files: Vec<String>,
492 pub loaded_extensions: BTreeSet<String>,
494 pub installer_dev_mode: bool,
497}
498
499impl UpdateSession {
500 pub fn prepare(
501 project_dir: &Path,
502 composer_home: Option<&Path>,
503 dev_mode: bool,
504 ) -> Result<UpdateSession, SessionError> {
505 Self::prepare_with(project_dir, composer_home, dev_mode, None)
506 }
507
508 pub fn prepare_with(
510 project_dir: &Path,
511 composer_home: Option<&Path>,
512 dev_mode: bool,
513 http: Option<HttpTransports>,
514 ) -> Result<UpdateSession, SessionError> {
515 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
516 }
517
518 pub fn prepare_full(
521 project_dir: &Path,
522 composer_home: Option<&Path>,
523 dev_mode: bool,
524 http: Option<HttpTransports>,
525 cache_repo_dir: Option<&Path>,
526 ) -> Result<UpdateSession, SessionError> {
527 Self::prepare_update(
528 project_dir,
529 composer_home,
530 dev_mode,
531 http,
532 cache_repo_dir,
533 &UpdateOptions::default(),
534 )
535 }
536
537 pub fn prepare_update(
540 project_dir: &Path,
541 composer_home: Option<&Path>,
542 dev_mode: bool,
543 http: Option<HttpTransports>,
544 cache_repo_dir: Option<&Path>,
545 options: &UpdateOptions,
546 ) -> Result<UpdateSession, SessionError> {
547 let partial_update = !options.allow_list.is_empty();
548 let manifest_path = project_dir.join("composer.json");
549 let manifest_text = std::fs::read_to_string(&manifest_path)
550 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
551 let manifest: Value = serde_json::from_str(&manifest_text)
552 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
553 let config = MergedConfig::load(&manifest, composer_home)?;
554 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
555 .map_err(SessionError::from)?;
556 policy_config
557 .apply_no_blocking(options.no_blocking)
558 .map_err(SessionError::from)?;
559 let mut root = RootPackage::load(&manifest, project_dir).map_err(SessionError::from)?;
560 if let Some(merged) = &options.merged {
561 let flags = std::mem::take(&mut root.stability_flags);
568 root = RootPackage::load(&merged.manifest, project_dir).map_err(SessionError::from)?;
569 root.stability_flags = flags;
570 for include in &merged.include_links {
573 crate::root::merge_plugin_stability_flags(
574 &mut root.stability_flags,
575 &root.minimum_stability,
576 &include.require,
577 );
578 }
579 for include in &merged.include_links {
580 crate::root::merge_plugin_stability_flags(
581 &mut root.stability_flags,
582 &root.minimum_stability,
583 &include.require_dev,
584 );
585 }
586 }
587 if let Some(patch) = &options.root_patch {
588 root.apply_patch(patch).map_err(SessionError::from)?;
589 }
590 let temporary_constraints =
594 temporary_constraints(&options.temporary_requirements, &mut root)?;
595 let probed = probe().map_err(SessionError::from)?;
596 let php_version = probed
600 .iter()
601 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
602 .and_then(|p| p.get("version").and_then(Value::as_str))
603 .map(|v| {
604 v.split(['.', '-', '+'])
605 .take(3)
606 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
607 .collect::<Vec<_>>()
608 .join(".")
609 })
610 .unwrap_or_default();
611 let platform_pkgs =
612 platform_packages(&probed, &config.platform).map_err(SessionError::from)?;
613
614 let mut arena: Vec<Package> = Vec::new();
615
616 let mut fixed = root.package.clone();
618 fixed.requires = Default::default();
619 fixed.dev_requires = Default::default();
620 arena.push(fixed);
621 let fixed_root = 0;
622 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
623 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
624 arena.push(a);
625 arena.len() - 1
626 });
627 let root_members: Vec<usize> = fixed_root_alias
628 .into_iter()
629 .chain(std::iter::once(fixed_root))
630 .collect();
631
632 let mut platform: Vec<usize> = Vec::new();
633 for p in platform_pkgs {
634 arena.push(p);
635 platform.push(arena.len() - 1);
636 }
637
638 let lock_path = project_dir.join("composer.lock");
642 let lock: Option<Value> = if lock_path.is_file() {
643 let text = std::fs::read_to_string(&lock_path)
644 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
645 match serde_json::from_str::<Value>(&text) {
646 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
647 Ok(_) => None,
648 Err(e) => {
649 if partial_update {
650 return Err(SessionError::new(format!(
651 "\"{}\" does not contain valid JSON\n{e}",
652 lock_path.display()
653 )));
654 }
655 None
656 }
657 }
658 } else {
659 None
660 };
661 let locked = match &lock {
662 Some(v) => Some(locked_repository(v, &mut arena).map_err(SessionError::from)?),
663 None => None,
664 };
665
666 let mut stability_flags = root.stability_flags.clone();
669 let root_version = match fixed_root_alias {
670 Some(a) => arena[a].version.clone(),
671 None => arena[fixed_root].version.clone(),
672 };
673 stability_flags.insert(
674 root.package.name.clone(),
675 stability_rank(parse_stability(&root_version)),
676 );
677
678 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
683 let requires = match &root.branch_alias {
684 Some((normalized, pretty)) => {
685 let aliased = root.package.alias(0, normalized, pretty);
686 let mut all = aliased.requires.clone();
687 for l in aliased.dev_requires.iter() {
688 all.insert(l.clone());
689 }
690 all
691 }
692 None => root.all_requires(),
693 };
694 for link in requires.iter() {
695 root_requires.insert(&link.target, link.constraint.clone());
696 }
697
698 let mut set = RepositorySet::new(
699 &root.minimum_stability,
700 stability_flags,
701 &root.aliases,
702 root.references.clone(),
703 root_requires,
704 temporary_constraints,
705 );
706 set.add_repository(Repository::Root(root_members));
707 set.add_repository(Repository::Platform(platform.clone()));
708 for repo in &config.repositories {
709 let origin = Origin::Repository(set.repositories.len());
710 set.add_repository(open_repository(
711 repo,
712 http.as_ref(),
713 cache_repo_dir,
714 project_dir,
715 origin,
716 &mut arena,
717 )?);
718 }
719 if let Some(ids) = &locked {
720 set.add_repository(Repository::Locked(ids.clone()));
721 }
722
723 if partial_update && locked.is_none() {
726 return Err(SessionError {
727 message: "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.".to_owned(),
728 kind: SessionErrorKind::Printed(3),
729 stdout: None,
730 parts: vec![(
732 vivacity_core::io::Verbosity::Quiet,
733 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.".to_owned(),
734 )],
735 });
736 }
737 let mut request = Request::new(locked.clone());
738 if partial_update {
739 request.set_update_allow_list(
740 options.allow_list.clone(),
741 options
742 .transitive
743 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
744 );
745 }
746 if let Some(a) = fixed_root_alias {
747 request.fix_package(a);
748 }
749 request.fix_package(fixed_root);
750 for &p in &platform {
751 let provided = arena[fixed_root]
752 .provides
753 .get(&arena[p].name)
754 .map(|l| l.constraint.clone());
755 let provided_here = provided
756 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
757 if !provided_here {
758 request.fix_package(p);
759 }
760 }
761 for link in requires.iter() {
762 request.require_name_pretty(
763 &link.target,
764 Some(link.constraint.clone()),
765 &link.pretty_constraint,
766 )?;
767 }
768
769 Ok(UpdateSession {
770 flex: None,
771 arena,
772 root,
773 fixed_root,
774 fixed_root_alias,
775 platform,
776 locked,
777 set,
778 request,
779 config,
780 dev_mode,
781 prefer_stable: false,
782 prefer_lowest: false,
783 php_version,
784 policy_config,
785 ini_files: crate::platform::ini_files(&probed),
786 loaded_extensions: crate::platform::loaded_extensions(&probed),
787 installer_dev_mode: true,
788 })
789 }
790
791 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
792 let pre_pool = self.flex.as_ref().map(|f| {
793 let mut root_constraints = BTreeMap::new();
796 for link in self
797 .root
798 .package
799 .requires
800 .0
801 .iter()
802 .chain(self.root.package.dev_requires.0.iter())
803 {
804 root_constraints
805 .entry(link.target.clone())
806 .or_insert_with(|| link.constraint.clone());
807 }
808 crate::pool::PrePoolFilter {
809 symfony_require: f.symfony_require.clone(),
810 symfony: f.symfony.clone(),
811 root_constraints,
812 versions: f.versions.clone(),
813 }
814 });
815 Ok(self
816 .set
817 .create_pool(&mut self.request, &mut self.arena, pre_pool.as_ref())?)
818 }
819
820 pub fn policy(&self) -> DefaultPolicy {
824 let mut policy = DefaultPolicy::new(
825 self.prefer_stable || self.root.prefer_stable,
826 self.prefer_lowest,
827 None,
828 );
829 if self.flex.is_some() {
830 policy.prefer_dev_over_prerelease = true;
831 }
832 policy
833 }
834
835 pub fn extract_dev_packages(
839 &mut self,
840 transaction: &mut LockTransaction,
841 policy: &mut DefaultPolicy,
842 filter: &PlatformRequirementFilter,
843 events: &mut Vec<(vivacity_core::io::Verbosity, String)>,
844 ) -> Result<(), SessionError> {
845 if self.root.package.dev_requires.is_empty() {
846 return Ok(());
847 }
848 let dumps: Vec<Value> = transaction
851 .new_lock_packages(&self.arena, false)
852 .iter()
853 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
854 .collect();
855 let result_ids =
856 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
857 .map_err(SessionError::from)?;
858 let mut members: Vec<usize> = Vec::new();
861 members.extend(self.fixed_root_alias);
862 members.push(self.fixed_root);
863 members.extend(self.platform.iter().copied());
864 members.extend(result_ids);
865 let mut pool_packages: Vec<usize> = Vec::new();
866 for idx in members {
867 pool_packages.push(idx);
868 let (name, version) = (
869 self.arena[idx].name.clone(),
870 self.arena[idx].version.clone(),
871 );
872 if let Some((alias, alias_normalized)) = self
873 .set
874 .root_aliases
875 .get(&name)
876 .and_then(|m| m.get(&version))
877 {
878 let mut base = idx;
879 while let Some(b) = self.arena[base].alias_of {
880 base = b;
881 }
882 let mut a = self.arena[base].alias(base, alias_normalized, alias);
883 a.root_package_alias = true;
884 a.origin = Origin::Detached;
885 self.arena.push(a);
886 pool_packages.push(self.arena.len() - 1);
887 }
888 }
889 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
890 let mut request = Request::new(None);
892 if let Some(a) = self.fixed_root_alias {
893 request.fix_package(a);
894 }
895 request.fix_package(self.fixed_root);
896 for &p in &self.platform {
897 let provided = self.arena[self.fixed_root]
898 .provides
899 .get(&self.arena[p].name)
900 .map(|l| l.constraint.clone());
901 let provided_here = provided.is_some_and(|c| {
902 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
903 });
904 if !provided_here {
905 request.fix_package(p);
906 }
907 }
908 let requires = match &self.root.branch_alias {
909 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
910 None => self.root.package.requires.clone(),
911 };
912 for link in requires.iter() {
913 request.require_name_pretty(
914 &link.target,
915 Some(link.constraint.clone()),
916 &link.pretty_constraint,
917 )?;
918 }
919 let mut solver = Solver::new(&pool, &self.arena);
920 let before = std::time::Instant::now();
921 let solved = solver.solve(&request, policy, filter);
922 events.push(resolution_completed(before));
925 let non_dev = match solved {
926 Ok(t) => t,
927 Err(SolveError::Problems(problems)) => {
928 drop(solver);
929 let pretty = {
932 let mut ctx = crate::problem::MessageContext {
933 arena: &mut self.arena,
934 set: &self.set,
935 pool: &pool,
936 request: &request,
937 is_verbose: vivacity_core::io::shows(vivacity_core::io::Verbosity::Verbose),
938 ini_files: &self.ini_files,
939 loaded_extensions: &self.loaded_extensions,
940 };
941 crate::problem::pretty_string(&mut ctx, &problems, true)
942 };
943 let head = "Unable to find a compatible set of packages based on your non-dev requirements alone.";
946 let rest = format!("Your 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}");
947 return Err(SessionError {
948 message: format!("{head}\n{rest}"),
949 kind: SessionErrorKind::Unsolvable,
950 stdout: None,
951 parts: vec![
952 (vivacity_core::io::Verbosity::Quiet, head.to_owned()),
953 (vivacity_core::io::Verbosity::Normal, rest),
954 ],
955 });
956 }
957 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
958 };
959 transaction.set_non_dev_packages(&self.arena, &non_dev);
960 Ok(())
961 }
962
963 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
965 let mut out = Map::new();
966 for l in links.iter() {
967 if crate::platform::is_platform_package(&l.target) {
968 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
969 }
970 }
971 out
972 }
973
974 pub fn lock_json(
976 &self,
977 transaction: &LockTransaction,
978 manifest_text: &str,
979 ) -> Result<Value, SessionError> {
980 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
981 .map_err(|e| SessionError::new(e.to_string()))?;
982 let packages = lock_packages(
983 &self.arena,
984 &transaction.new_lock_packages(&self.arena, false),
985 )
986 .map_err(SessionError::new)?;
987 let packages_dev = lock_packages(
988 &self.arena,
989 &transaction.new_lock_packages(&self.arena, true),
990 )
991 .map_err(SessionError::new)?;
992 let (requires, dev_requires) = match &self.root.branch_alias {
993 Some((normalized, pretty)) => {
994 let a = self.root.package.alias(0, normalized, pretty);
995 (a.requires, a.dev_requires)
996 }
997 None => (
998 self.root.package.requires.clone(),
999 self.root.package.dev_requires.clone(),
1000 ),
1001 };
1002 Ok(lock_data(LockInput {
1003 content_hash: &content_hash,
1004 packages,
1005 packages_dev: Some(packages_dev),
1006 platform: Self::platform_requirements(&requires),
1007 platform_dev: Self::platform_requirements(&dev_requires),
1008 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
1009 minimum_stability: &self.root.minimum_stability,
1010 stability_flags: &self.root.stability_flags,
1011 prefer_stable: self.prefer_stable || self.root.prefer_stable,
1012 prefer_lowest: self.prefer_lowest,
1013 platform_overrides: &self.config.platform,
1014 }))
1015 }
1016
1017 pub fn update(
1020 &mut self,
1021 manifest_text: &str,
1022 filter: &PlatformRequirementFilter,
1023 ) -> Result<(Value, SolveReport), SessionError> {
1024 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
1025 let t = std::time::Instant::now();
1026 let lap = |label: &str, t: &std::time::Instant| {
1027 if trace {
1028 eprintln!(
1029 "trace: {label:<22} {:>7.1} ms",
1030 t.elapsed().as_secs_f64() * 1000.0
1031 );
1032 }
1033 };
1034 let mut policy = self.policy();
1035 let pool = self.create_filtered_pool()?;
1036 if let Some(notice) = &pool.flex_notice {
1040 vivacity_core::say!("{notice}");
1041 }
1042 vivacity_core::say!("Updating dependencies");
1043 for w in &pool.warnings {
1044 vivacity_core::say!("{w}");
1045 }
1046 lap("pool", &t);
1047 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
1048 pool
1049 } else {
1050 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
1051 };
1052 lap("optimize", &t);
1053 let before = std::time::Instant::now();
1054 let solved = self.solve(&pool, &mut policy, filter);
1055 let completed = resolution_completed(before);
1056 let mut report = match solved {
1057 Ok(mut r) => {
1058 r.events.push(completed);
1059 r
1060 }
1061 Err(SolveError::Problems(problems)) => {
1062 say_event(&completed);
1064 let pretty = self.pretty_problems(&pool, &problems, false);
1069 let head =
1070 "Your requirements could not be resolved to an installable set of packages.";
1071 let mut parts = vec![
1072 (vivacity_core::io::Verbosity::Quiet, head.to_owned()),
1073 (vivacity_core::io::Verbosity::Normal, pretty),
1074 ];
1075 if !self.installer_dev_mode {
1076 parts.push((vivacity_core::io::Verbosity::Quiet, "Running 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.".to_owned()));
1077 }
1078 let message = parts
1079 .iter()
1080 .map(|(_, p)| p.as_str())
1081 .collect::<Vec<_>>()
1082 .join("\n");
1083 return Err(SessionError {
1084 message,
1085 kind: SessionErrorKind::Unsolvable,
1086 stdout: None,
1087 parts,
1088 });
1089 }
1090 Err(SolveError::Bug(b)) => {
1091 say_event(&completed);
1092 return Err(SessionError::new(b));
1093 }
1094 };
1095 lap("solve", &t);
1096 for &idx in &report.transaction.all {
1100 if let Err(e) = crate::lockfile::validate_package(&self.arena[idx]) {
1101 say_events(&report.events);
1102 return Err(SessionError::new(e));
1103 }
1104 }
1105 use vivacity_core::io::Verbosity;
1109 report.events.push((
1110 Verbosity::Verbose,
1111 format!("Analyzed {} packages to resolve dependencies", pool.len()),
1112 ));
1113 report.events.push((
1114 Verbosity::Verbose,
1115 format!("Analyzed {} rules to resolve dependencies", report.rules),
1116 ));
1117 if report.transaction.transaction.operations.is_empty() {
1118 report.events.push((
1119 Verbosity::Normal,
1120 "Nothing to modify in lock file".to_owned(),
1121 ));
1122 }
1123 drop(pool);
1124 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
1125 if let Err(e) =
1126 self.extract_dev_packages(&mut transaction, &mut policy, filter, &mut report.events)
1127 {
1128 say_events(&report.events);
1130 return Err(e);
1131 }
1132 lap("extract dev", &t);
1133 let lock = self.lock_json(&transaction, manifest_text)?;
1134 lap("lock data", &t);
1135 report.transaction = transaction;
1136 Ok((lock, report))
1137 }
1138
1139 pub fn pretty_problems(
1142 &mut self,
1143 pool: &Pool,
1144 problems: &[crate::solver::SolvedProblem],
1145 is_dev_extraction: bool,
1146 ) -> String {
1147 let mut ctx = crate::problem::MessageContext {
1148 arena: &mut self.arena,
1149 set: &self.set,
1150 pool,
1151 request: &self.request,
1152 is_verbose: vivacity_core::io::shows(vivacity_core::io::Verbosity::Verbose),
1153 ini_files: &self.ini_files,
1154 loaded_extensions: &self.loaded_extensions,
1155 };
1156 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
1157 }
1158
1159 pub fn find_packages_for_require(
1166 &mut self,
1167 name: &str,
1168 ignore_stability: bool,
1169 ) -> Result<Vec<usize>, SessionError> {
1170 let name = name.to_lowercase();
1171 let mut acceptable = BTreeMap::new();
1172 let min = crate::version::stability_rank(&self.root.minimum_stability);
1173 for st in ["stable", "RC", "beta", "alpha", "dev"] {
1174 let rank = crate::version::stability_rank(st);
1175 if ignore_stability || rank <= min {
1176 acceptable.insert(st.to_owned(), rank);
1177 }
1178 }
1179 let flags = BTreeMap::new();
1180 let already = BTreeMap::new();
1181 let map = vec![(name.clone(), Constraint::MatchAll)];
1182 let mut found: Vec<usize> = Vec::new();
1183 let (_, ids) = crate::pool::array_repository_load_packages(
1184 &self.platform,
1185 &map,
1186 &acceptable,
1187 &flags,
1188 &already,
1189 &self.arena,
1190 );
1191 found.extend(ids);
1192 for (i, repo) in self.set.repositories.iter().enumerate() {
1193 if let Repository::Composer(repo) = repo {
1194 let (_, ids) = repo
1195 .load_packages(
1196 &map,
1197 &acceptable,
1198 &flags,
1199 &already,
1200 Origin::Repository(i),
1201 &mut self.arena,
1202 )
1203 .map_err(SessionError::from)?;
1204 found.extend(ids);
1205 }
1206 }
1207 Ok(found)
1208 }
1209
1210 pub fn create_optimized_pool(
1211 &mut self,
1212 policy: &mut DefaultPolicy,
1213 ) -> Result<Pool, SessionError> {
1214 let pool = self.create_filtered_pool()?;
1215 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
1216 return Ok(pool);
1217 }
1218 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
1219 }
1220
1221 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
1224 let mut pool = self.create_pool()?;
1225 let before = pool.len();
1226 let flex_notice = pool.flex_notice.take();
1229 let mut warnings = Vec::new();
1230 pool = crate::pool_filters::security_advisory_filter(
1231 pool,
1232 &self.arena,
1233 &self.set.repositories,
1234 &self.request,
1235 &self.policy_config,
1236 &mut warnings,
1237 )
1238 .map_err(SessionError::from)?;
1239 pool = crate::pool_filters::filter_list_filter(
1240 pool,
1241 &self.arena,
1242 &self.set.repositories,
1243 &self.request,
1244 &self.policy_config,
1245 "update",
1246 &mut warnings,
1247 )
1248 .map_err(SessionError::from)?;
1249 pool.warnings.extend(warnings);
1250 pool.flex_notice = flex_notice;
1251 if std::env::var_os("VIVACITY_TRACE").is_some() {
1252 eprintln!(
1253 "trace: policy filters {before} → {} package versions ({} removed by lists)",
1254 pool.len(),
1255 pool.filter_list_removed
1256 .values()
1257 .map(Vec::len)
1258 .sum::<usize>()
1259 );
1260 }
1261 Ok(pool)
1262 }
1263
1264 pub fn solve(
1267 &self,
1268 pool: &Pool,
1269 policy: &mut DefaultPolicy,
1270 filter: &PlatformRequirementFilter,
1271 ) -> Result<SolveReport, SolveError> {
1272 let mut solver = Solver::new(pool, &self.arena);
1273 let transaction = solver.solve(&self.request, policy, filter)?;
1274 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
1275 Ok(SolveReport {
1276 learned: solver
1277 .rules
1278 .ids_of_type(crate::rule::RuleType::Learned)
1279 .len(),
1280 rules: solver.rule_set_size(),
1281 decisions,
1282 transaction,
1283 events: Vec::new(),
1284 })
1285 }
1286}
1287
1288fn resolution_completed(before: std::time::Instant) -> (vivacity_core::io::Verbosity, String) {
1291 (
1292 vivacity_core::io::Verbosity::Verbose,
1293 format!(
1294 "Dependency resolution completed in {:.3} seconds",
1295 before.elapsed().as_secs_f64()
1296 ),
1297 )
1298}
1299
1300fn say_event((level, line): &(vivacity_core::io::Verbosity, String)) {
1302 if vivacity_core::io::shows(*level) {
1303 eprintln!("{line}");
1304 }
1305}
1306
1307fn say_events(events: &[(vivacity_core::io::Verbosity, String)]) {
1310 events.iter().for_each(say_event);
1311}
1312
1313pub fn install_policy_problems(
1319 project_dir: &Path,
1320 composer_home: Option<&Path>,
1321 http: Option<HttpTransports>,
1322 cache_repo_dir: Option<&Path>,
1323 with_dev: bool,
1324 no_blocking: bool,
1325) -> Result<(Vec<String>, Vec<String>), SessionError> {
1326 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
1327 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1328 let manifest: Value = serde_json::from_str(&manifest_text)
1329 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1330 let config = MergedConfig::load(&manifest, composer_home)?;
1331 let mut policy =
1332 crate::policy_config::PolicyConfig::from_raw(&config.policy).map_err(SessionError::from)?;
1333 policy
1334 .apply_no_blocking(no_blocking)
1335 .map_err(SessionError::from)?;
1336 if !policy.malware_blocks("install") {
1337 return Ok((Vec::new(), Vec::new()));
1338 }
1339 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
1340 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1341 let lock: Value = serde_json::from_str(&lock_text)
1342 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1343 let mut repositories: Vec<Repository> = Vec::new();
1346 for repo in &config.repositories {
1347 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
1348 continue;
1349 }
1350 repositories.push(open_repository_with(
1353 repo,
1354 http.as_ref(),
1355 cache_repo_dir,
1356 true,
1357 )?);
1358 }
1359 let mut arena: Vec<Package> = Vec::new();
1360 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
1361 .map_err(SessionError::from)?;
1362 let mut request = Request::new(Some(locked.clone()));
1363 for &idx in &locked {
1364 request.fix_locked_package(idx);
1365 }
1366 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
1367 let mut warnings = Vec::new();
1368 let pool = crate::pool_filters::filter_list_filter(
1369 pool,
1370 &arena,
1371 &repositories,
1372 &request,
1373 &policy,
1374 "install",
1375 &mut warnings,
1376 )
1377 .map_err(SessionError::from)?;
1378 let problems: Vec<String> = locked
1379 .iter()
1380 .map(|&idx| &arena[idx])
1381 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
1382 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
1383 .collect();
1384 Ok((problems, warnings))
1385}
1386
1387fn open_repository(
1391 repo: &RepoConfig,
1392 http: Option<&HttpTransports>,
1393 cache_repo_dir: Option<&Path>,
1394 project_dir: &Path,
1395 origin: Origin,
1396 arena: &mut Vec<Package>,
1397) -> Result<Repository, SessionError> {
1398 if repo.definition.get("type").and_then(Value::as_str) == Some("path") {
1399 if repo.definition.get("only").is_some()
1400 || repo.definition.get("exclude").is_some()
1401 || repo.definition.get("canonical").is_some()
1402 {
1403 return Err(SessionError::new(format!(
1404 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1405 key_string(&repo.key)
1406 )));
1407 }
1408 let path_repo = crate::path_repo::open(&repo.definition, project_dir, origin, arena)
1409 .map_err(SessionError::from)?;
1410 return Ok(Repository::Path(path_repo));
1411 }
1412 open_repository_with(repo, http, cache_repo_dir, false)
1413}
1414
1415fn open_repository_with(
1419 repo: &RepoConfig,
1420 http: Option<&HttpTransports>,
1421 cache_repo_dir: Option<&Path>,
1422 for_policies: bool,
1423) -> Result<Repository, SessionError> {
1424 let def = &repo.definition;
1425 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1426 SessionError::new(format!(
1427 "Repository \"{}\" ({def}) must have a type defined",
1428 key_string(&repo.key)
1429 ))
1430 })?;
1431 if kind != "composer" {
1432 return Err(SessionError::new(format!(
1433 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1434 key_string(&repo.key)
1435 )));
1436 }
1437 if !for_policies
1438 && (def.get("only").is_some()
1439 || def.get("exclude").is_some()
1440 || def.get("canonical").is_some())
1441 {
1442 return Err(SessionError::new(format!(
1443 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1444 key_string(&repo.key)
1445 )));
1446 }
1447 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
1448 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
1449 })?;
1450 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
1451 Box::new(FileTransport)
1452 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
1453 match http {
1454 Some(h) => Box::new(HttpTransport {
1455 fetch: h.0.clone(),
1456 fetch_many: h.1.clone(),
1457 post: h.2.clone(),
1458 }),
1459 None => {
1460 return Err(SessionError::new(format!(
1461 "remote composer repositories need a network transport ({url})"
1462 )))
1463 }
1464 }
1465 } else {
1466 return Err(SessionError::new(format!(
1467 "unsupported repository url scheme ({url})"
1468 )));
1469 };
1470 let mut repo = ComposerRepository::open(url, transport).map_err(SessionError::from)?;
1471 if let Some(options) = def.get("options") {
1472 repo.options = options.clone();
1473 }
1474 repo.set_user_filter(def.get("filter"))
1475 .map_err(SessionError::from)?;
1476 if for_policies {
1477 repo.set_name_filter(def.get("only"), def.get("exclude"))
1478 .map_err(SessionError::from)?;
1479 }
1480 if let Some(dir) = cache_repo_dir {
1481 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1482 }
1483 Ok(Repository::Composer(Box::new(repo)))
1484}
1485
1486fn key_string(key: &RepoKey) -> String {
1487 match key {
1488 RepoKey::Named(n) => n.clone(),
1489 RepoKey::Indexed(i) => i.to_string(),
1490 }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496 use serde_json::json;
1497
1498 fn names(repos: &[RepoConfig]) -> Vec<String> {
1499 repos.iter().map(|r| key_string(&r.key)).collect()
1500 }
1501
1502 #[test]
1503 fn merges_repositories_like_composer_config() {
1504 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1505 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1506 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1508 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1509 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1511 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1512 cfg.merge(
1515 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1516 );
1517 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1518 assert_eq!(cfg.repositories[0].definition["url"], "a");
1519 assert_eq!(cfg.repositories[1].definition["url"], "b");
1520 assert_eq!(
1521 cfg.repositories[2].definition["url"],
1522 "https://packages.drupal.org/8"
1523 );
1524 }
1525
1526 #[test]
1527 fn packagist_url_disables_default() {
1528 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1529 cfg.merge(
1530 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1531 );
1532 assert_eq!(names(&cfg.repositories), vec!["0"]);
1533 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1534 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1535 assert!(cfg.repositories.is_empty());
1536 }
1537}