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 pub annotation: Option<String>,
47 pub preface: Box<[String]>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum SessionErrorKind {
59 Other,
60 Unsolvable,
61 Transport,
64 Unsupported,
68 Printed(i32),
73}
74
75impl SessionError {
76 pub fn new(message: impl Into<String>) -> Self {
77 Self {
78 message: message.into(),
79 kind: SessionErrorKind::Other,
80 stdout: None,
81 parts: Vec::new(),
82 annotation: None,
83 preface: Box::default(),
84 }
85 }
86}
87
88impl From<PoolError> for SessionError {
89 fn from(e: PoolError) -> SessionError {
90 let transport = e.is_transport();
91 SessionError {
92 message: e.0,
93 kind: if transport {
94 SessionErrorKind::Transport
95 } else {
96 SessionErrorKind::Other
97 },
98 stdout: None,
99 parts: Vec::new(),
100 annotation: None,
101 preface: Box::default(),
102 }
103 }
104}
105
106macro_rules! session_error_from {
109 ($($ty:path),+ $(,)?) => {
110 $(impl From<$ty> for SessionError {
111 fn from(e: $ty) -> SessionError {
112 SessionError::new(e.0)
113 }
114 })+
115 };
116}
117
118session_error_from!(
119 crate::loader::LoadError,
120 crate::platform::PlatformError,
121 crate::policy_config::PolicyError,
122 crate::root::RootError,
123);
124
125impl From<crate::pool_filters::FilterError> for SessionError {
127 fn from(e: crate::pool_filters::FilterError) -> SessionError {
128 SessionError {
129 kind: if e.1 == crate::repository::RepoErrorKind::Transport {
130 SessionErrorKind::Transport
131 } else {
132 SessionErrorKind::Other
133 },
134 message: e.0,
135 stdout: None,
136 parts: Vec::new(),
137 annotation: None,
138 preface: Box::default(),
139 }
140 }
141}
142
143impl From<crate::repository::RepoError> for SessionError {
148 fn from(e: crate::repository::RepoError) -> SessionError {
149 SessionError {
150 kind: match e.1 {
151 crate::repository::RepoErrorKind::Transport => SessionErrorKind::Transport,
152 crate::repository::RepoErrorKind::Unsupported => SessionErrorKind::Unsupported,
153 crate::repository::RepoErrorKind::Data => SessionErrorKind::Other,
154 },
155 message: e.0,
156 stdout: None,
157 parts: Vec::new(),
158 annotation: None,
159 preface: Box::default(),
160 }
161 }
162}
163
164#[derive(Debug, Clone, PartialEq)]
166pub struct RepoConfig {
167 pub key: RepoKey,
168 pub definition: Value,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub enum RepoKey {
173 Named(String),
174 Indexed(u64),
175}
176
177pub fn merge_repositories(current: &mut Vec<RepoConfig>, new: &Value) {
180 static PACKAGIST: OnceLock<Regex> = OnceLock::new();
181 let entries: Vec<(RepoKey, Value)> = match new {
182 Value::Array(list) => list
183 .iter()
184 .enumerate()
185 .map(|(i, v)| (RepoKey::Indexed(i as u64), v.clone()))
186 .collect(),
187 Value::Object(map) => map
188 .iter()
189 .map(|(k, v)| {
190 let key = match k.parse::<u64>() {
191 Ok(i) if i.to_string() == *k => RepoKey::Indexed(i),
192 _ => RepoKey::Named(k.clone()),
193 };
194 (key, v.clone())
195 })
196 .collect(),
197 _ => return,
198 };
199 if entries.is_empty() {
200 return;
201 }
202 current.reverse();
203 let disable = |current: &mut Vec<RepoConfig>, name: &str| {
206 let key = match name.parse::<u64>() {
207 Ok(i) if i.to_string() == name => RepoKey::Indexed(i),
208 _ => RepoKey::Named(name.to_owned()),
209 };
210 if current.iter().any(|r| r.key == key) {
211 current.retain(|r| r.key != key);
212 } else if name == "packagist" {
213 current.retain(|r| r.key != RepoKey::Named("packagist.org".into()));
214 }
215 };
216 for (key, repository) in entries.into_iter().rev() {
217 if repository == Value::Bool(false) {
218 let name = match &key {
219 RepoKey::Named(n) => n.clone(),
220 RepoKey::Indexed(i) => i.to_string(),
221 };
222 disable(current, &name);
223 continue;
224 }
225 if let Some(obj) = repository.as_object() {
226 if obj.len() == 1 && obj.values().next() == Some(&Value::Bool(false)) {
227 disable(current, obj.keys().next().map(String::as_str).unwrap_or(""));
228 continue;
229 }
230 }
231 if repository.get("type").and_then(Value::as_str) == Some("composer") {
232 if let Some(url) = repository.get("url").and_then(Value::as_str) {
233 let re = regex(
234 &PACKAGIST,
235 r"^https?://(?:[a-z0-9-.]+\.)?packagist.org(/|$)",
236 false,
237 );
238 if re.is_match(url.as_bytes()).unwrap_or(false) {
239 disable(current, "packagist.org");
240 }
241 }
242 }
243 match key {
244 RepoKey::Indexed(i) => {
245 if current.iter().any(|r| r.key == RepoKey::Indexed(i)) {
246 let next = current
247 .iter()
248 .filter_map(|r| match r.key {
249 RepoKey::Indexed(j) => Some(j + 1),
250 _ => None,
251 })
252 .max()
253 .unwrap_or(0);
254 current.push(RepoConfig {
255 key: RepoKey::Indexed(next),
256 definition: repository,
257 });
258 } else {
259 current.push(RepoConfig {
260 key: RepoKey::Indexed(i),
261 definition: repository,
262 });
263 }
264 }
265 RepoKey::Named(name) => {
266 let name = if name == "packagist" {
267 "packagist.org".to_owned()
268 } else {
269 name
270 };
271 let key = RepoKey::Named(name);
272 if let Some(slot) = current.iter_mut().find(|r| r.key == key) {
273 slot.definition = repository;
274 } else {
275 current.push(RepoConfig {
276 key,
277 definition: repository,
278 });
279 }
280 }
281 }
282 }
283 current.reverse();
284}
285
286#[derive(Debug, Clone, Default)]
288pub struct MergedConfig {
289 pub repositories: Vec<RepoConfig>,
290 pub platform: Map<String, Value>,
292 pub policy: crate::policy_config::RawPolicyConfig,
294}
295
296impl MergedConfig {
297 pub fn load(
300 manifest: &Value,
301 composer_home: Option<&Path>,
302 ) -> Result<MergedConfig, SessionError> {
303 let mut cfg = MergedConfig {
304 repositories: vec![RepoConfig {
305 key: RepoKey::Named("packagist.org".into()),
306 definition: serde_json::json!({"type": "composer", "url": "https://repo.packagist.org"}),
307 }],
308 platform: Map::new(),
309 policy: crate::policy_config::RawPolicyConfig::default(),
310 };
311 if let Some(home) = composer_home {
312 let global = home.join("config.json");
313 if global.is_file() {
314 let text = std::fs::read_to_string(&global)
315 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
316 let v: Value = serde_json::from_str(&text)
317 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
318 cfg.merge(&v);
319 }
320 }
321 cfg.merge(manifest);
322 Ok(cfg)
323 }
324
325 fn merge(&mut self, config: &Value) {
326 if let Some(platform) = config.get("config").and_then(|c| c.get("platform")) {
329 self.platform = platform.as_object().cloned().unwrap_or_default();
330 }
331 if let Some(repos) = config.get("repositories") {
332 merge_repositories(&mut self.repositories, repos);
333 }
334 if let Some(cfg) = config.get("config").and_then(Value::as_object) {
335 self.policy.merge(cfg);
336 }
337 }
338}
339
340pub struct SolveReport {
343 pub transaction: LockTransaction,
344 pub decisions: Vec<i64>,
346 pub rules: usize,
348 pub learned: usize,
350 pub events: Vec<(vivacity_core::io::Verbosity, String)>,
356}
357
358#[derive(Debug, Clone, Default)]
361pub struct UpdateOptions {
362 pub allow_list: Vec<String>,
364 pub transitive: Option<crate::pool::UpdateMode>,
365 pub no_blocking: bool,
367 pub root_patch: Option<crate::root::RootPatch>,
370 pub merged: Option<crate::merge_plugin::Merged>,
374 pub temporary_requirements: Vec<(String, String)>,
378 pub ignore_lock: bool,
382}
383
384impl UpdateOptions {
385 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
387 let mut allow_list: Vec<String> = Vec::new();
388 for p in packages {
389 let l = p.to_lowercase();
390 if !allow_list.contains(&l) {
391 allow_list.push(l);
392 }
393 }
394 UpdateOptions {
395 allow_list,
396 transitive: Some(transitive),
397 ..Default::default()
398 }
399 }
400}
401
402fn temporary_constraints(
408 reqs: &[(String, String)],
409 root: &mut RootPackage,
410) -> Result<BTreeMap<String, crate::pool::TemporaryConstraint>, SessionError> {
411 if reqs.is_empty() {
412 return Ok(BTreeMap::new());
413 }
414 let pairs: Vec<(String, String)> = reqs.to_vec();
415 crate::root::extract_references(&pairs, &mut root.references);
416 let minimum = root.minimum_stability.clone();
417 crate::root::extract_stability_flags(&pairs, &minimum, &mut root.stability_flags);
418 let root_requires: Vec<(String, String, Constraint)> = root
422 .all_requires()
423 .iter()
424 .map(|l| {
425 (
426 l.target.clone(),
427 l.pretty_constraint.clone(),
428 l.constraint.clone(),
429 )
430 })
431 .collect();
432 let fail = |message: String, stdout: Option<String>| SessionError {
433 message,
434 kind: SessionErrorKind::Printed(1),
435 stdout,
436 parts: Vec::new(),
437 annotation: None,
438 preface: Box::default(),
439 };
440 let mut out: BTreeMap<String, crate::pool::TemporaryConstraint> = BTreeMap::new();
441 for (name, text) in &pairs {
442 let name = name.to_lowercase();
443 let parsed = crate::pool::TemporaryConstraint {
444 pretty: text.clone(),
445 constraint: crate::constraint::parse_constraints(text)
446 .map_err(|e| SessionError::new(format!("{name}: {e}")))?
447 .constraint,
448 };
449 if name.contains('*') {
450 let re = crate::pool::package_name_regexp(&name);
451 for (target, pretty, constraint) in &root_requires {
452 if !re.is_match(target.as_bytes()).unwrap_or(false) {
453 continue;
454 }
455 out.insert(target.clone(), parsed.clone());
456 if !crate::intervals::have_intersections(&parsed.constraint, constraint) {
457 return Err(fail(
458 format!(
459 "The temporary constraint \"{text}\" for \"{name}\" matching \"{target}\" must be a subset of the constraint in your composer.json ({pretty})"
460 ),
461 None,
462 ));
463 }
464 }
465 } else {
466 out.insert(name.clone(), parsed.clone());
467 if let Some((_, pretty, constraint)) = root_requires.iter().find(|(t, _, _)| *t == name)
468 {
469 if !crate::intervals::have_intersections(&parsed.constraint, constraint) {
470 return Err(fail(
471 format!(
472 "The temporary constraint \"{text}\" for \"{name}\" must be a subset of the constraint in your composer.json ({pretty})"
473 ),
474 Some(format!(
475 "Run `composer require {name}` or `composer require {name}:{text}` instead to replace the constraint"
476 )),
477 ));
478 }
479 }
480 }
481 }
482 Ok(out)
483}
484
485#[derive(Debug, Clone)]
488pub struct FlexSession {
489 pub symfony_require: String,
490 pub symfony: Constraint,
491 pub versions: crate::flex_filter::FlexVersions,
492}
493
494pub struct UpdateSession {
495 pub flex: Option<FlexSession>,
497 pub arena: Vec<Package>,
498 pub root: RootPackage,
499 pub fixed_root: usize,
501 pub fixed_root_alias: Option<usize>,
502 pub platform: Vec<usize>,
503 pub locked: Option<Vec<usize>>,
504 pub set: RepositorySet,
505 pub request: Request,
506 pub config: MergedConfig,
507 pub dev_mode: bool,
508 pub prefer_stable: bool,
510 pub prefer_lowest: bool,
511 pub php_version: String,
514 pub policy_config: crate::policy_config::PolicyConfig,
516 pub ini_files: Vec<String>,
519 pub loaded_extensions: BTreeSet<String>,
521 pub installer_dev_mode: bool,
524}
525
526impl UpdateSession {
527 pub fn prepare(
528 project_dir: &Path,
529 composer_home: Option<&Path>,
530 dev_mode: bool,
531 ) -> Result<UpdateSession, SessionError> {
532 Self::prepare_with(project_dir, composer_home, dev_mode, None)
533 }
534
535 pub fn prepare_with(
537 project_dir: &Path,
538 composer_home: Option<&Path>,
539 dev_mode: bool,
540 http: Option<HttpTransports>,
541 ) -> Result<UpdateSession, SessionError> {
542 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
543 }
544
545 pub fn prepare_full(
548 project_dir: &Path,
549 composer_home: Option<&Path>,
550 dev_mode: bool,
551 http: Option<HttpTransports>,
552 cache_repo_dir: Option<&Path>,
553 ) -> Result<UpdateSession, SessionError> {
554 Self::prepare_update(
555 project_dir,
556 composer_home,
557 dev_mode,
558 http,
559 cache_repo_dir,
560 &UpdateOptions::default(),
561 )
562 }
563
564 pub fn prepare_update(
567 project_dir: &Path,
568 composer_home: Option<&Path>,
569 dev_mode: bool,
570 http: Option<HttpTransports>,
571 cache_repo_dir: Option<&Path>,
572 options: &UpdateOptions,
573 ) -> Result<UpdateSession, SessionError> {
574 let partial_update = !options.allow_list.is_empty();
575 let manifest_path = project_dir.join("composer.json");
576 let manifest_text = std::fs::read_to_string(&manifest_path)
577 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
578 let manifest: Value = serde_json::from_str(&manifest_text)
579 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
580 let config = MergedConfig::load(&manifest, composer_home)?;
581 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
582 .map_err(SessionError::from)?;
583 policy_config
584 .apply_no_blocking(options.no_blocking)
585 .map_err(SessionError::from)?;
586 let mut root = RootPackage::load(&manifest, project_dir).map_err(SessionError::from)?;
587 if let Some(merged) = &options.merged {
588 let flags = std::mem::take(&mut root.stability_flags);
595 root = RootPackage::load(&merged.manifest, project_dir).map_err(SessionError::from)?;
596 root.stability_flags = flags;
597 for include in &merged.include_links {
600 crate::root::merge_plugin_stability_flags(
601 &mut root.stability_flags,
602 &root.minimum_stability,
603 &include.require,
604 );
605 }
606 for include in &merged.include_links {
607 crate::root::merge_plugin_stability_flags(
608 &mut root.stability_flags,
609 &root.minimum_stability,
610 &include.require_dev,
611 );
612 }
613 }
614 if let Some(patch) = &options.root_patch {
615 root.apply_patch(patch).map_err(SessionError::from)?;
616 }
617 let temporary_constraints =
621 temporary_constraints(&options.temporary_requirements, &mut root)?;
622 let probed = probe().map_err(SessionError::from)?;
623 let php_version = probed
627 .iter()
628 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
629 .and_then(|p| p.get("version").and_then(Value::as_str))
630 .map(|v| {
631 v.split(['.', '-', '+'])
632 .take(3)
633 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
634 .collect::<Vec<_>>()
635 .join(".")
636 })
637 .unwrap_or_default();
638 let platform_pkgs =
639 platform_packages(&probed, &config.platform).map_err(SessionError::from)?;
640
641 let mut arena: Vec<Package> = Vec::new();
642
643 let mut fixed = root.package.clone();
645 fixed.requires = Default::default();
646 fixed.dev_requires = Default::default();
647 arena.push(fixed);
648 let fixed_root = 0;
649 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
650 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
651 arena.push(a);
652 arena.len() - 1
653 });
654 let root_members: Vec<usize> = fixed_root_alias
655 .into_iter()
656 .chain(std::iter::once(fixed_root))
657 .collect();
658
659 let mut platform: Vec<usize> = Vec::new();
660 for p in platform_pkgs {
661 arena.push(p);
662 platform.push(arena.len() - 1);
663 }
664
665 let lock_path = project_dir.join("composer.lock");
669 let lock: Option<Value> = if lock_path.is_file() && !options.ignore_lock {
670 let text = std::fs::read_to_string(&lock_path)
671 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
672 match serde_json::from_str::<Value>(&text) {
673 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
674 Ok(_) => None,
675 Err(e) => {
676 if partial_update {
677 return Err(SessionError::new(format!(
678 "\"{}\" does not contain valid JSON\n{e}",
679 lock_path.display()
680 )));
681 }
682 None
683 }
684 }
685 } else {
686 None
687 };
688 let locked = match &lock {
689 Some(v) => Some(locked_repository(v, &mut arena).map_err(SessionError::from)?),
690 None => None,
691 };
692
693 let mut stability_flags = root.stability_flags.clone();
696 let root_version = match fixed_root_alias {
697 Some(a) => arena[a].version.clone(),
698 None => arena[fixed_root].version.clone(),
699 };
700 stability_flags.insert(
701 root.package.name.clone(),
702 stability_rank(parse_stability(&root_version)),
703 );
704
705 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
710 let requires = match &root.branch_alias {
711 Some((normalized, pretty)) => {
712 let aliased = root.package.alias(0, normalized, pretty);
713 let mut all = aliased.requires.clone();
714 for l in aliased.dev_requires.iter() {
715 all.insert(l.clone());
716 }
717 all
718 }
719 None => root.all_requires(),
720 };
721 for link in requires.iter() {
722 root_requires.insert(&link.target, link.constraint.clone());
723 }
724
725 let mut set = RepositorySet::new(
726 &root.minimum_stability,
727 stability_flags,
728 &root.aliases,
729 root.references.clone(),
730 root_requires,
731 temporary_constraints,
732 );
733 set.add_repository(Repository::Root(root_members));
734 set.add_repository(Repository::Platform(platform.clone()));
735 for repo in &config.repositories {
736 let origin = Origin::Repository(set.repositories.len());
737 set.add_repository(open_repository(
738 repo,
739 http.as_ref(),
740 cache_repo_dir,
741 project_dir,
742 origin,
743 &mut arena,
744 )?);
745 }
746 if let Some(ids) = &locked {
747 set.add_repository(Repository::Locked(ids.clone()));
748 }
749
750 if partial_update && locked.is_none() {
753 return Err(SessionError {
754 message: "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.".to_owned(),
755 kind: SessionErrorKind::Printed(3),
756 stdout: None,
757 parts: vec![(
759 vivacity_core::io::Verbosity::Quiet,
760 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.".to_owned(),
761 )],
762 annotation: None,
763 preface: Box::default(),
764 });
765 }
766 let mut request = Request::new(locked.clone());
767 if partial_update {
768 request.set_update_allow_list(
769 options.allow_list.clone(),
770 options
771 .transitive
772 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
773 );
774 }
775 if let Some(a) = fixed_root_alias {
776 request.fix_package(a);
777 }
778 request.fix_package(fixed_root);
779 for &p in &platform {
780 let provided = arena[fixed_root]
781 .provides
782 .get(&arena[p].name)
783 .map(|l| l.constraint.clone());
784 let provided_here = provided
785 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
786 if !provided_here {
787 request.fix_package(p);
788 }
789 }
790 for link in requires.iter() {
791 request.require_name_pretty(
792 &link.target,
793 Some(link.constraint.clone()),
794 &link.pretty_constraint,
795 )?;
796 }
797
798 Ok(UpdateSession {
799 flex: None,
800 arena,
801 root,
802 fixed_root,
803 fixed_root_alias,
804 platform,
805 locked,
806 set,
807 request,
808 config,
809 dev_mode,
810 prefer_stable: false,
811 prefer_lowest: false,
812 php_version,
813 policy_config,
814 ini_files: crate::platform::ini_files(&probed),
815 loaded_extensions: crate::platform::loaded_extensions(&probed),
816 installer_dev_mode: true,
817 })
818 }
819
820 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
821 let pre_pool = self.flex.as_ref().map(|f| {
822 let mut root_constraints = BTreeMap::new();
825 for link in self
826 .root
827 .package
828 .requires
829 .0
830 .iter()
831 .chain(self.root.package.dev_requires.0.iter())
832 {
833 root_constraints
834 .entry(link.target.clone())
835 .or_insert_with(|| link.constraint.clone());
836 }
837 crate::pool::PrePoolFilter {
838 symfony_require: f.symfony_require.clone(),
839 symfony: f.symfony.clone(),
840 root_constraints,
841 versions: f.versions.clone(),
842 }
843 });
844 Ok(self.set.create_pool(
845 &mut self.request,
846 &mut self.arena,
847 pre_pool.as_ref(),
848 crate::pool::INSTALLER_IGNORED_TYPES,
849 )?)
850 }
851
852 pub fn policy(&self) -> DefaultPolicy {
856 let mut policy = DefaultPolicy::new(
857 self.prefer_stable || self.root.prefer_stable,
858 self.prefer_lowest,
859 None,
860 );
861 if self.flex.is_some() {
862 policy.prefer_dev_over_prerelease = true;
863 }
864 policy
865 }
866
867 pub fn extract_dev_packages(
871 &mut self,
872 transaction: &mut LockTransaction,
873 policy: &mut DefaultPolicy,
874 filter: &PlatformRequirementFilter,
875 events: &mut Vec<(vivacity_core::io::Verbosity, String)>,
876 ) -> Result<(), SessionError> {
877 if self.root.package.dev_requires.is_empty() {
878 return Ok(());
879 }
880 let dumps: Vec<Value> = transaction
883 .new_lock_packages(&self.arena, false)
884 .iter()
885 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
886 .collect();
887 let result_ids =
888 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
889 .map_err(SessionError::from)?;
890 let mut members: Vec<usize> = Vec::new();
893 members.extend(self.fixed_root_alias);
894 members.push(self.fixed_root);
895 members.extend(self.platform.iter().copied());
896 members.extend(result_ids);
897 let mut pool_packages: Vec<usize> = Vec::new();
898 for idx in members {
899 pool_packages.push(idx);
900 let (name, version) = (
901 self.arena[idx].name.clone(),
902 self.arena[idx].version.clone(),
903 );
904 if let Some((alias, alias_normalized)) = self
905 .set
906 .root_aliases
907 .get(&name)
908 .and_then(|m| m.get(&version))
909 {
910 let mut base = idx;
911 while let Some(b) = self.arena[base].alias_of {
912 base = b;
913 }
914 let mut a = self.arena[base].alias(base, alias_normalized, alias);
915 a.root_package_alias = true;
916 a.origin = Origin::Detached;
917 self.arena.push(a);
918 pool_packages.push(self.arena.len() - 1);
919 }
920 }
921 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
922 let mut request = Request::new(None);
924 if let Some(a) = self.fixed_root_alias {
925 request.fix_package(a);
926 }
927 request.fix_package(self.fixed_root);
928 for &p in &self.platform {
929 let provided = self.arena[self.fixed_root]
930 .provides
931 .get(&self.arena[p].name)
932 .map(|l| l.constraint.clone());
933 let provided_here = provided.is_some_and(|c| {
934 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
935 });
936 if !provided_here {
937 request.fix_package(p);
938 }
939 }
940 let requires = match &self.root.branch_alias {
941 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
942 None => self.root.package.requires.clone(),
943 };
944 for link in requires.iter() {
945 request.require_name_pretty(
946 &link.target,
947 Some(link.constraint.clone()),
948 &link.pretty_constraint,
949 )?;
950 }
951 let mut solver = Solver::new(&pool, &self.arena);
952 let before = std::time::Instant::now();
953 let solved = solver.solve(&request, policy, filter);
954 events.push(resolution_completed(before));
957 let non_dev = match solved {
958 Ok(t) => t,
959 Err(SolveError::Problems(problems)) => {
960 drop(solver);
961 let pretty = {
964 let mut ctx = crate::problem::MessageContext {
965 arena: &mut self.arena,
966 set: &self.set,
967 pool: &pool,
968 request: &request,
969 is_verbose: vivacity_core::io::shows(vivacity_core::io::Verbosity::Verbose),
970 ini_files: &self.ini_files,
971 loaded_extensions: &self.loaded_extensions,
972 };
973 crate::problem::pretty_string(&mut ctx, &problems, true)
974 };
975 let head = "Unable to find a compatible set of packages based on your non-dev requirements alone.";
978 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}");
979 return Err(SessionError {
980 message: format!("{head}\n{rest}"),
981 kind: SessionErrorKind::Unsolvable,
982 stdout: None,
983 parts: vec![
984 (vivacity_core::io::Verbosity::Quiet, head.to_owned()),
985 (vivacity_core::io::Verbosity::Normal, rest),
986 ],
987 annotation: Some(format!("{head}\n{pretty}")),
989 preface: Box::default(),
990 });
991 }
992 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
993 };
994 transaction.set_non_dev_packages(&self.arena, &non_dev);
995 Ok(())
996 }
997
998 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
1000 let mut out = Map::new();
1001 for l in links.iter() {
1002 if crate::platform::is_platform_package(&l.target) {
1003 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
1004 }
1005 }
1006 out
1007 }
1008
1009 pub fn lock_json(
1011 &self,
1012 transaction: &LockTransaction,
1013 manifest_text: &str,
1014 ) -> Result<Value, SessionError> {
1015 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
1016 .map_err(|e| SessionError::new(e.to_string()))?;
1017 let packages = lock_packages(
1018 &self.arena,
1019 &transaction.new_lock_packages(&self.arena, false),
1020 )
1021 .map_err(SessionError::new)?;
1022 let packages_dev = lock_packages(
1023 &self.arena,
1024 &transaction.new_lock_packages(&self.arena, true),
1025 )
1026 .map_err(SessionError::new)?;
1027 let (requires, dev_requires) = match &self.root.branch_alias {
1028 Some((normalized, pretty)) => {
1029 let a = self.root.package.alias(0, normalized, pretty);
1030 (a.requires, a.dev_requires)
1031 }
1032 None => (
1033 self.root.package.requires.clone(),
1034 self.root.package.dev_requires.clone(),
1035 ),
1036 };
1037 Ok(lock_data(LockInput {
1038 content_hash: &content_hash,
1039 packages,
1040 packages_dev: Some(packages_dev),
1041 platform: Self::platform_requirements(&requires),
1042 platform_dev: Self::platform_requirements(&dev_requires),
1043 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
1044 minimum_stability: &self.root.minimum_stability,
1045 stability_flags: &self.root.stability_flags,
1046 prefer_stable: self.prefer_stable || self.root.prefer_stable,
1047 prefer_lowest: self.prefer_lowest,
1048 platform_overrides: &self.config.platform,
1049 }))
1050 }
1051
1052 pub fn update(
1055 &mut self,
1056 manifest_text: &str,
1057 filter: &PlatformRequirementFilter,
1058 ) -> Result<(Value, SolveReport), SessionError> {
1059 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
1060 let t = std::time::Instant::now();
1061 let lap = |label: &str, t: &std::time::Instant| {
1062 if trace {
1063 eprintln!(
1064 "trace: {label:<22} {:>7.1} ms",
1065 t.elapsed().as_secs_f64() * 1000.0
1066 );
1067 }
1068 };
1069 let mut policy = self.policy();
1070 let pool = self.create_filtered_pool()?;
1071 if let Some(notice) = &pool.flex_notice {
1075 vivacity_core::say!("{notice}");
1076 }
1077 vivacity_core::say!("Updating dependencies");
1078 for w in &pool.warnings {
1079 vivacity_core::say!("{w}");
1080 }
1081 lap("pool", &t);
1082 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
1083 pool
1084 } else {
1085 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
1086 };
1087 lap("optimize", &t);
1088 let before = std::time::Instant::now();
1089 let solved = self.solve(&pool, &mut policy, filter);
1090 let completed = resolution_completed(before);
1091 let mut report = match solved {
1092 Ok(mut r) => {
1093 r.events.push(completed);
1094 r
1095 }
1096 Err(SolveError::Problems(problems)) => {
1097 say_event(&completed);
1099 let pretty = self.pretty_problems(&pool, &problems, false);
1104 let head =
1105 "Your requirements could not be resolved to an installable set of packages.";
1106 let annotation = Some(format!("{head}\n{pretty}"));
1108 let mut parts = vec![
1109 (vivacity_core::io::Verbosity::Quiet, head.to_owned()),
1110 (vivacity_core::io::Verbosity::Normal, pretty),
1111 ];
1112 if !self.installer_dev_mode {
1113 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()));
1114 }
1115 let message = parts
1116 .iter()
1117 .map(|(_, p)| p.as_str())
1118 .collect::<Vec<_>>()
1119 .join("\n");
1120 return Err(SessionError {
1121 message,
1122 kind: SessionErrorKind::Unsolvable,
1123 stdout: None,
1124 parts,
1125 annotation,
1126 preface: Box::default(),
1127 });
1128 }
1129 Err(SolveError::Bug(b)) => {
1130 say_event(&completed);
1131 return Err(SessionError::new(b));
1132 }
1133 };
1134 lap("solve", &t);
1135 for &idx in &report.transaction.all {
1139 if let Err(e) = crate::lockfile::validate_package(&self.arena[idx]) {
1140 say_events(&report.events);
1141 return Err(SessionError::new(e));
1142 }
1143 }
1144 use vivacity_core::io::Verbosity;
1148 report.events.push((
1149 Verbosity::Verbose,
1150 format!("Analyzed {} packages to resolve dependencies", pool.len()),
1151 ));
1152 report.events.push((
1153 Verbosity::Verbose,
1154 format!("Analyzed {} rules to resolve dependencies", report.rules),
1155 ));
1156 if report.transaction.transaction.operations.is_empty() {
1157 report.events.push((
1158 Verbosity::Normal,
1159 "Nothing to modify in lock file".to_owned(),
1160 ));
1161 }
1162 drop(pool);
1163 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
1164 if let Err(e) =
1165 self.extract_dev_packages(&mut transaction, &mut policy, filter, &mut report.events)
1166 {
1167 say_events(&report.events);
1169 return Err(e);
1170 }
1171 lap("extract dev", &t);
1172 let lock = self.lock_json(&transaction, manifest_text)?;
1173 lap("lock data", &t);
1174 report.transaction = transaction;
1175 Ok((lock, report))
1176 }
1177
1178 pub fn pretty_problems(
1181 &mut self,
1182 pool: &Pool,
1183 problems: &[crate::solver::SolvedProblem],
1184 is_dev_extraction: bool,
1185 ) -> String {
1186 let mut ctx = crate::problem::MessageContext {
1187 arena: &mut self.arena,
1188 set: &self.set,
1189 pool,
1190 request: &self.request,
1191 is_verbose: vivacity_core::io::shows(vivacity_core::io::Verbosity::Verbose),
1192 ini_files: &self.ini_files,
1193 loaded_extensions: &self.loaded_extensions,
1194 };
1195 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
1196 }
1197
1198 pub fn find_packages_for_require(
1205 &mut self,
1206 name: &str,
1207 ignore_stability: bool,
1208 ) -> Result<Vec<usize>, SessionError> {
1209 let name = name.to_lowercase();
1210 let mut acceptable = BTreeMap::new();
1211 let min = crate::version::stability_rank(&self.root.minimum_stability);
1212 for st in ["stable", "RC", "beta", "alpha", "dev"] {
1213 let rank = crate::version::stability_rank(st);
1214 if ignore_stability || rank <= min {
1215 acceptable.insert(st.to_owned(), rank);
1216 }
1217 }
1218 let flags = BTreeMap::new();
1219 let already = BTreeMap::new();
1220 let map = vec![(name.clone(), Constraint::MatchAll)];
1221 let mut found: Vec<usize> = Vec::new();
1222 let (_, ids) = crate::pool::array_repository_load_packages(
1223 &self.platform,
1224 &map,
1225 &acceptable,
1226 &flags,
1227 &already,
1228 &self.arena,
1229 );
1230 found.extend(ids);
1231 for (i, repo) in self.set.repositories.iter().enumerate() {
1235 match repo {
1236 Repository::Composer(repo) => {
1237 let (_, ids) = repo
1238 .load_packages(
1239 &map,
1240 &acceptable,
1241 &flags,
1242 &already,
1243 Origin::Repository(i),
1244 &mut self.arena,
1245 )
1246 .map_err(SessionError::from)?;
1247 found.extend(ids);
1248 }
1249 Repository::Path(repo) => {
1250 let (_, ids) = crate::pool::array_repository_load_packages(
1251 &repo.members,
1252 &map,
1253 &acceptable,
1254 &flags,
1255 &already,
1256 &self.arena,
1257 );
1258 found.extend(ids);
1259 }
1260 Repository::Root(_) | Repository::Platform(_) | Repository::Locked(_) => {}
1261 }
1262 }
1263 Ok(found)
1264 }
1265
1266 pub fn create_optimized_pool(
1267 &mut self,
1268 policy: &mut DefaultPolicy,
1269 ) -> Result<Pool, SessionError> {
1270 let pool = self.create_filtered_pool()?;
1271 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
1272 return Ok(pool);
1273 }
1274 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
1275 }
1276
1277 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
1280 let mut pool = self.create_pool()?;
1281 let before = pool.len();
1282 let flex_notice = pool.flex_notice.take();
1285 let mut warnings = Vec::new();
1286 pool = crate::pool_filters::security_advisory_filter(
1287 pool,
1288 &self.arena,
1289 &self.set.repositories,
1290 &self.request,
1291 &self.policy_config,
1292 &mut warnings,
1293 )
1294 .map_err(SessionError::from)?;
1295 pool = crate::pool_filters::filter_list_filter(
1296 pool,
1297 &self.arena,
1298 &self.set.repositories,
1299 &self.request,
1300 &self.policy_config,
1301 "update",
1302 &mut warnings,
1303 )
1304 .map_err(SessionError::from)?;
1305 pool.warnings.extend(warnings);
1306 pool.flex_notice = flex_notice;
1307 if std::env::var_os("VIVACITY_TRACE").is_some() {
1308 eprintln!(
1309 "trace: policy filters {before} → {} package versions ({} removed by lists)",
1310 pool.len(),
1311 pool.filter_list_removed
1312 .values()
1313 .map(Vec::len)
1314 .sum::<usize>()
1315 );
1316 }
1317 Ok(pool)
1318 }
1319
1320 pub fn solve(
1323 &self,
1324 pool: &Pool,
1325 policy: &mut DefaultPolicy,
1326 filter: &PlatformRequirementFilter,
1327 ) -> Result<SolveReport, SolveError> {
1328 let mut solver = Solver::new(pool, &self.arena);
1329 let transaction = solver.solve(&self.request, policy, filter)?;
1330 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
1331 Ok(SolveReport {
1332 learned: solver
1333 .rules
1334 .ids_of_type(crate::rule::RuleType::Learned)
1335 .len(),
1336 rules: solver.rule_set_size(),
1337 decisions,
1338 transaction,
1339 events: Vec::new(),
1340 })
1341 }
1342}
1343
1344fn resolution_completed(before: std::time::Instant) -> (vivacity_core::io::Verbosity, String) {
1347 (
1348 vivacity_core::io::Verbosity::Verbose,
1349 format!(
1350 "Dependency resolution completed in {:.3} seconds",
1351 before.elapsed().as_secs_f64()
1352 ),
1353 )
1354}
1355
1356fn say_event((level, line): &(vivacity_core::io::Verbosity, String)) {
1358 vivacity_core::say_at!(*level, "{line}");
1359}
1360
1361fn say_events(events: &[(vivacity_core::io::Verbosity, String)]) {
1364 events.iter().for_each(say_event);
1365}
1366
1367pub struct InstallVerifyInput {
1370 pub project_dir: std::path::PathBuf,
1371 pub composer_home: Option<std::path::PathBuf>,
1372 pub http: Option<HttpTransports>,
1373 pub cache_repo_dir: Option<std::path::PathBuf>,
1374 pub manifest_text: String,
1377 pub root_manifest: Value,
1380 pub dev_mode: bool,
1382 pub no_blocking: bool,
1384 pub ignore_platform_reqs: bool,
1386 pub ignore_platform_req: Vec<String>,
1388 pub prepended_repositories: Vec<Value>,
1392 pub abandoned: std::sync::Arc<std::sync::atomic::AtomicBool>,
1397}
1398
1399#[derive(Debug, PartialEq, Eq)]
1401pub enum InstallVerdict {
1402 Installable,
1404 Problems(String),
1406 NeedsChanges,
1409 Invalid(String),
1412}
1413
1414#[derive(Debug)]
1415pub struct InstallVerification {
1416 pub warnings: Vec<String>,
1418 pub php_missing: bool,
1422 pub verdict: InstallVerdict,
1423}
1424
1425struct LockSettings {
1429 minimum_stability: String,
1430 stability_flags: BTreeMap<String, i32>,
1431 prefer_stable: Option<bool>,
1435 prefer_lowest: Option<bool>,
1436}
1437
1438impl LockSettings {
1439 fn of(lock: &Value) -> LockSettings {
1440 let truthy = |key: &str| match lock.get(key)? {
1442 Value::Null => None,
1443 Value::Bool(b) => Some(*b),
1444 Value::Number(n) => Some(n.as_f64() != Some(0.0)),
1445 Value::String(s) => Some(!(s.is_empty() || s == "0")),
1446 Value::Array(a) => Some(!a.is_empty()),
1447 Value::Object(o) => Some(!o.is_empty()),
1448 };
1449 LockSettings {
1450 minimum_stability: lock
1451 .get("minimum-stability")
1452 .and_then(Value::as_str)
1453 .unwrap_or("stable")
1454 .to_owned(),
1455 stability_flags: lock
1456 .get("stability-flags")
1457 .and_then(Value::as_object)
1458 .map(|flags| {
1459 flags
1460 .iter()
1461 .filter_map(|(n, v)| Some((n.clone(), i32::try_from(v.as_i64()?).ok()?)))
1462 .collect()
1463 })
1464 .unwrap_or_default(),
1465 prefer_stable: truthy("prefer-stable"),
1466 prefer_lowest: truthy("prefer-lowest"),
1467 }
1468 }
1469}
1470
1471fn root_requires(root: &Package, dev_mode: bool) -> crate::package::Links {
1475 let mut links = root.requires.clone();
1476 if dev_mode {
1477 for link in root.dev_requires.iter() {
1478 links.insert(link.clone());
1479 }
1480 }
1481 links
1482}
1483
1484fn install_request(
1490 arena: &[Package],
1491 (fixed_root, fixed_root_alias): (usize, Option<usize>),
1492 platform: &[usize],
1493 locked: &[usize],
1494 root_links: &crate::package::Links,
1495 lock: &Value,
1496 dev_mode: bool,
1497) -> Result<Request, SessionError> {
1498 use crate::package::LinkType;
1499 let mut request = Request::new(Some(locked.to_vec()));
1500 if let Some(a) = fixed_root_alias {
1501 request.fix_package(a);
1502 }
1503 request.fix_package(fixed_root);
1504 for &p in platform {
1505 let provided_here = arena[fixed_root]
1506 .provides
1507 .get(&arena[p].name)
1508 .is_some_and(|l| {
1509 l.constraint
1510 .matches(&Constraint::new(Op::Eq, arena[p].version.clone()))
1511 });
1512 if !provided_here {
1513 request.fix_package(p);
1514 }
1515 }
1516 for &idx in locked {
1517 request.fix_locked_package(idx);
1518 }
1519 for link in root_links.iter() {
1520 if crate::platform::is_platform_package(&link.target) {
1521 request.require_name_pretty(
1522 &link.target,
1523 Some(link.constraint.clone()),
1524 &link.pretty_constraint,
1525 )?;
1526 }
1527 }
1528 let parse = |key: &str| {
1531 crate::loader::parse_links("__root__", "1.0.0", LinkType::Require, lock.get(key), false)
1532 .map_err(|e| SessionError::new(format!("composer.lock: {}", e.0)))
1533 };
1534 let mut lock_links = parse("platform")?;
1535 if dev_mode {
1536 for link in parse("platform-dev")?.iter() {
1537 lock_links.insert(link.clone());
1538 }
1539 }
1540 for link in lock_links.iter() {
1541 if root_links.get(&link.target).is_none() {
1542 request.require_name_pretty(
1543 &link.target,
1544 Some(link.constraint.clone()),
1545 &link.pretty_constraint,
1546 )?;
1547 }
1548 }
1549 Ok(request)
1550}
1551
1552fn install_repository_set(
1557 arena: &[Package],
1558 root: &RootPackage,
1559 root_members: Vec<usize>,
1560 platform: &[usize],
1561 locked: &[usize],
1562 settings: &LockSettings,
1563 filter: &PlatformRequirementFilter,
1564) -> RepositorySet {
1565 let mut requires: OrderedMap<Constraint> = OrderedMap::default();
1566 for &idx in locked {
1567 let p = &arena[idx];
1568 if filter.is_ignored(&p.name) {
1569 continue;
1570 }
1571 let constraint = Constraint::new(Op::Eq, p.version.clone());
1572 let constraint = filter.filter_constraint(&p.name, &constraint, true);
1573 requires.insert(&p.name, constraint);
1574 }
1575 let mut stability_flags = settings.stability_flags.clone();
1578 let root_version = match &root.branch_alias {
1579 Some((normalized, _)) => normalized.clone(),
1580 None => root.package.version.clone(),
1581 };
1582 stability_flags.insert(
1583 root.package.name.clone(),
1584 stability_rank(parse_stability(&root_version)),
1585 );
1586 let mut set = RepositorySet::new(
1587 &settings.minimum_stability,
1588 stability_flags,
1589 &[],
1590 root.references.clone(),
1591 requires,
1592 BTreeMap::new(),
1593 );
1594 set.add_repository(Repository::Root(root_members));
1595 set.add_repository(Repository::Platform(platform.to_vec()));
1596 set.add_repository(Repository::Locked(locked.to_vec()));
1597 set
1598}
1599
1600fn locked_platform(probed: &[Value], lock: &Value) -> Result<Vec<Package>, SessionError> {
1604 let empty = Map::new();
1605 let overrides = lock
1606 .get("platform-overrides")
1607 .and_then(Value::as_object)
1608 .unwrap_or(&empty);
1609 platform_packages(probed, overrides).map_err(SessionError::from)
1610}
1611
1612fn push_fixed_root(arena: &mut Vec<Package>, root: &RootPackage) -> (usize, Option<usize>) {
1615 let mut fixed = root.package.clone();
1616 fixed.requires = Default::default();
1617 fixed.dev_requires = Default::default();
1618 arena.push(fixed);
1619 let fixed_root = arena.len() - 1;
1620 let alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
1621 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
1622 arena.push(a);
1623 arena.len() - 1
1624 });
1625 (fixed_root, alias)
1626}
1627
1628struct LockedPool<'a> {
1631 lock: &'a Value,
1632 settings: LockSettings,
1633 locked: Vec<usize>,
1634 platform: Vec<usize>,
1635 dev_mode: bool,
1636 filter: PlatformRequirementFilter,
1638}
1639
1640type Filtered = (BTreeSet<usize>, crate::pool::FilterListRemoved);
1643
1644impl LockedPool<'_> {
1645 fn build(
1648 &self,
1649 arena: &mut Vec<Package>,
1650 root: &RootPackage,
1651 ) -> Result<(RepositorySet, Request, Pool), SessionError> {
1652 let (fixed_root, alias) = push_fixed_root(arena, root);
1653 let members: Vec<usize> = alias.into_iter().chain([fixed_root]).collect();
1654 let set = install_repository_set(
1655 arena,
1656 root,
1657 members,
1658 &self.platform,
1659 &self.locked,
1660 &self.settings,
1661 &self.filter,
1662 );
1663 let mut request = install_request(
1664 arena,
1665 (fixed_root, alias),
1666 &self.platform,
1667 &self.locked,
1668 &root_requires(&root.package, self.dev_mode),
1669 self.lock,
1670 self.dev_mode,
1671 )?;
1672 let pool = set.create_pool(
1673 &mut request,
1674 arena,
1675 None,
1676 crate::pool::INSTALLER_IGNORED_TYPES,
1677 )?;
1678 Ok((set, request, pool))
1679 }
1680
1681 fn solve(
1684 &self,
1685 arena: &mut Vec<Package>,
1686 root: &RootPackage,
1687 filtered: Option<Filtered>,
1688 probed: Option<&[Value]>,
1689 ) -> Result<InstallVerdict, SessionError> {
1690 let (set, request, mut pool) = self.build(arena, root)?;
1691 if let Some((removed, records)) = filtered {
1692 let removed_here: Vec<usize> = pool
1697 .packages
1698 .iter()
1699 .copied()
1700 .filter(|idx| removed.contains(idx))
1701 .collect();
1702 if !removed_here.is_empty() {
1703 let mut kept_records = crate::pool::FilterListRemoved::new();
1704 for &idx in &removed_here {
1705 let p = &arena[idx];
1706 for name in p.names(false) {
1707 let Some(record) = records
1708 .get(&name)
1709 .and_then(|versions| versions.iter().find(|(v, _)| *v == p.version))
1710 else {
1711 continue;
1712 };
1713 let versions = kept_records.entry(name).or_default();
1714 if !versions.iter().any(|(v, _)| *v == p.version) {
1715 versions.push(record.clone());
1716 }
1717 }
1718 }
1719 let kept: Vec<usize> = pool
1720 .packages
1721 .iter()
1722 .copied()
1723 .filter(|idx| !removed.contains(idx))
1724 .collect();
1725 pool = pool.with_packages(kept, arena);
1726 pool.filter_list_removed = kept_records;
1727 }
1728 }
1729 let mut policy = DefaultPolicy::new(
1732 self.settings.prefer_stable.unwrap_or(root.prefer_stable),
1733 self.settings.prefer_lowest.unwrap_or(false),
1734 None,
1735 );
1736 let solved = Solver::new(&pool, arena).solve(&request, &mut policy, &self.filter);
1737 match solved {
1738 Ok(transaction) => {
1739 for &idx in &transaction.all {
1742 if let Err(e) = crate::lockfile::validate_package(&arena[idx]) {
1743 return Ok(InstallVerdict::Invalid(e));
1744 }
1745 }
1746 Ok(if transaction.transaction.operations.is_empty() {
1747 InstallVerdict::Installable
1748 } else {
1749 InstallVerdict::NeedsChanges
1750 })
1751 }
1752 Err(SolveError::Problems(problems)) => {
1753 let ini_files = probed.map(crate::platform::ini_files).unwrap_or_default();
1754 let loaded_extensions = probed
1755 .map(crate::platform::loaded_extensions)
1756 .unwrap_or_default();
1757 let mut ctx = crate::problem::MessageContext {
1758 arena,
1759 set: &set,
1760 pool: &pool,
1761 request: &request,
1762 is_verbose: vivacity_core::io::shows(vivacity_core::io::Verbosity::Verbose),
1763 ini_files: &ini_files,
1764 loaded_extensions: &loaded_extensions,
1765 };
1766 Ok(InstallVerdict::Problems(crate::problem::pretty_string(
1767 &mut ctx, &problems, false,
1768 )))
1769 }
1770 Err(SolveError::Bug(b)) => Err(SessionError::new(b)),
1771 }
1772 }
1773}
1774
1775pub fn verify_install(
1789 input: InstallVerifyInput,
1790 root_version: std::sync::mpsc::Receiver<vivacity_core::root_version::RootVersion>,
1791) -> Result<InstallVerification, SessionError> {
1792 let manifest: Value = serde_json::from_str(&input.manifest_text)
1793 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
1794 let config = MergedConfig::load(&manifest, input.composer_home.as_deref())?;
1795 let mut policy =
1796 crate::policy_config::PolicyConfig::from_raw(&config.policy).map_err(SessionError::from)?;
1797 policy
1798 .apply_no_blocking(input.no_blocking)
1799 .map_err(SessionError::from)?;
1800 let lock_text = std::fs::read_to_string(input.project_dir.join("composer.lock"))
1801 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1802 let lock: Value = serde_json::from_str(&lock_text)
1803 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
1804
1805 let mut arena: Vec<Package> = Vec::new();
1806 let locked = crate::repository::locked_repository_with(&lock, &mut arena, input.dev_mode)
1807 .map_err(SessionError::from)?;
1808
1809 let mut warnings = Vec::new();
1810 let filtered: Option<Filtered> = if policy.malware_blocks("install") {
1811 let mut repositories: Vec<Repository> = Vec::new();
1814 let prepended: Vec<RepoConfig> = input
1815 .prepended_repositories
1816 .iter()
1817 .enumerate()
1818 .map(|(i, definition)| RepoConfig {
1819 key: RepoKey::Indexed(i as u64),
1820 definition: definition.clone(),
1821 })
1822 .collect();
1823 for repo in prepended.iter().chain(&config.repositories) {
1824 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
1825 continue;
1826 }
1827 let mut opened = open_repository_with(
1830 repo,
1831 input.http.as_ref(),
1832 input.cache_repo_dir.as_deref(),
1833 true,
1834 )?;
1835 if let Repository::Composer(r) = &mut opened {
1836 r.hold_output();
1837 }
1838 repositories.push(opened);
1839 }
1840 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
1841 let before = pool.packages.clone();
1842 let request = Request::new(Some(locked.clone()));
1844 let mut filter_warnings = Vec::new();
1845 let pool = match crate::pool_filters::filter_list_filter(
1846 pool,
1847 &arena,
1848 &repositories,
1849 &request,
1850 &policy,
1851 "install",
1852 &mut filter_warnings,
1853 ) {
1854 Ok(pool) => pool,
1855 Err(e) => {
1859 let mut e = SessionError::from(e);
1860 let mut preface = Vec::new();
1861 for repo in &repositories {
1862 if let Repository::Composer(r) = repo {
1863 preface.extend(r.take_held());
1864 }
1865 }
1866 e.preface = preface.into_boxed_slice();
1867 return Err(e);
1868 }
1869 };
1870 for repo in &repositories {
1872 if let Repository::Composer(r) = repo {
1873 warnings.extend(r.take_held());
1874 }
1875 }
1876 warnings.extend(filter_warnings);
1877 let removed: BTreeSet<usize> = before
1878 .into_iter()
1879 .filter(|idx| pool.id_of(*idx).is_none())
1880 .collect();
1881 Some((removed, pool.filter_list_removed))
1882 } else {
1883 None
1884 };
1885 let abandoned = || {
1886 if input.abandoned.load(std::sync::atomic::Ordering::Relaxed) {
1887 Err(SessionError::new("the lock verification was abandoned"))
1888 } else {
1889 Ok(())
1890 }
1891 };
1892 abandoned()?;
1893
1894 let probed = probe().ok();
1898 let packages = match &probed {
1899 Some(probed) => locked_platform(probed, &lock)?,
1900 None => locked_platform(&[], &lock)?,
1901 };
1902 let mut platform: Vec<usize> = Vec::new();
1903 for p in packages {
1904 arena.push(p);
1905 platform.push(arena.len() - 1);
1906 }
1907 let filter = if input.ignore_platform_reqs {
1908 PlatformRequirementFilter::IgnoreAll
1909 } else if probed.is_none() {
1910 let mut list = input.ignore_platform_req.clone();
1911 list.extend(["php", "php-*", "hhvm", "ext-*", "lib-*"].map(str::to_owned));
1912 PlatformRequirementFilter::from_list(&list)
1913 } else if !input.ignore_platform_req.is_empty() {
1914 PlatformRequirementFilter::from_list(&input.ignore_platform_req)
1915 } else {
1916 PlatformRequirementFilter::IgnoreNothing
1917 };
1918 let locked_pool = LockedPool {
1919 lock: &lock,
1920 settings: LockSettings::of(&lock),
1921 locked,
1922 platform,
1923 dev_mode: input.dev_mode,
1924 filter,
1925 };
1926 let rv = root_version
1927 .recv()
1928 .map_err(|_| SessionError::new("the root version was not detected"))?;
1929 let root = RootPackage::load_with_version(&input.root_manifest, rv)
1930 .map_err(|e| SessionError::new(e.0))?;
1931 abandoned()?;
1932 let verdict = locked_pool.solve(&mut arena, &root, filtered, probed.as_deref())?;
1933 Ok(InstallVerification {
1934 warnings,
1935 php_missing: probed.is_none(),
1936 verdict,
1937 })
1938}
1939
1940fn open_repository(
1944 repo: &RepoConfig,
1945 http: Option<&HttpTransports>,
1946 cache_repo_dir: Option<&Path>,
1947 project_dir: &Path,
1948 origin: Origin,
1949 arena: &mut Vec<Package>,
1950) -> Result<Repository, SessionError> {
1951 if repo.definition.get("type").and_then(Value::as_str) == Some("path") {
1952 if repo.definition.get("only").is_some()
1953 || repo.definition.get("exclude").is_some()
1954 || repo.definition.get("canonical").is_some()
1955 {
1956 return Err(SessionError::new(format!(
1957 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1958 key_string(&repo.key)
1959 )));
1960 }
1961 let path_repo = crate::path_repo::open(&repo.definition, project_dir, origin, arena)
1962 .map_err(SessionError::from)?;
1963 return Ok(Repository::Path(path_repo));
1964 }
1965 open_repository_with(repo, http, cache_repo_dir, false)
1966}
1967
1968fn open_repository_with(
1972 repo: &RepoConfig,
1973 http: Option<&HttpTransports>,
1974 cache_repo_dir: Option<&Path>,
1975 for_policies: bool,
1976) -> Result<Repository, SessionError> {
1977 let def = &repo.definition;
1978 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1979 SessionError::new(format!(
1980 "Repository \"{}\" ({def}) must have a type defined",
1981 key_string(&repo.key)
1982 ))
1983 })?;
1984 if kind != "composer" {
1985 return Err(SessionError::new(format!(
1986 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1987 key_string(&repo.key)
1988 )));
1989 }
1990 if !for_policies
1991 && (def.get("only").is_some()
1992 || def.get("exclude").is_some()
1993 || def.get("canonical").is_some())
1994 {
1995 return Err(SessionError::new(format!(
1996 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1997 key_string(&repo.key)
1998 )));
1999 }
2000 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
2001 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
2002 })?;
2003 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
2004 Box::new(FileTransport)
2005 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
2006 match http {
2007 Some(h) => Box::new(HttpTransport {
2008 fetch: h.0.clone(),
2009 fetch_many: h.1.clone(),
2010 post: h.2.clone(),
2011 }),
2012 None => {
2013 return Err(SessionError::new(format!(
2014 "remote composer repositories need a network transport ({url})"
2015 )))
2016 }
2017 }
2018 } else {
2019 return Err(SessionError::new(format!(
2020 "unsupported repository url scheme ({url})"
2021 )));
2022 };
2023 let mut repo = ComposerRepository::open(url, transport).map_err(SessionError::from)?;
2024 if let Some(options) = def.get("options") {
2025 repo.options = options.clone();
2026 }
2027 repo.set_user_filter(def.get("filter"))
2028 .map_err(SessionError::from)?;
2029 if for_policies {
2030 repo.set_name_filter(def.get("only"), def.get("exclude"))
2031 .map_err(SessionError::from)?;
2032 }
2033 if let Some(dir) = cache_repo_dir {
2034 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
2035 }
2036 Ok(Repository::Composer(Box::new(repo)))
2037}
2038
2039fn key_string(key: &RepoKey) -> String {
2040 match key {
2041 RepoKey::Named(n) => n.clone(),
2042 RepoKey::Indexed(i) => i.to_string(),
2043 }
2044}
2045
2046#[cfg(test)]
2047mod tests {
2048 use super::*;
2049 use serde_json::json;
2050
2051 fn names(repos: &[RepoConfig]) -> Vec<String> {
2052 repos.iter().map(|r| key_string(&r.key)).collect()
2053 }
2054
2055 #[test]
2056 fn merges_repositories_like_composer_config() {
2057 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
2058 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
2059 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
2061 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
2062 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
2064 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
2065 cfg.merge(
2068 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
2069 );
2070 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
2071 assert_eq!(cfg.repositories[0].definition["url"], "a");
2072 assert_eq!(cfg.repositories[1].definition["url"], "b");
2073 assert_eq!(
2074 cfg.repositories[2].definition["url"],
2075 "https://packages.drupal.org/8"
2076 );
2077 }
2078
2079 #[test]
2080 fn packagist_url_disables_default() {
2081 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
2082 cfg.merge(
2083 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
2084 );
2085 assert_eq!(names(&cfg.repositories), vec!["0"]);
2086 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
2087 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
2088 assert!(cfg.repositories.is_empty());
2089 }
2090
2091 fn probed() -> Vec<Value> {
2095 vec![
2096 json!({"kind": "php", "name": "php", "version": "8.2.5", "description": "The PHP interpreter"}),
2097 json!({"kind": "ext", "name": "mbstring", "version": "8.2.5"}),
2098 json!({"kind": "ext", "name": "intl", "version": "8.2.5"}),
2099 ]
2100 }
2101
2102 fn root_version() -> vivacity_core::root_version::RootVersion {
2103 vivacity_core::root_version::RootVersion {
2104 pretty_version: "1.0.0+no-version-set".to_owned(),
2105 version: "1.0.0.0".to_owned(),
2106 reference: None,
2107 }
2108 }
2109
2110 fn verify(
2112 manifest: Value,
2113 lock: Value,
2114 dev_mode: bool,
2115 filter: PlatformRequirementFilter,
2116 ) -> (InstallVerdict, Request) {
2117 verify_flagged(manifest, lock, dev_mode, filter, &[])
2118 }
2119
2120 fn verify_flagged(
2123 manifest: Value,
2124 lock: Value,
2125 dev_mode: bool,
2126 filter: PlatformRequirementFilter,
2127 flagged: &[&str],
2128 ) -> (InstallVerdict, Request) {
2129 let mut arena = Vec::new();
2130 let locked = crate::repository::locked_repository_with(&lock, &mut arena, dev_mode)
2131 .expect("locked repository");
2132 let mut platform = Vec::new();
2133 for p in locked_platform(&probed(), &lock).expect("platform") {
2134 arena.push(p);
2135 platform.push(arena.len() - 1);
2136 }
2137 let pool = LockedPool {
2138 lock: &lock,
2139 settings: LockSettings::of(&lock),
2140 locked,
2141 platform,
2142 dev_mode,
2143 filter,
2144 };
2145 let root = RootPackage::load_with_version(&manifest, root_version()).expect("root");
2146 let (_, request, _) = pool.build(&mut arena.clone(), &root).expect("build");
2147 let filtered = (!flagged.is_empty()).then(|| {
2148 let mut removed = BTreeSet::new();
2149 let mut records = crate::pool::FilterListRemoved::new();
2150 for &idx in &pool.locked {
2151 let p = &arena[idx];
2152 if !flagged.contains(&p.name.as_str()) {
2153 continue;
2154 }
2155 removed.insert(idx);
2156 let entry = crate::repository::FilterEntry {
2157 package_name: p.name.clone(),
2158 constraint: Constraint::MatchAll,
2159 list_name: "malware".to_owned(),
2160 url: None,
2161 reason: None,
2162 id: None,
2163 source: Some("testsource".to_owned()),
2164 };
2165 records
2166 .entry(p.name.clone())
2167 .or_default()
2168 .push((p.version.clone(), vec![entry]));
2169 }
2170 (removed, records)
2171 });
2172 let verdict = pool
2173 .solve(&mut arena, &root, filtered, Some(&probed()))
2174 .expect("solve");
2175 (verdict, request)
2176 }
2177
2178 fn requires(request: &Request) -> Vec<(String, String)> {
2179 request
2180 .pretty_requires
2181 .iter()
2182 .map(|(n, p)| (n.clone(), p.clone()))
2183 .collect()
2184 }
2185
2186 fn lock_with(packages: Value, dev: Value, platform: Value, platform_dev: Value) -> Value {
2187 json!({
2188 "packages": packages, "packages-dev": dev, "aliases": [],
2189 "minimum-stability": "stable", "stability-flags": {},
2190 "prefer-stable": false, "prefer-lowest": false,
2191 "platform": platform, "platform-dev": platform_dev,
2192 })
2193 }
2194
2195 fn pair(n: &str, p: &str) -> (String, String) {
2196 (n.to_owned(), p.to_owned())
2197 }
2198
2199 #[test]
2200 fn requires_the_roots_platform_requirements_then_the_locks_it_does_not_declare() {
2201 let manifest = json!({
2202 "name": "acme/root",
2203 "require": {"php": "^8.0", "ext-intl": "*", "acme/lib": "^1.0"},
2204 "require-dev": {"ext-intl": ">=1", "ext-mbstring": "*"},
2205 });
2206 let lock = lock_with(
2207 json!([]),
2208 json!([]),
2209 json!({"php": ">=8.3", "ext-mbstring": "^8", "ext-json": "*"}),
2210 json!({"ext-intl": "^9", "ext-zip": "*"}),
2211 );
2212 let (_, request) = verify(
2216 manifest.clone(),
2217 lock.clone(),
2218 true,
2219 PlatformRequirementFilter::IgnoreNothing,
2220 );
2221 assert_eq!(
2222 requires(&request),
2223 vec![
2224 pair("php", "^8.0"),
2225 pair("ext-intl", ">=1"),
2226 pair("ext-mbstring", "*"),
2227 pair("ext-json", "*"),
2228 pair("ext-zip", "*"),
2229 ]
2230 );
2231 let (_, request) = verify(
2234 manifest,
2235 lock,
2236 false,
2237 PlatformRequirementFilter::IgnoreNothing,
2238 );
2239 assert_eq!(
2240 requires(&request),
2241 vec![
2242 pair("php", "^8.0"),
2243 pair("ext-intl", "*"),
2244 pair("ext-mbstring", "^8"),
2245 pair("ext-json", "*"),
2246 ]
2247 );
2248 }
2249
2250 #[test]
2251 fn the_roots_constraint_wins_over_the_locks() {
2252 let lock = lock_with(json!([]), json!([]), json!({"php": "^99"}), json!({}));
2253 let (verdict, _) = verify(
2254 json!({"require": {"php": "^8.0"}}),
2255 lock.clone(),
2256 true,
2257 PlatformRequirementFilter::IgnoreNothing,
2258 );
2259 assert_eq!(verdict, InstallVerdict::Installable);
2260 let (verdict, _) = verify(
2261 json!({}),
2262 lock,
2263 true,
2264 PlatformRequirementFilter::IgnoreNothing,
2265 );
2266 let InstallVerdict::Problems(text) = verdict else {
2267 panic!("{verdict:?}")
2268 };
2269 assert!(
2270 text.contains("Root composer.json requires php ^99 but your php version (8.2.5)"),
2271 "{text}"
2272 );
2273 }
2274
2275 #[test]
2276 fn locked_packages_platform_requirements_and_the_ignore_filter() {
2277 let lock = lock_with(
2278 json!([{"name": "acme/a", "version": "1.0.0", "type": "library", "require": {"ext-gd": "*"}}]),
2279 json!([]),
2280 json!({}),
2281 json!({}),
2282 );
2283 let (verdict, _) = verify(
2284 json!({"require": {"acme/a": "^1.0"}}),
2285 lock.clone(),
2286 true,
2287 PlatformRequirementFilter::IgnoreNothing,
2288 );
2289 let InstallVerdict::Problems(text) = verdict else {
2290 panic!("{verdict:?}")
2291 };
2292 assert!(
2293 text.contains("acme/a 1.0.0 requires ext-gd * -> it is missing from your system."),
2294 "{text}"
2295 );
2296 for ignored in [
2297 PlatformRequirementFilter::IgnoreAll,
2298 PlatformRequirementFilter::from_list(&["ext-gd".to_owned()]),
2299 PlatformRequirementFilter::from_list(&["ext-*".to_owned()]),
2300 ] {
2301 let (verdict, _) = verify(
2302 json!({"require": {"acme/a": "^1.0"}}),
2303 lock.clone(),
2304 true,
2305 ignored,
2306 );
2307 assert_eq!(verdict, InstallVerdict::Installable);
2308 }
2309 }
2310
2311 #[test]
2312 fn platform_dev_only_in_dev_mode() {
2313 let lock = lock_with(
2314 json!([]),
2315 json!([{"name": "acme/dev", "version": "1.0.0", "type": "library", "require": {"ext-zip": "*"}}]),
2316 json!({}),
2317 json!({"ext-zip": "*"}),
2318 );
2319 let (verdict, _) = verify(
2320 json!({}),
2321 lock.clone(),
2322 false,
2323 PlatformRequirementFilter::IgnoreNothing,
2324 );
2325 assert_eq!(verdict, InstallVerdict::Installable);
2326 let (verdict, _) = verify(
2327 json!({}),
2328 lock,
2329 true,
2330 PlatformRequirementFilter::IgnoreNothing,
2331 );
2332 assert!(
2333 matches!(verdict, InstallVerdict::Problems(_)),
2334 "{verdict:?}"
2335 );
2336 }
2337
2338 #[test]
2339 fn the_locks_platform_overrides_apply() {
2340 let mut lock = lock_with(
2341 json!([]),
2342 json!([]),
2343 json!({"php": ">=8.3", "ext-gd": "*"}),
2344 json!({}),
2345 );
2346 let (verdict, _) = verify(
2347 json!({}),
2348 lock.clone(),
2349 true,
2350 PlatformRequirementFilter::IgnoreNothing,
2351 );
2352 assert!(
2353 matches!(verdict, InstallVerdict::Problems(_)),
2354 "{verdict:?}"
2355 );
2356 lock["platform-overrides"] = json!({"php": "8.3.0", "ext-gd": "2.0.0"});
2357 let (verdict, _) = verify(
2358 json!({}),
2359 lock,
2360 true,
2361 PlatformRequirementFilter::IgnoreNothing,
2362 );
2363 assert_eq!(verdict, InstallVerdict::Installable);
2364 }
2365
2366 #[test]
2367 fn the_fixed_root_keeps_its_conflicts() {
2368 let lock = lock_with(
2369 json!([{"name": "acme/a", "version": "1.0.0", "type": "library"}]),
2370 json!([]),
2371 json!({}),
2372 json!({}),
2373 );
2374 let (verdict, _) = verify(
2375 json!({"name": "acme/root", "require": {"acme/a": "*"}, "conflict": {"acme/a": "*"}}),
2376 lock,
2377 true,
2378 PlatformRequirementFilter::IgnoreNothing,
2379 );
2380 let InstallVerdict::Problems(text) = verdict else {
2381 panic!("{verdict:?}")
2382 };
2383 assert!(
2384 text.contains("acme/root 1.0.0+no-version-set conflicts with acme/a 1.0.0."),
2385 "{text}"
2386 );
2387 }
2388
2389 #[test]
2390 fn without_php_the_composer_packages_remain() {
2391 let names: Vec<String> = platform_packages(&[], &Map::new())
2392 .expect("platform")
2393 .into_iter()
2394 .map(|p| p.name)
2395 .collect();
2396 assert_eq!(
2397 names,
2398 ["composer", "composer-plugin-api", "composer-runtime-api"]
2399 );
2400 }
2401
2402 #[test]
2403 fn a_lock_without_preferences_leaves_them_to_the_root() {
2404 let mut lock = lock_with(json!([]), json!([]), json!({}), json!({}));
2405 assert_eq!(LockSettings::of(&lock).prefer_stable, Some(false));
2406 lock.as_object_mut().expect("lock").remove("prefer-stable");
2407 lock.as_object_mut().expect("lock").remove("prefer-lowest");
2408 let settings = LockSettings::of(&lock);
2409 assert_eq!(
2410 (settings.prefer_stable, settings.prefer_lowest),
2411 (None, None)
2412 );
2413 }
2414
2415 #[test]
2416 fn a_locked_package_the_locks_stability_rejects_needs_changes() {
2417 let lock = lock_with(
2420 json!([{"name": "acme/dev", "version": "dev-main", "type": "library"}]),
2421 json!([]),
2422 json!({}),
2423 json!({}),
2424 );
2425 let (verdict, _) = verify(
2426 json!({"require": {"acme/dev": "dev-main"}}),
2427 lock.clone(),
2428 true,
2429 PlatformRequirementFilter::IgnoreNothing,
2430 );
2431 assert_eq!(verdict, InstallVerdict::NeedsChanges);
2432 let mut flagged = lock;
2433 flagged["stability-flags"] = json!({"acme/dev": 20});
2434 let (verdict, _) = verify(
2435 json!({"require": {"acme/dev": "dev-main"}}),
2436 flagged,
2437 true,
2438 PlatformRequirementFilter::IgnoreNothing,
2439 );
2440 assert_eq!(verdict, InstallVerdict::Installable);
2441 }
2442
2443 #[test]
2444 fn what_the_list_filter_removed_is_one_problem_among_the_others() {
2445 let lock = lock_with(
2446 json!([
2447 {"name": "acme/bad", "version": "1.1.0", "type": "library"},
2448 {"name": "acme/dev", "version": "dev-main", "type": "library"},
2449 ]),
2450 json!([]),
2451 json!({"ext-nope": "*"}),
2452 json!({}),
2453 );
2454 let (verdict, _) = verify_flagged(
2455 json!({}),
2456 lock,
2457 true,
2458 PlatformRequirementFilter::IgnoreNothing,
2459 &["acme/bad", "acme/dev"],
2460 );
2461 let InstallVerdict::Problems(text) = verdict else {
2462 panic!("{verdict:?}")
2463 };
2464 let nope = text.find("requires PHP extension ext-nope").expect(&text);
2467 let bad = text
2468 .find("acme/bad 1.1.0 (in the lock file) was not loaded, because it was flagged as malware reported by testsource")
2469 .expect(&text);
2470 assert!(nope < bad, "{text}");
2471 assert!(!text.contains("acme/dev"), "{text}");
2472 }
2473}