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;
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}
257
258impl UpdateOptions {
259 pub fn partial(packages: &[String], transitive: crate::pool::UpdateMode) -> UpdateOptions {
261 let mut allow_list: Vec<String> = Vec::new();
262 for p in packages {
263 let l = p.to_lowercase();
264 if !allow_list.contains(&l) {
265 allow_list.push(l);
266 }
267 }
268 UpdateOptions {
269 allow_list,
270 transitive: Some(transitive),
271 no_blocking: false,
272 }
273 }
274}
275
276pub struct UpdateSession {
278 pub arena: Vec<Package>,
279 pub root: RootPackage,
280 pub fixed_root: usize,
282 pub fixed_root_alias: Option<usize>,
283 pub platform: Vec<usize>,
284 pub locked: Option<Vec<usize>>,
285 pub set: RepositorySet,
286 pub request: Request,
287 pub config: MergedConfig,
288 pub dev_mode: bool,
289 pub prefer_stable: bool,
291 pub prefer_lowest: bool,
292 pub php_version: String,
295 pub policy_config: crate::policy_config::PolicyConfig,
297}
298
299impl UpdateSession {
300 pub fn prepare(
301 project_dir: &Path,
302 composer_home: Option<&Path>,
303 dev_mode: bool,
304 ) -> Result<UpdateSession, SessionError> {
305 Self::prepare_with(project_dir, composer_home, dev_mode, None)
306 }
307
308 pub fn prepare_with(
310 project_dir: &Path,
311 composer_home: Option<&Path>,
312 dev_mode: bool,
313 http: Option<HttpTransports>,
314 ) -> Result<UpdateSession, SessionError> {
315 Self::prepare_full(project_dir, composer_home, dev_mode, http, None)
316 }
317
318 pub fn prepare_full(
321 project_dir: &Path,
322 composer_home: Option<&Path>,
323 dev_mode: bool,
324 http: Option<HttpTransports>,
325 cache_repo_dir: Option<&Path>,
326 ) -> Result<UpdateSession, SessionError> {
327 Self::prepare_update(
328 project_dir,
329 composer_home,
330 dev_mode,
331 http,
332 cache_repo_dir,
333 &UpdateOptions::default(),
334 )
335 }
336
337 pub fn prepare_update(
340 project_dir: &Path,
341 composer_home: Option<&Path>,
342 dev_mode: bool,
343 http: Option<HttpTransports>,
344 cache_repo_dir: Option<&Path>,
345 options: &UpdateOptions,
346 ) -> Result<UpdateSession, SessionError> {
347 let partial_update = !options.allow_list.is_empty();
348 let manifest_path = project_dir.join("composer.json");
349 let manifest_text = std::fs::read_to_string(&manifest_path)
350 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
351 let manifest: Value = serde_json::from_str(&manifest_text)
352 .map_err(|e| SessionError::new(format!("{}: {e}", manifest_path.display())))?;
353 let config = MergedConfig::load(&manifest, composer_home)?;
354 let mut policy_config = crate::policy_config::PolicyConfig::from_raw(&config.policy)
355 .map_err(|e| SessionError::new(e.0))?;
356 policy_config
357 .apply_no_blocking(options.no_blocking)
358 .map_err(|e| SessionError::new(e.0))?;
359 let root = RootPackage::load(&manifest, project_dir).map_err(|e| SessionError::new(e.0))?;
360 let probed = probe().map_err(|e| SessionError::new(e.0))?;
361 let php_version = probed
365 .iter()
366 .find(|p| p.get("name").and_then(Value::as_str) == Some("php"))
367 .and_then(|p| p.get("version").and_then(Value::as_str))
368 .map(|v| {
369 v.split(['.', '-', '+'])
370 .take(3)
371 .map(|part| part.trim_end_matches(|c: char| !c.is_ascii_digit()))
372 .collect::<Vec<_>>()
373 .join(".")
374 })
375 .unwrap_or_default();
376 let platform_pkgs =
377 platform_packages(&probed, &config.platform).map_err(|e| SessionError::new(e.0))?;
378
379 let mut arena: Vec<Package> = Vec::new();
380
381 let mut fixed = root.package.clone();
383 fixed.requires = Default::default();
384 fixed.dev_requires = Default::default();
385 arena.push(fixed);
386 let fixed_root = 0;
387 let fixed_root_alias = root.branch_alias.as_ref().map(|(normalized, pretty)| {
388 let a = arena[fixed_root].alias(fixed_root, normalized, pretty);
389 arena.push(a);
390 arena.len() - 1
391 });
392 let root_members: Vec<usize> = fixed_root_alias
393 .into_iter()
394 .chain(std::iter::once(fixed_root))
395 .collect();
396
397 let mut platform: Vec<usize> = Vec::new();
398 for p in platform_pkgs {
399 arena.push(p);
400 platform.push(arena.len() - 1);
401 }
402
403 let lock_path = project_dir.join("composer.lock");
407 let lock: Option<Value> = if lock_path.is_file() {
408 let text = std::fs::read_to_string(&lock_path)
409 .map_err(|e| SessionError::new(format!("{}: {e}", lock_path.display())))?;
410 match serde_json::from_str::<Value>(&text) {
411 Ok(v) if v.get("packages").is_some_and(|p| !p.is_null()) => Some(v),
412 Ok(_) => None,
413 Err(e) => {
414 if partial_update {
415 return Err(SessionError::new(format!(
416 "\"{}\" does not contain valid JSON\n{e}",
417 lock_path.display()
418 )));
419 }
420 None
421 }
422 }
423 } else {
424 None
425 };
426 let locked = match &lock {
427 Some(v) => Some(locked_repository(v, &mut arena).map_err(|e| SessionError::new(e.0))?),
428 None => None,
429 };
430
431 let mut stability_flags = root.stability_flags.clone();
434 let root_version = match fixed_root_alias {
435 Some(a) => arena[a].version.clone(),
436 None => arena[fixed_root].version.clone(),
437 };
438 stability_flags.insert(
439 root.package.name.clone(),
440 stability_rank(parse_stability(&root_version)),
441 );
442
443 let mut root_requires: OrderedMap<Constraint> = OrderedMap::default();
448 let requires = match &root.branch_alias {
449 Some((normalized, pretty)) => {
450 let aliased = root.package.alias(0, normalized, pretty);
451 let mut all = aliased.requires.clone();
452 for l in aliased.dev_requires.iter() {
453 all.insert(l.clone());
454 }
455 all
456 }
457 None => root.all_requires(),
458 };
459 for link in requires.iter() {
460 root_requires.insert(&link.target, link.constraint.clone());
461 }
462
463 let mut set = RepositorySet::new(
464 &root.minimum_stability,
465 stability_flags,
466 &root.aliases,
467 root.references.clone(),
468 root_requires,
469 Default::default(),
470 );
471 set.add_repository(Repository::Root(root_members));
472 set.add_repository(Repository::Platform(platform.clone()));
473 for repo in &config.repositories {
474 set.add_repository(open_repository(repo, http.as_ref(), cache_repo_dir)?);
475 }
476 if let Some(ids) = &locked {
477 set.add_repository(Repository::Locked(ids.clone()));
478 }
479
480 if partial_update && locked.is_none() {
482 return Err(SessionError::new(
483 "Cannot update only a partial set of packages without a lock file present. Run `composer update` to generate a lock file.",
484 ));
485 }
486 let mut request = Request::new(locked.clone());
487 if partial_update {
488 request.set_update_allow_list(
489 options.allow_list.clone(),
490 options
491 .transitive
492 .unwrap_or(crate::pool::UpdateMode::OnlyListed),
493 );
494 }
495 if let Some(a) = fixed_root_alias {
496 request.fix_package(a);
497 }
498 request.fix_package(fixed_root);
499 for &p in &platform {
500 let provided = arena[fixed_root]
501 .provides
502 .get(&arena[p].name)
503 .map(|l| l.constraint.clone());
504 let provided_here = provided
505 .is_some_and(|c| c.matches(&Constraint::new(Op::Eq, arena[p].version.clone())));
506 if !provided_here {
507 request.fix_package(p);
508 }
509 }
510 for link in requires.iter() {
511 request.require_name(&link.target, Some(link.constraint.clone()))?;
512 }
513
514 Ok(UpdateSession {
515 arena,
516 root,
517 fixed_root,
518 fixed_root_alias,
519 platform,
520 locked,
521 set,
522 request,
523 config,
524 dev_mode,
525 prefer_stable: false,
526 prefer_lowest: false,
527 php_version,
528 policy_config,
529 })
530 }
531
532 pub fn create_pool(&mut self) -> Result<Pool, SessionError> {
533 Ok(self.set.create_pool(&mut self.request, &mut self.arena)?)
534 }
535
536 pub fn policy(&self) -> DefaultPolicy {
538 DefaultPolicy::new(
539 self.prefer_stable || self.root.prefer_stable,
540 self.prefer_lowest,
541 None,
542 )
543 }
544
545 pub fn extract_dev_packages(
549 &mut self,
550 transaction: &mut LockTransaction,
551 policy: &mut DefaultPolicy,
552 filter: &PlatformRequirementFilter,
553 ) -> Result<(), SessionError> {
554 if self.root.package.dev_requires.is_empty() {
555 return Ok(());
556 }
557 let dumps: Vec<Value> = transaction
560 .new_lock_packages(&self.arena, false)
561 .iter()
562 .map(|&idx| Value::Object(dump_package(&self.arena[idx])))
563 .collect();
564 let result_ids =
565 crate::loader::load_packages(&dumps, Origin::Result, &mut self.arena, false)
566 .map_err(|e| SessionError::new(e.0))?;
567 let mut members: Vec<usize> = Vec::new();
570 members.extend(self.fixed_root_alias);
571 members.push(self.fixed_root);
572 members.extend(self.platform.iter().copied());
573 members.extend(result_ids);
574 let mut pool_packages: Vec<usize> = Vec::new();
575 for idx in members {
576 pool_packages.push(idx);
577 let (name, version) = (
578 self.arena[idx].name.clone(),
579 self.arena[idx].version.clone(),
580 );
581 if let Some((alias, alias_normalized)) = self
582 .set
583 .root_aliases
584 .get(&name)
585 .and_then(|m| m.get(&version))
586 {
587 let mut base = idx;
588 while let Some(b) = self.arena[base].alias_of {
589 base = b;
590 }
591 let mut a = self.arena[base].alias(base, alias_normalized, alias);
592 a.root_package_alias = true;
593 a.origin = Origin::Detached;
594 self.arena.push(a);
595 pool_packages.push(self.arena.len() - 1);
596 }
597 }
598 let pool = Pool::new(pool_packages, Vec::new(), &self.arena);
599 let mut request = Request::new(None);
601 if let Some(a) = self.fixed_root_alias {
602 request.fix_package(a);
603 }
604 request.fix_package(self.fixed_root);
605 for &p in &self.platform {
606 let provided = self.arena[self.fixed_root]
607 .provides
608 .get(&self.arena[p].name)
609 .map(|l| l.constraint.clone());
610 let provided_here = provided.is_some_and(|c| {
611 c.matches(&Constraint::new(Op::Eq, self.arena[p].version.clone()))
612 });
613 if !provided_here {
614 request.fix_package(p);
615 }
616 }
617 let requires = match &self.root.branch_alias {
618 Some((normalized, pretty)) => self.root.package.alias(0, normalized, pretty).requires,
619 None => self.root.package.requires.clone(),
620 };
621 for link in requires.iter() {
622 request.require_name(&link.target, Some(link.constraint.clone()))?;
623 }
624 let mut solver = Solver::new(&pool, &self.arena);
625 let non_dev = solver.solve(&request, policy, filter).map_err(|e| match e {
626 SolveError::Problems(_) => SessionError {
627 message: "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.".into(),
628 kind: SessionErrorKind::Unsolvable,
629 },
630 SolveError::Bug(b) => SessionError::new(b),
631 })?;
632 transaction.set_non_dev_packages(&self.arena, &non_dev);
633 Ok(())
634 }
635
636 fn platform_requirements(links: &crate::package::Links) -> Map<String, Value> {
638 let mut out = Map::new();
639 for l in links.iter() {
640 if crate::platform::is_platform_package(&l.target) {
641 out.insert(l.target.clone(), Value::String(l.pretty_constraint.clone()));
642 }
643 }
644 out
645 }
646
647 pub fn lock_json(
649 &self,
650 transaction: &LockTransaction,
651 manifest_text: &str,
652 ) -> Result<Value, SessionError> {
653 let content_hash = vivacity_core::content_hash::content_hash(manifest_text)
654 .map_err(|e| SessionError::new(e.to_string()))?;
655 let packages = lock_packages(
656 &self.arena,
657 &transaction.new_lock_packages(&self.arena, false),
658 )
659 .map_err(SessionError::new)?;
660 let packages_dev = lock_packages(
661 &self.arena,
662 &transaction.new_lock_packages(&self.arena, true),
663 )
664 .map_err(SessionError::new)?;
665 let (requires, dev_requires) = match &self.root.branch_alias {
666 Some((normalized, pretty)) => {
667 let a = self.root.package.alias(0, normalized, pretty);
668 (a.requires, a.dev_requires)
669 }
670 None => (
671 self.root.package.requires.clone(),
672 self.root.package.dev_requires.clone(),
673 ),
674 };
675 Ok(lock_data(LockInput {
676 content_hash: &content_hash,
677 packages,
678 packages_dev: Some(packages_dev),
679 platform: Self::platform_requirements(&requires),
680 platform_dev: Self::platform_requirements(&dev_requires),
681 aliases: &transaction.aliases(&self.arena, &self.root.aliases),
682 minimum_stability: &self.root.minimum_stability,
683 stability_flags: &self.root.stability_flags,
684 prefer_stable: self.prefer_stable || self.root.prefer_stable,
685 prefer_lowest: self.prefer_lowest,
686 platform_overrides: &self.config.platform,
687 }))
688 }
689
690 pub fn update(
693 &mut self,
694 manifest_text: &str,
695 filter: &PlatformRequirementFilter,
696 ) -> Result<(Value, SolveReport), SessionError> {
697 let trace = std::env::var_os("VIVACITY_TRACE").is_some();
698 let t = std::time::Instant::now();
699 let lap = |label: &str, t: &std::time::Instant| {
700 if trace {
701 eprintln!(
702 "trace: {label:<22} {:>7.1} ms",
703 t.elapsed().as_secs_f64() * 1000.0
704 );
705 }
706 };
707 let mut policy = self.policy();
708 let pool = self.create_filtered_pool()?;
709 for w in &pool.warnings {
710 eprintln!("Warning: {w}");
711 }
712 lap("pool", &t);
713 let pool = if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
714 pool
715 } else {
716 PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, &mut policy)
717 };
718 lap("optimize", &t);
719 let mut report = self.solve(&pool, &mut policy, filter).map_err(|e| match e {
720 SolveError::Problems(p) => SessionError {
721 message: format!(
722 "Your requirements could not be resolved to an installable set of packages ({} problem(s)).",
723 p.len()
724 ),
725 kind: SessionErrorKind::Unsolvable,
726 },
727 SolveError::Bug(b) => SessionError::new(b),
728 })?;
729 lap("solve", &t);
730 for &idx in &report.transaction.all {
734 crate::lockfile::validate_package(&self.arena[idx]).map_err(SessionError::new)?;
735 }
736 drop(pool);
737 let mut transaction = std::mem::replace(&mut report.transaction, LockTransaction::empty());
738 self.extract_dev_packages(&mut transaction, &mut policy, filter)?;
739 lap("extract dev", &t);
740 let lock = self.lock_json(&transaction, manifest_text)?;
741 lap("lock data", &t);
742 report.transaction = transaction;
743 Ok((lock, report))
744 }
745
746 pub fn find_packages_for_require(
753 &mut self,
754 name: &str,
755 ignore_stability: bool,
756 ) -> Result<Vec<usize>, SessionError> {
757 let name = name.to_lowercase();
758 let mut acceptable = BTreeMap::new();
759 let min = crate::version::stability_rank(&self.root.minimum_stability);
760 for st in ["stable", "RC", "beta", "alpha", "dev"] {
761 let rank = crate::version::stability_rank(st);
762 if ignore_stability || rank <= min {
763 acceptable.insert(st.to_owned(), rank);
764 }
765 }
766 let flags = BTreeMap::new();
767 let already = BTreeMap::new();
768 let map = vec![(name.clone(), Constraint::MatchAll)];
769 let mut found: Vec<usize> = Vec::new();
770 let (_, ids) = crate::pool::array_repository_load_packages(
771 &self.platform,
772 &map,
773 &acceptable,
774 &flags,
775 &already,
776 &self.arena,
777 );
778 found.extend(ids);
779 for (i, repo) in self.set.repositories.iter().enumerate() {
780 if let Repository::Composer(repo) = repo {
781 let (_, ids) = repo
782 .load_packages(
783 &map,
784 &acceptable,
785 &flags,
786 &already,
787 Origin::Repository(i),
788 &mut self.arena,
789 )
790 .map_err(|e| SessionError::new(e.0))?;
791 found.extend(ids);
792 }
793 }
794 Ok(found)
795 }
796
797 pub fn create_optimized_pool(
798 &mut self,
799 policy: &mut DefaultPolicy,
800 ) -> Result<Pool, SessionError> {
801 let pool = self.create_filtered_pool()?;
802 if std::env::var("COMPOSER_POOL_OPTIMIZER").as_deref() == Ok("0") {
803 return Ok(pool);
804 }
805 Ok(PoolOptimizer::new().optimize(&self.request, &pool, &self.arena, policy))
806 }
807
808 pub fn create_filtered_pool(&mut self) -> Result<Pool, SessionError> {
811 let mut pool = self.create_pool()?;
812 let before = pool.len();
813 let mut warnings = Vec::new();
814 pool = crate::pool_filters::security_advisory_filter(
815 pool,
816 &self.arena,
817 &self.set.repositories,
818 &self.request,
819 &self.policy_config,
820 &mut warnings,
821 )
822 .map_err(|e| SessionError::new(e.0))?;
823 pool = crate::pool_filters::filter_list_filter(
824 pool,
825 &self.arena,
826 &self.set.repositories,
827 &self.request,
828 &self.policy_config,
829 "update",
830 &mut warnings,
831 )
832 .map_err(|e| SessionError::new(e.0))?;
833 pool.warnings.extend(warnings);
834 if std::env::var_os("VIVACITY_TRACE").is_some() {
835 eprintln!(
836 "trace: policy filters {before} → {} package versions ({} removed by lists)",
837 pool.len(),
838 pool.filter_list_removed
839 .values()
840 .map(Vec::len)
841 .sum::<usize>()
842 );
843 }
844 Ok(pool)
845 }
846
847 pub fn solve(
850 &self,
851 pool: &Pool,
852 policy: &mut DefaultPolicy,
853 filter: &PlatformRequirementFilter,
854 ) -> Result<SolveReport, SolveError> {
855 let mut solver = Solver::new(pool, &self.arena);
856 let transaction = solver.solve(&self.request, policy, filter)?;
857 let decisions = solver.decisions.queue.iter().map(|d| d.literal).collect();
858 Ok(SolveReport {
859 learned: solver
860 .rules
861 .ids_of_type(crate::rule::RuleType::Learned)
862 .len(),
863 rules: solver.rule_set_size(),
864 decisions,
865 transaction,
866 })
867 }
868}
869
870pub fn install_policy_problems(
876 project_dir: &Path,
877 composer_home: Option<&Path>,
878 http: Option<HttpTransports>,
879 cache_repo_dir: Option<&Path>,
880 with_dev: bool,
881 no_blocking: bool,
882) -> Result<(Vec<String>, Vec<String>), SessionError> {
883 let manifest_text = std::fs::read_to_string(project_dir.join("composer.json"))
884 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
885 let manifest: Value = serde_json::from_str(&manifest_text)
886 .map_err(|e| SessionError::new(format!("composer.json: {e}")))?;
887 let config = MergedConfig::load(&manifest, composer_home)?;
888 let mut policy = crate::policy_config::PolicyConfig::from_raw(&config.policy)
889 .map_err(|e| SessionError::new(e.0))?;
890 policy
891 .apply_no_blocking(no_blocking)
892 .map_err(|e| SessionError::new(e.0))?;
893 if !policy.malware_blocks("install") {
894 return Ok((Vec::new(), Vec::new()));
895 }
896 let lock_text = std::fs::read_to_string(project_dir.join("composer.lock"))
897 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
898 let lock: Value = serde_json::from_str(&lock_text)
899 .map_err(|e| SessionError::new(format!("composer.lock: {e}")))?;
900 let mut repositories: Vec<Repository> = Vec::new();
903 for repo in &config.repositories {
904 if repo.definition.get("type").and_then(Value::as_str) != Some("composer") {
905 continue;
906 }
907 repositories.push(open_repository_with(
910 repo,
911 http.as_ref(),
912 cache_repo_dir,
913 true,
914 )?);
915 }
916 let mut arena: Vec<Package> = Vec::new();
917 let locked = crate::repository::locked_repository_with(&lock, &mut arena, with_dev)
918 .map_err(|e| SessionError::new(e.0))?;
919 let mut request = Request::new(Some(locked.clone()));
920 for &idx in &locked {
921 request.fix_locked_package(idx);
922 }
923 let pool = Pool::new(locked.clone(), Vec::new(), &arena);
924 let mut warnings = Vec::new();
925 let pool = crate::pool_filters::filter_list_filter(
926 pool,
927 &arena,
928 &repositories,
929 &request,
930 &policy,
931 "install",
932 &mut warnings,
933 )
934 .map_err(|e| SessionError::new(e.0))?;
935 let problems: Vec<String> = locked
936 .iter()
937 .map(|&idx| &arena[idx])
938 .filter(|p| pool.is_filter_list_removed(&p.name, &p.version))
939 .map(|p| crate::pool_filters::locked_removed_problem_text(&pool, p))
940 .collect();
941 Ok((problems, warnings))
942}
943
944fn open_repository(
947 repo: &RepoConfig,
948 http: Option<&HttpTransports>,
949 cache_repo_dir: Option<&Path>,
950) -> Result<Repository, SessionError> {
951 open_repository_with(repo, http, cache_repo_dir, false)
952}
953
954fn open_repository_with(
958 repo: &RepoConfig,
959 http: Option<&HttpTransports>,
960 cache_repo_dir: Option<&Path>,
961 for_policies: bool,
962) -> Result<Repository, SessionError> {
963 let def = &repo.definition;
964 let kind = def.get("type").and_then(Value::as_str).ok_or_else(|| {
965 SessionError::new(format!(
966 "Repository \"{}\" ({def}) must have a type defined",
967 key_string(&repo.key)
968 ))
969 })?;
970 if kind != "composer" {
971 return Err(SessionError::new(format!(
972 "repository type \"{kind}\" is not supported by vivacity update yet ({})",
973 key_string(&repo.key)
974 )));
975 }
976 if !for_policies
977 && (def.get("only").is_some()
978 || def.get("exclude").is_some()
979 || def.get("canonical").is_some())
980 {
981 return Err(SessionError::new(format!(
982 "repository filters (only/exclude/canonical) are not supported by vivacity update yet ({})",
983 key_string(&repo.key)
984 )));
985 }
986 let url = def.get("url").and_then(Value::as_str).ok_or_else(|| {
987 SessionError::new(format!("repository {} has no url", key_string(&repo.key)))
988 })?;
989 let transport: Box<dyn crate::repository::Transport> = if url.starts_with("file://") {
990 Box::new(FileTransport)
991 } else if url.starts_with("http://") || url.starts_with("https://") || !url.contains("://") {
992 match http {
993 Some(h) => Box::new(HttpTransport {
994 fetch: h.0.clone(),
995 fetch_many: h.1.clone(),
996 post: h.2.clone(),
997 }),
998 None => {
999 return Err(SessionError::new(format!(
1000 "remote composer repositories need a network transport ({url})"
1001 )))
1002 }
1003 }
1004 } else {
1005 return Err(SessionError::new(format!(
1006 "unsupported repository url scheme ({url})"
1007 )));
1008 };
1009 let mut repo = ComposerRepository::open(url, transport).map_err(|e| SessionError::new(e.0))?;
1010 if let Some(options) = def.get("options") {
1011 repo.options = options.clone();
1012 }
1013 repo.set_user_filter(def.get("filter"))
1014 .map_err(|e| SessionError::new(e.0))?;
1015 if for_policies {
1016 repo.set_name_filter(def.get("only"), def.get("exclude"))
1017 .map_err(|e| SessionError::new(e.0))?;
1018 }
1019 if let Some(dir) = cache_repo_dir {
1020 repo.cache = Some(crate::metacache::MetadataCache::new(dir, &repo.url));
1021 }
1022 Ok(Repository::Composer(Box::new(repo)))
1023}
1024
1025fn key_string(key: &RepoKey) -> String {
1026 match key {
1027 RepoKey::Named(n) => n.clone(),
1028 RepoKey::Indexed(i) => i.to_string(),
1029 }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035 use serde_json::json;
1036
1037 fn names(repos: &[RepoConfig]) -> Vec<String> {
1038 repos.iter().map(|r| key_string(&r.key)).collect()
1039 }
1040
1041 #[test]
1042 fn merges_repositories_like_composer_config() {
1043 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1044 assert_eq!(names(&cfg.repositories), vec!["packagist.org"]);
1045 cfg.merge(&json!({"repositories": {"snapshot": {"type": "composer", "url": "file:///s"}, "packagist.org": false}}));
1047 assert_eq!(names(&cfg.repositories), vec!["snapshot"]);
1048 cfg.merge(&json!({"repositories": [{"type": "composer", "url": "https://packages.drupal.org/8"}]}));
1050 assert_eq!(names(&cfg.repositories), vec!["0", "snapshot"]);
1051 cfg.merge(
1054 &json!({"repositories": [{"type": "vcs", "url": "a"}, {"type": "vcs", "url": "b"}]}),
1055 );
1056 assert_eq!(names(&cfg.repositories), vec!["2", "1", "0", "snapshot"]);
1057 assert_eq!(cfg.repositories[0].definition["url"], "a");
1058 assert_eq!(cfg.repositories[1].definition["url"], "b");
1059 assert_eq!(
1060 cfg.repositories[2].definition["url"],
1061 "https://packages.drupal.org/8"
1062 );
1063 }
1064
1065 #[test]
1066 fn packagist_url_disables_default() {
1067 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1068 cfg.merge(
1069 &json!({"repositories": [{"type": "composer", "url": "https://repo.packagist.org"}]}),
1070 );
1071 assert_eq!(names(&cfg.repositories), vec!["0"]);
1072 let mut cfg = MergedConfig::load(&json!({}), None).unwrap();
1073 cfg.merge(&json!({"repositories": [{"packagist": false}]}));
1074 assert!(cfg.repositories.is_empty());
1075 }
1076}