1use crate::constraint::{Constraint, Op};
8use crate::lockfile::{dump_package, lock_data, lock_packages, LockInput};
9use crate::optimizer::PoolOptimizer;
10use crate::package::{Origin, Package};
11use crate::platform::{platform_packages, probe};
12use crate::platform_filter::PlatformRequirementFilter;
13use crate::policy::DefaultPolicy;
14use crate::pool::{OrderedMap, Pool, PoolError, Repository, RepositorySet, Request};
15use crate::repository::{
16 locked_repository, ComposerRepository, FileTransport, HttpTransport, HttpTransports,
17};
18use crate::root::RootPackage;
19use crate::solver::{SolveError, Solver};
20use crate::transaction::LockTransaction;
21use crate::version::{parse_stability, regex, stability_rank};
22use pcre2::bytes::Regex;
23use serde_json::{Map, Value};
24use std::collections::{BTreeMap, BTreeSet};
25use std::path::Path;
26use std::sync::OnceLock;
27
28#[derive(Debug, thiserror::Error)]
29#[error("{message}")]
30pub struct SessionError {
31 pub message: String,
32 pub kind: SessionErrorKind,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SessionErrorKind {
40 Other,
41 Unsolvable,
42}
43
44impl SessionError {
45 pub fn new(message: impl Into<String>) -> Self {
46 Self {
47 message: message.into(),
48 kind: SessionErrorKind::Other,
49 }
50 }
51}
52
53impl From<PoolError> for SessionError {
54 fn from(e: PoolError) -> SessionError {
55 SessionError::new(e.0)
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct RepoConfig {
62 pub key: RepoKey,
63 pub definition: Value,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum RepoKey {
68 Named(String),
69 Indexed(u64),
70}
71
72pub fn merge_repositories(current: &mut Vec<RepoConfig>, new: &Value) {
75 static PACKAGIST: OnceLock<Regex> = OnceLock::new();
76 let entries: Vec<(RepoKey, Value)> = match new {
77 Value::Array(list) => list
78 .iter()
79 .enumerate()
80 .map(|(i, v)| (RepoKey::Indexed(i as u64), v.clone()))
81 .collect(),
82 Value::Object(map) => map
83 .iter()
84 .map(|(k, v)| {
85 let key = match k.parse::<u64>() {
86 Ok(i) if i.to_string() == *k => RepoKey::Indexed(i),
87 _ => RepoKey::Named(k.clone()),
88 };
89 (key, v.clone())
90 })
91 .collect(),
92 _ => return,
93 };
94 if entries.is_empty() {
95 return;
96 }
97 current.reverse();
98 let disable = |current: &mut Vec<RepoConfig>, name: &str| {
101 let key = match name.parse::<u64>() {
102 Ok(i) if i.to_string() == name => RepoKey::Indexed(i),
103 _ => RepoKey::Named(name.to_owned()),
104 };
105 if current.iter().any(|r| r.key == key) {
106 current.retain(|r| r.key != key);
107 } else if name == "packagist" {
108 current.retain(|r| r.key != RepoKey::Named("packagist.org".into()));
109 }
110 };
111 for (key, repository) in entries.into_iter().rev() {
112 if repository == Value::Bool(false) {
113 let name = match &key {
114 RepoKey::Named(n) => n.clone(),
115 RepoKey::Indexed(i) => i.to_string(),
116 };
117 disable(current, &name);
118 continue;
119 }
120 if let Some(obj) = repository.as_object() {
121 if obj.len() == 1 && obj.values().next() == Some(&Value::Bool(false)) {
122 disable(current, obj.keys().next().map(String::as_str).unwrap_or(""));
123 continue;
124 }
125 }
126 if repository.get("type").and_then(Value::as_str) == Some("composer") {
127 if let Some(url) = repository.get("url").and_then(Value::as_str) {
128 let re = regex(
129 &PACKAGIST,
130 r"^https?://(?:[a-z0-9-.]+\.)?packagist.org(/|$)",
131 false,
132 );
133 if re.is_match(url.as_bytes()).unwrap_or(false) {
134 disable(current, "packagist.org");
135 }
136 }
137 }
138 match key {
139 RepoKey::Indexed(i) => {
140 if current.iter().any(|r| r.key == RepoKey::Indexed(i)) {
141 let next = current
142 .iter()
143 .filter_map(|r| match r.key {
144 RepoKey::Indexed(j) => Some(j + 1),
145 _ => None,
146 })
147 .max()
148 .unwrap_or(0);
149 current.push(RepoConfig {
150 key: RepoKey::Indexed(next),
151 definition: repository,
152 });
153 } else {
154 current.push(RepoConfig {
155 key: RepoKey::Indexed(i),
156 definition: repository,
157 });
158 }
159 }
160 RepoKey::Named(name) => {
161 let name = if name == "packagist" {
162 "packagist.org".to_owned()
163 } else {
164 name
165 };
166 let key = RepoKey::Named(name);
167 if let Some(slot) = current.iter_mut().find(|r| r.key == key) {
168 slot.definition = repository;
169 } else {
170 current.push(RepoConfig {
171 key,
172 definition: repository,
173 });
174 }
175 }
176 }
177 }
178 current.reverse();
179}
180
181#[derive(Debug, Clone, Default)]
183pub struct MergedConfig {
184 pub repositories: Vec<RepoConfig>,
185 pub platform: Map<String, Value>,
187 pub policy: crate::policy_config::RawPolicyConfig,
189}
190
191impl MergedConfig {
192 pub fn load(
195 manifest: &Value,
196 composer_home: Option<&Path>,
197 ) -> Result<MergedConfig, SessionError> {
198 let mut cfg = MergedConfig {
199 repositories: vec![RepoConfig {
200 key: RepoKey::Named("packagist.org".into()),
201 definition: serde_json::json!({"type": "composer", "url": "https://repo.packagist.org"}),
202 }],
203 platform: Map::new(),
204 policy: crate::policy_config::RawPolicyConfig::default(),
205 };
206 if let Some(home) = composer_home {
207 let global = home.join("config.json");
208 if global.is_file() {
209 let text = std::fs::read_to_string(&global)
210 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
211 let v: Value = serde_json::from_str(&text)
212 .map_err(|e| SessionError::new(format!("{}: {e}", global.display())))?;
213 cfg.merge(&v);
214 }
215 }
216 cfg.merge(manifest);
217 Ok(cfg)
218 }
219
220 fn merge(&mut self, config: &Value) {
221 if let Some(platform) = config.get("config").and_then(|c| c.get("platform")) {
224 self.platform = platform.as_object().cloned().unwrap_or_default();
225 }
226 if let Some(repos) = config.get("repositories") {
227 merge_repositories(&mut self.repositories, repos);
228 }
229 if let Some(cfg) = config.get("config").and_then(Value::as_object) {
230 self.policy.merge(cfg);
231 }
232 }
233}
234
235pub struct SolveReport {
238 pub transaction: LockTransaction,
239 pub decisions: Vec<i64>,
241 pub rules: usize,
243 pub learned: usize,
245}
246
247#[derive(Debug, Clone, Default)]
250pub struct UpdateOptions {
251 pub allow_list: Vec<String>,
253 pub transitive: Option<crate::pool::UpdateMode>,
254 pub no_blocking: bool,
256 pub root_patch: Option<crate::root::RootPatch>,
259}
260
261impl UpdateOptions {
262 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
264 let mut allow_list: Vec<String> = Vec::new();
265 for p in packages {
266 let l = p.to_lowercase();
267 if !allow_list.contains(&l) {
268 allow_list.push(l);
269 }
270 }
271 UpdateOptions {
272 allow_list,
273 transitive: Some(transitive),
274 ..Default::default()
275 }
276 }
277}
278
279pub struct UpdateSession {
281 pub arena: Vec<Package>,
282 pub root: RootPackage,
283 pub fixed_root: usize,
285 pub fixed_root_alias: Option<usize>,
286 pub platform: Vec<usize>,
287 pub locked: Option<Vec<usize>>,
288 pub set: RepositorySet,
289 pub request: Request,
290 pub config: MergedConfig,
291 pub dev_mode: bool,
292 pub prefer_stable: bool,
294 pub prefer_lowest: bool,
295 pub php_version: String,
298 pub policy_config: crate::policy_config::PolicyConfig,
300 pub ini_files: Vec<String>,
303 pub loaded_extensions: BTreeSet<String>,
305 pub installer_dev_mode: bool,
308}
309
310impl UpdateSession {
311 pub fn prepare(
312 project_dir: &Path,
313 composer_home: Option<&Path>,
314 dev_mode: bool,
315 ) -> Result<UpdateSession, SessionError> {
316 Self::prepare_with(project_dir, composer_home, dev_mode, None)
317 }
318
319 pub fn prepare_with(
321 project_dir: &Path,
322 composer_home: Option<&Path>,
323 dev_mode: bool,
324 http: Option<HttpTransports>,
325 ) -> Result<UpdateSession, SessionError> {
326 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
327 }
328
329 pub fn prepare_full(
332 project_dir: &Path,
333 composer_home: Option<&Path>,
334 dev_mode: bool,
335 http: Option<HttpTransports>,
336 cache_repo_dir: Option<&Path>,
337 ) -> Result<UpdateSession, SessionError> {
338 Self::prepare_update(
339 project_dir,
340 composer_home,
341 dev_mode,
342 http,
343 cache_repo_dir,
344 &UpdateOptions::default(),
345 )
346 }
347
348 pub fn prepare_update(
351 project_dir: &Path,
352 composer_home: Option<&Path>,
353 dev_mode: bool,
354 http: Option<HttpTransports>,
355 cache_repo_dir: Option<&Path>,
356 options: &UpdateOptions,
357 ) -> Result<UpdateSession, SessionError> {
358 let partial_update = !options.allow_list.is_empty();
359 let manifest_path = project_dir.join("composer.json");
360 let manifest_text = std::fs::read_to_string(&manifest_path)
361 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
362 let manifest: Value = serde_json::from_str(&manifest_text)
363 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
364 let config = MergedConfig::load(&manifest, composer_home)?;
365 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
366 .map_err(|e| SessionError::new(e.0))?;
367 policy_config
368 .apply_no_blocking(options.no_blocking)
369 .map_err(|e| SessionError::new(e.0))?;
370 let mut root =
371 RootPackage::load(&manifest, project_dir).map_err(|e| SessionError::new(e.0))?;
372 if let Some(patch) = &options.root_patch {
373 root.apply_patch(patch)
374 .map_err(|e| SessionError::new(e.0))?;
375 }
376 let probed = probe().map_err(|e| SessionError::new(e.0))?;
377 let php_version = probed
381 .iter()
382 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
383 .and_then(|p| p.get("version").and_then(Value::as_str))
384 .map(|v| {
385 v.split(['.', '-', '+'])
386 .take(3)
387 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
388 .collect::<Vec<_>>()
389 .join(".")
390 })
391 .unwrap_or_default();
392 let platform_pkgs =
393 platform_packages(&probed, &config.platform).map_err(|e| SessionError::new(e.0))?;
394
395 let mut arena: Vec<Package> = Vec::new();
396
397 let mut fixed = root.package.clone();
399 fixed.requires = Default::default();
400 fixed.dev_requires = Default::default();
401 arena.push(fixed);
402 let fixed_root = 0;
403 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
404 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
405 arena.push(a);
406 arena.len() - 1
407 });
408 let root_members: Vec<usize> = fixed_root_alias
409 .into_iter()
410 .chain(std::iter::once(fixed_root))
411 .collect();
412
413 let mut platform: Vec<usize> = Vec::new();
414 for p in platform_pkgs {
415 arena.push(p);
416 platform.push(arena.len() - 1);
417 }
418
419 let lock_path = project_dir.join("composer.lock");
423 let lock: Option<Value> = if lock_path.is_file() {
424 let text = std::fs::read_to_string(&lock_path)
425 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
426 match serde_json::from_str::<Value>(&text) {
427 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
428 Ok(_) => None,
429 Err(e) => {
430 if partial_update {
431 return Err(SessionError::new(format!(
432 "\"{}\" does not contain valid JSON\n{e}",
433 lock_path.display()
434 )));
435 }
436 None
437 }
438 }
439 } else {
440 None
441 };
442 let locked = match &lock {
443 Some(v) => Some(locked_repository(v, &mut arena).map_err(|e| SessionError::new(e.0))?),
444 None => None,
445 };
446
447 let mut stability_flags = root.stability_flags.clone();
450 let root_version = match fixed_root_alias {
451 Some(a) => arena[a].version.clone(),
452 None => arena[fixed_root].version.clone(),
453 };
454 stability_flags.insert(
455 root.package.name.clone(),
456 stability_rank(parse_stability(&root_version)),
457 );
458
459 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
464 let requires = match &root.branch_alias {
465 Some((normalized, pretty)) => {
466 let aliased = root.package.alias(0, normalized, pretty);
467 let mut all = aliased.requires.clone();
468 for l in aliased.dev_requires.iter() {
469 all.insert(l.clone());
470 }
471 all
472 }
473 None => root.all_requires(),
474 };
475 for link in requires.iter() {
476 root_requires.insert(&link.target, link.constraint.clone());
477 }
478
479 let mut set = RepositorySet::new(
480 &root.minimum_stability,
481 stability_flags,
482 &root.aliases,
483 root.references.clone(),
484 root_requires,
485 Default::default(),
486 );
487 set.add_repository(Repository::Root(root_members));
488 set.add_repository(Repository::Platform(platform.clone()));
489 for repo in &config.repositories {
490 set.add_repository(open_repository(repo, http.as_ref(), cache_repo_dir)?);
491 }
492 if let Some(ids) = &locked {
493 set.add_repository(Repository::Locked(ids.clone()));
494 }
495
496 if partial_update && locked.is_none() {
498 return Err(SessionError::new(
499 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.",
500 ));
501 }
502 let mut request = Request::new(locked.clone());
503 if partial_update {
504 request.set_update_allow_list(
505 options.allow_list.clone(),
506 options
507 .transitive
508 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
509 );
510 }
511 if let Some(a) = fixed_root_alias {
512 request.fix_package(a);
513 }
514 request.fix_package(fixed_root);
515 for &p in &platform {
516 let provided = arena[fixed_root]
517 .provides
518 .get(&arena[p].name)
519 .map(|l| l.constraint.clone());
520 let provided_here = provided
521 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
522 if !provided_here {
523 request.fix_package(p);
524 }
525 }
526 for link in requires.iter() {
527 request.require_name_pretty(
528 &link.target,
529 Some(link.constraint.clone()),
530 &link.pretty_constraint,
531 )?;
532 }
533
534 Ok(UpdateSession {
535 arena,
536 root,
537 fixed_root,
538 fixed_root_alias,
539 platform,
540 locked,
541 set,
542 request,
543 config,
544 dev_mode,
545 prefer_stable: false,
546 prefer_lowest: false,
547 php_version,
548 policy_config,
549 ini_files: crate::platform::ini_files(&probed),
550 loaded_extensions: crate::platform::loaded_extensions(&probed),
551 installer_dev_mode: true,
552 })
553 }
554
555 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
556 Ok(self.set.create_pool(&mut self.request, &mut self.arena)?)
557 }
558
559 pub fn policy(&self) -> DefaultPolicy {
561 DefaultPolicy::new(
562 self.prefer_stable || self.root.prefer_stable,
563 self.prefer_lowest,
564 None,
565 )
566 }
567
568 pub fn extract_dev_packages(
572 &mut self,
573 transaction: &mut LockTransaction,
574 policy: &mut DefaultPolicy,
575 filter: &PlatformRequirementFilter,
576 ) -> Result<(), SessionError> {
577 if self.root.package.dev_requires.is_empty() {
578 return Ok(());
579 }
580 let dumps: Vec<Value> = transaction
583 .new_lock_packages(&self.arena, false)
584 .iter()
585 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
586 .collect();
587 let result_ids =
588 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
589 .map_err(|e| SessionError::new(e.0))?;
590 let mut members: Vec<usize> = Vec::new();
593 members.extend(self.fixed_root_alias);
594 members.push(self.fixed_root);
595 members.extend(self.platform.iter().copied());
596 members.extend(result_ids);
597 let mut pool_packages: Vec<usize> = Vec::new();
598 for idx in members {
599 pool_packages.push(idx);
600 let (name, version) = (
601 self.arena[idx].name.clone(),
602 self.arena[idx].version.clone(),
603 );
604 if let Some((alias, alias_normalized)) = self
605 .set
606 .root_aliases
607 .get(&name)
608 .and_then(|m| m.get(&version))
609 {
610 let mut base = idx;
611 while let Some(b) = self.arena[base].alias_of {
612 base = b;
613 }
614 let mut a = self.arena[base].alias(base, alias_normalized, alias);
615 a.root_package_alias = true;
616 a.origin = Origin::Detached;
617 self.arena.push(a);
618 pool_packages.push(self.arena.len() - 1);
619 }
620 }
621 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
622 let mut request = Request::new(None);
624 if let Some(a) = self.fixed_root_alias {
625 request.fix_package(a);
626 }
627 request.fix_package(self.fixed_root);
628 for &p in &self.platform {
629 let provided = self.arena[self.fixed_root]
630 .provides
631 .get(&self.arena[p].name)
632 .map(|l| l.constraint.clone());
633 let provided_here = provided.is_some_and(|c| {
634 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
635 });
636 if !provided_here {
637 request.fix_package(p);
638 }
639 }
640 let requires = match &self.root.branch_alias {
641 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
642 None => self.root.package.requires.clone(),
643 };
644 for link in requires.iter() {
645 request.require_name_pretty(
646 &link.target,
647 Some(link.constraint.clone()),
648 &link.pretty_constraint,
649 )?;
650 }
651 let mut solver = Solver::new(&pool, &self.arena);
652 let non_dev = match solver.solve(&request, policy, filter) {
653 Ok(t) => t,
654 Err(SolveError::Problems(problems)) => {
655 drop(solver);
656 let pretty = {
659 let mut ctx = crate::problem::MessageContext {
660 arena: &mut self.arena,
661 set: &self.set,
662 pool: &pool,
663 request: &request,
664 is_verbose: false,
665 ini_files: &self.ini_files,
666 loaded_extensions: &self.loaded_extensions,
667 };
668 crate::problem::pretty_string(&mut ctx, &problems, true)
669 };
670 return Err(SessionError {
671 message: format!("Unable to find a compatible set of packages based on your non-dev requirements alone.\nYour requirements can be resolved successfully when require-dev packages are present.\nYou may need to move packages from require-dev or some of their dependencies to require.\n{pretty}"),
672 kind: SessionErrorKind::Unsolvable,
673 });
674 }
675 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
676 };
677 transaction.set_non_dev_packages(&self.arena, &non_dev);
678 Ok(())
679 }
680
681 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
683 let mut out = Map::new();
684 for l in links.iter() {
685 if crate::platform::is_platform_package(&l.target) {
686 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
687 }
688 }
689 out
690 }
691
692 pub fn lock_json(
694 &self,
695 transaction: &LockTransaction,
696 manifest_text: &str,
697 ) -> Result<Value, SessionError> {
698 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
699 .map_err(|e| SessionError::new(e.to_string()))?;
700 let packages = lock_packages(
701 &self.arena,
702 &transaction.new_lock_packages(&self.arena, false),
703 )
704 .map_err(SessionError::new)?;
705 let packages_dev = lock_packages(
706 &self.arena,
707 &transaction.new_lock_packages(&self.arena, true),
708 )
709 .map_err(SessionError::new)?;
710 let (requires, dev_requires) = match &self.root.branch_alias {
711 Some((normalized, pretty)) => {
712 let a = self.root.package.alias(0, normalized, pretty);
713 (a.requires, a.dev_requires)
714 }
715 None => (
716 self.root.package.requires.clone(),
717 self.root.package.dev_requires.clone(),
718 ),
719 };
720 Ok(lock_data(LockInput {
721 content_hash: &content_hash,
722 packages,
723 packages_dev: Some(packages_dev),
724 platform: Self::platform_requirements(&requires),
725 platform_dev: Self::platform_requirements(&dev_requires),
726 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
727 minimum_stability: &self.root.minimum_stability,
728 stability_flags: &self.root.stability_flags,
729 prefer_stable: self.prefer_stable || self.root.prefer_stable,
730 prefer_lowest: self.prefer_lowest,
731 platform_overrides: &self.config.platform,
732 }))
733 }
734
735 pub fn update(
738 &mut self,
739 manifest_text: &str,
740 filter: &PlatformRequirementFilter,
741 ) -> Result<(Value, SolveReport), SessionError> {
742 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
743 let t = std::time::Instant::now();
744 let lap = |label: &str, t: &std::time::Instant| {
745 if trace {
746 eprintln!(
747 "trace: {label:<22} {:>7.1} ms",
748 t.elapsed().as_secs_f64() * 1000.0
749 );
750 }
751 };
752 let mut policy = self.policy();
753 let pool = self.create_filtered_pool()?;
754 for w in &pool.warnings {
755 eprintln!("{w}");
756 }
757 lap("pool", &t);
758 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
759 pool
760 } else {
761 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
762 };
763 lap("optimize", &t);
764 let mut report = match self.solve(&pool, &mut policy, filter) {
765 Ok(r) => r,
766 Err(SolveError::Problems(problems)) => {
767 let pretty = self.pretty_problems(&pool, &problems, false);
770 let mut message = format!(
771 "Your requirements could not be resolved to an installable set of packages.\n{pretty}"
772 );
773 if !self.installer_dev_mode {
774 message.push_str("\nRunning update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.");
775 }
776 return Err(SessionError {
777 message,
778 kind: SessionErrorKind::Unsolvable,
779 });
780 }
781 Err(SolveError::Bug(b)) => return Err(SessionError::new(b)),
782 };
783 lap("solve", &t);
784 for &idx in &report.transaction.all {
788 crate::lockfile::validate_package(&self.arena[idx]).map_err(SessionError::new)?;
789 }
790 drop(pool);
791 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
792 self.extract_dev_packages(&mut transaction, &mut policy, filter)?;
793 lap("extract dev", &t);
794 let lock = self.lock_json(&transaction, manifest_text)?;
795 lap("lock data", &t);
796 report.transaction = transaction;
797 Ok((lock, report))
798 }
799
800 pub fn pretty_problems(
803 &mut self,
804 pool: &Pool,
805 problems: &[crate::solver::SolvedProblem],
806 is_dev_extraction: bool,
807 ) -> String {
808 let mut ctx = crate::problem::MessageContext {
809 arena: &mut self.arena,
810 set: &self.set,
811 pool,
812 request: &self.request,
813 is_verbose: false,
814 ini_files: &self.ini_files,
815 loaded_extensions: &self.loaded_extensions,
816 };
817 crate::problem::pretty_string(&mut ctx, problems, is_dev_extraction)
818 }
819
820 pub fn find_packages_for_require(
827 &mut self,
828 name: &str,
829 ignore_stability: bool,
830 ) -> Result<Vec<usize>, SessionError> {
831 let name = name.to_lowercase();
832 let mut acceptable = BTreeMap::new();
833 let min = crate::version::stability_rank(&self.root.minimum_stability);
834 for st in ["stable", "RC", "beta", "alpha", "dev"] {
835 let rank = crate::version::stability_rank(st);
836 if ignore_stability || rank <= min {
837 acceptable.insert(st.to_owned(), rank);
838 }
839 }
840 let flags = BTreeMap::new();
841 let already = BTreeMap::new();
842 let map = vec![(name.clone(), Constraint::MatchAll)];
843 let mut found: Vec<usize> = Vec::new();
844 let (_, ids) = crate::pool::array_repository_load_packages(
845 &self.platform,
846 &map,
847 &acceptable,
848 &flags,
849 &already,
850 &self.arena,
851 );
852 found.extend(ids);
853 for (i, repo) in self.set.repositories.iter().enumerate() {
854 if let Repository::Composer(repo) = repo {
855 let (_, ids) = repo
856 .load_packages(
857 &map,
858 &acceptable,
859 &flags,
860 &already,
861 Origin::Repository(i),
862 &mut self.arena,
863 )
864 .map_err(|e| SessionError::new(e.0))?;
865 found.extend(ids);
866 }
867 }
868 Ok(found)
869 }
870
871 pub fn create_optimized_pool(
872 &mut self,
873 policy: &mut DefaultPolicy,
874 ) -> Result<Pool, SessionError> {
875 let pool = self.create_filtered_pool()?;
876 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
877 return Ok(pool);
878 }
879 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
880 }
881
882 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
885 let mut pool = self.create_pool()?;
886 let before = pool.len();
887 let mut warnings = Vec::new();
888 pool = crate::pool_filters::security_advisory_filter(
889 pool,
890 &self.arena,
891 &self.set.repositories,
892 &self.request,
893 &self.policy_config,
894 &mut warnings,
895 )
896 .map_err(|e| SessionError::new(e.0))?;
897 pool = crate::pool_filters::filter_list_filter(
898 pool,
899 &self.arena,
900 &self.set.repositories,
901 &self.request,
902 &self.policy_config,
903 "update",
904 &mut warnings,
905 )
906 .map_err(|e| SessionError::new(e.0))?;
907 pool.warnings.extend(warnings);
908 if std::env::var_os("VIVACITY_TRACE").is_some() {
909 eprintln!(
910 "trace: policy filters {before} → {} package versions ({} removed by lists)",
911 pool.len(),
912 pool.filter_list_removed
913 .values()
914 .map(Vec::len)
915 .sum::<usize>()
916 );
917 }
918 Ok(pool)
919 }
920
921 pub fn solve(
924 &self,
925 pool: &Pool,
926 policy: &mut DefaultPolicy,
927 filter: &PlatformRequirementFilter,
928 ) -> Result<SolveReport, SolveError> {
929 let mut solver = Solver::new(pool, &self.arena);
930 let transaction = solver.solve(&self.request, policy, filter)?;
931 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
932 Ok(SolveReport {
933 learned: solver
934 .rules
935 .ids_of_type(crate::rule::RuleType::Learned)
936 .len(),
937 rules: solver.rule_set_size(),
938 decisions,
939 transaction,
940 })
941 }
942}
943
944pub fn install_policy_problems(
950 project_dir: &Path,
951 composer_home: Option<&Path>,
952 http: Option<HttpTransports>,
953 cache_repo_dir: Option<&Path>,
954 with_dev: bool,
955 no_blocking: bool,
956) -> Result<(Vec<String>, Vec<String>), SessionError> {
957 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
958 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
959 let manifest: Value = serde_json::from_str(&manifest_text)
960 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
961 let config = MergedConfig::load(&manifest, composer_home)?;
962 let mut policy = crate::policy_config::PolicyConfig::from_raw(&config.policy)
963 .map_err(|e| SessionError::new(e.0))?;
964 policy
965 .apply_no_blocking(no_blocking)
966 .map_err(|e| SessionError::new(e.0))?;
967 if !policy.malware_blocks("install") {
968 return Ok((Vec::new(), Vec::new()));
969 }
970 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
971 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
972 let lock: Value = serde_json::from_str(&lock_text)
973 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
974 let mut repositories: Vec<Repository> = Vec::new();
977 for repo in &config.repositories {
978 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
979 continue;
980 }
981 repositories.push(open_repository_with(
984 repo,
985 http.as_ref(),
986 cache_repo_dir,
987 true,
988 )?);
989 }
990 let mut arena: Vec<Package> = Vec::new();
991 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
992 .map_err(|e| SessionError::new(e.0))?;
993 let mut request = Request::new(Some(locked.clone()));
994 for &idx in &locked {
995 request.fix_locked_package(idx);
996 }
997 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
998 let mut warnings = Vec::new();
999 let pool = crate::pool_filters::filter_list_filter(
1000 pool,
1001 &arena,
1002 &repositories,
1003 &request,
1004 &policy,
1005 "install",
1006 &mut warnings,
1007 )
1008 .map_err(|e| SessionError::new(e.0))?;
1009 let problems: Vec<String> = locked
1010 .iter()
1011 .map(|&idx| &arena[idx])
1012 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
1013 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
1014 .collect();
1015 Ok((problems, warnings))
1016}
1017
1018fn open_repository(
1021 repo: &RepoConfig,
1022 http: Option<&HttpTransports>,
1023 cache_repo_dir: Option<&Path>,
1024) -> Result<Repository, SessionError> {
1025 open_repository_with(repo, http, cache_repo_dir, false)
1026}
1027
1028fn open_repository_with(
1032 repo: &RepoConfig,
1033 http: Option<&HttpTransports>,
1034 cache_repo_dir: Option<&Path>,
1035 for_policies: bool,
1036) -> Result<Repository, SessionError> {
1037 let def = &repo.definition;
1038 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
1039 SessionError::new(format!(
1040 "Repository \"{}\" ({def}) must have a type defined",
1041 key_string(&repo.key)
1042 ))
1043 })?;
1044 if kind != "composer" {
1045 return Err(SessionError::new(format!(
1046 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
1047 key_string(&repo.key)
1048 )));
1049 }
1050 if !for_policies
1051 && (def.get("only").is_some()
1052 || def.get("exclude").is_some()
1053 || def.get("canonical").is_some())
1054 {
1055 return Err(SessionError::new(format!(
1056 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
1057 key_string(&repo.key)
1058 )));
1059 }
1060 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
1061 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
1062 })?;
1063 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
1064 Box::new(FileTransport)
1065 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
1066 match http {
1067 Some(h) => Box::new(HttpTransport {
1068 fetch: h.0.clone(),
1069 fetch_many: h.1.clone(),
1070 post: h.2.clone(),
1071 }),
1072 None => {
1073 return Err(SessionError::new(format!(
1074 "remote composer repositories need a network transport ({url})"
1075 )))
1076 }
1077 }
1078 } else {
1079 return Err(SessionError::new(format!(
1080 "unsupported repository url scheme ({url})"
1081 )));
1082 };
1083 let mut repo = ComposerRepository::open(url, transport).map_err(|e| SessionError::new(e.0))?;
1084 if let Some(options) = def.get("options") {
1085 repo.options = options.clone();
1086 }
1087 repo.set_user_filter(def.get("filter"))
1088 .map_err(|e| SessionError::new(e.0))?;
1089 if for_policies {
1090 repo.set_name_filter(def.get("only"), def.get("exclude"))
1091 .map_err(|e| SessionError::new(e.0))?;
1092 }
1093 if let Some(dir) = cache_repo_dir {
1094 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1095 }
1096 Ok(Repository::Composer(Box::new(repo)))
1097}
1098
1099fn key_string(key: &RepoKey) -> String {
1100 match key {
1101 RepoKey::Named(n) => n.clone(),
1102 RepoKey::Indexed(i) => i.to_string(),
1103 }
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109 use serde_json::json;
1110
1111 fn names(repos: &[RepoConfig]) -> Vec<String> {
1112 repos.iter().map(|r| key_string(&r.key)).collect()
1113 }
1114
1115 #[test]
1116 fn merges_repositories_like_composer_config() {
1117 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1118 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1119 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1121 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1122 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1124 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1125 cfg.merge(
1128 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1129 );
1130 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1131 assert_eq!(cfg.repositories[0].definition["url"], "a");
1132 assert_eq!(cfg.repositories[1].definition["url"], "b");
1133 assert_eq!(
1134 cfg.repositories[2].definition["url"],
1135 "https://packages.drupal.org/8"
1136 );
1137 }
1138
1139 #[test]
1140 fn packagist_url_disables_default() {
1141 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1142 cfg.merge(
1143 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1144 );
1145 assert_eq!(names(&cfg.repositories), vec!["0"]);
1146 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1147 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1148 assert!(cfg.repositories.is_empty());
1149 }
1150}