@@ -390,22 +390,22 @@ protected function do_get_available_packages($type)
390390 }
391391 }
392392
393- foreach ($ compatible_packages as $ name => $ versions )
393+ foreach ($ compatible_packages as $ package_name => $ package_versions )
394394 {
395395 // Determine the highest version of the package
396396 /** @var CompletePackage|CompleteAliasPackage $highest_version */
397397 $ highest_version = null ;
398398
399399 // Sort the versions array in descending order
400- usort ($ versions , function ($ a , $ b )
400+ usort ($ package_versions , function ($ a , $ b )
401401 {
402402 return version_compare ($ b ->getVersion (), $ a ->getVersion ());
403403 });
404404
405405 // The first element in the sorted array is the highest version
406- if (!empty ($ versions ))
406+ if (!empty ($ package_versions ))
407407 {
408- $ highest_version = $ versions [0 ];
408+ $ highest_version = $ package_versions [0 ];
409409
410410 // If highest version is a non-numeric dev branch, it's an instance of CompleteAliasPackage,
411411 // so we need to get the package being aliased in order to show the true non-numeric version.
@@ -416,23 +416,23 @@ protected function do_get_available_packages($type)
416416 }
417417
418418 // Generates the entry
419- $ available [$ name ] = [];
420- $ available [$ name ]['name ' ] = $ highest_version ->getPrettyName ();
421- $ available [$ name ]['display_name ' ] = $ highest_version ->getExtra ()['display-name ' ];
422- $ available [$ name ]['composer_name ' ] = $ highest_version ->getName ();
423- $ available [$ name ]['version ' ] = $ highest_version ->getPrettyVersion ();
419+ $ available [$ package_name ] = [];
420+ $ available [$ package_name ]['name ' ] = $ highest_version ->getPrettyName ();
421+ $ available [$ package_name ]['display_name ' ] = $ highest_version ->getExtra ()['display-name ' ];
422+ $ available [$ package_name ]['composer_name ' ] = $ highest_version ->getName ();
423+ $ available [$ package_name ]['version ' ] = $ highest_version ->getPrettyVersion ();
424424
425425 if ($ highest_version instanceof CompletePackage)
426426 {
427- $ available [$ name ]['description ' ] = $ highest_version ->getDescription ();
428- $ available [$ name ]['url ' ] = $ highest_version ->getHomepage ();
429- $ available [$ name ]['authors ' ] = $ highest_version ->getAuthors ();
427+ $ available [$ package_name ]['description ' ] = $ highest_version ->getDescription ();
428+ $ available [$ package_name ]['url ' ] = $ highest_version ->getHomepage ();
429+ $ available [$ package_name ]['authors ' ] = $ highest_version ->getAuthors ();
430430 }
431431 else
432432 {
433- $ available [$ name ]['description ' ] = '' ;
434- $ available [$ name ]['url ' ] = '' ;
435- $ available [$ name ]['authors ' ] = [];
433+ $ available [$ package_name ]['description ' ] = '' ;
434+ $ available [$ package_name ]['url ' ] = '' ;
435+ $ available [$ package_name ]['authors ' ] = [];
436436 }
437437 }
438438
@@ -545,8 +545,6 @@ private function get_compatible_versions(array $compatible_packages, ConstraintI
545545 */
546546 protected function generate_ext_json_file (array $ packages )
547547 {
548- $ io = new NullIO ();
549-
550548 $ composer = $ this ->get_composer (null );
551549
552550 $ core_packages = $ this ->get_core_packages ($ composer );
@@ -587,8 +585,148 @@ protected function generate_ext_json_file(array $packages)
587585 $ lockFile ->write ([]);
588586 }
589587
588+ // First pass write: base file with requested packages as provided
590589 $ json_file ->write ($ ext_json_data );
591590 $ this ->ext_json_file_backup = $ ext_json_file_backup ;
591+
592+ // Second pass: resolve and pin the highest compatible versions for unconstrained requested packages
593+ try
594+ {
595+ // Build a list of requested packages without explicit constraints
596+ $ unconstrained = [];
597+ foreach ($ packages as $ name => $ constraint )
598+ {
599+ // The $packages array can be either ['vendor/package' => '^1.2'] or ['vendor/package'] (numeric keys).
600+ if (is_int ($ name ))
601+ {
602+ // Numeric key means just a name
603+ $ package_name = $ constraint ;
604+ $ unconstrained [$ package_name ] = true ;
605+ }
606+ else
607+ {
608+ // If constraint is empty or '*' treat as unconstrained
609+ if ($ constraint === '' || $ constraint === '* ' || $ constraint === null )
610+ {
611+ $ unconstrained [$ name ] = true ;
612+ }
613+ }
614+ }
615+
616+ if (!empty ($ unconstrained ))
617+ {
618+ // Load composer on the just-written file so repositories and core constraints are available
619+ $ ext_composer = $ this ->get_composer ($ this ->get_composer_ext_json_filename ());
620+
621+ /** @var ConstraintInterface $core_constraint */
622+ $ core_constraint = $ ext_composer ->getPackage ()->getRequires ()['phpbb/phpbb ' ]->getConstraint ();
623+ $ core_stability = $ ext_composer ->getPackage ()->getMinimumStability ();
624+
625+ // Resolve highest compatible versions for each unconstrained package
626+ $ pins = $ this ->resolve_highest_versions (array_keys ($ unconstrained ), $ ext_composer , $ core_constraint , $ core_stability );
627+
628+ if (!empty ($ pins ))
629+ {
630+ // Merge pins into require section, overwriting unconstrained entries
631+ foreach ($ pins as $ pkg => $ version )
632+ {
633+ $ ext_json_data ['require ' ][$ pkg ] = $ version ;
634+ }
635+
636+ // Rewrite composer-ext.json with pinned versions
637+ $ json_file ->write ($ ext_json_data );
638+ }
639+ }
640+ }
641+ catch (\Exception $ e )
642+ {
643+ // If resolution fails for any reason, keep the first-pass file intact (Composer will still resolve).
644+ // Intentionally swallow to avoid breaking installation flow.
645+ }
646+ }
647+
648+ /**
649+ * Resolve the highest compatible versions for the given package names
650+ * based on repositories and phpBB/PHP constraints from the provided Composer instance.
651+ *
652+ * @param array $package_names list of package names to resolve
653+ * @param Composer|PartialComposer $composer Composer instance configured with repositories
654+ * @param ConstraintInterface $core_constraint phpBB version constraint
655+ * @param string $core_stability minimum stability
656+ * @return array [packageName => prettyVersion]
657+ */
658+ protected function resolve_highest_versions (array $ package_names , $ composer , ConstraintInterface $ core_constraint , $ core_stability ): array
659+ {
660+ $ compatible_packages = [];
661+ $ repositories = $ composer ->getRepositoryManager ()->getRepositories ();
662+
663+ foreach ($ repositories as $ repository )
664+ {
665+ try
666+ {
667+ if ($ repository instanceof ComposerRepository)
668+ {
669+ foreach ($ package_names as $ name )
670+ {
671+ $ versions = $ repository ->findPackages ($ name );
672+ if (!empty ($ versions ))
673+ {
674+ $ compatible_packages = $ this ->get_compatible_versions ($ compatible_packages , $ core_constraint , $ core_stability , $ name , $ versions );
675+ }
676+ }
677+ }
678+ else
679+ {
680+ // Preload and filter by name for non-composer repositories
681+ $ package_name = [];
682+ foreach ($ repository ->getPackages () as $ package )
683+ {
684+ $ name = $ package ->getName ();
685+ if (in_array ($ name , $ package_names , true ))
686+ {
687+ $ package_name [$ name ][] = $ package ;
688+ }
689+ }
690+
691+ foreach ($ package_name as $ name => $ versions )
692+ {
693+ $ compatible_packages = $ this ->get_compatible_versions ($ compatible_packages , $ core_constraint , $ core_stability , $ name , $ versions );
694+ }
695+ }
696+ }
697+ catch (\Exception $ e )
698+ {
699+ // If a repo fails, just skip it.
700+ continue ;
701+ }
702+ }
703+
704+ $ pins = [];
705+ foreach ($ package_names as $ name )
706+ {
707+ if (empty ($ compatible_packages [$ name ]))
708+ {
709+ continue ;
710+ }
711+
712+ $ package_versions = $ compatible_packages [$ name ];
713+
714+ // Sort descending by normalized version
715+ usort ($ package_versions , function ($ a , $ b ) {
716+ return version_compare ($ b ->getVersion (), $ a ->getVersion ());
717+ });
718+
719+ $ highest = $ package_versions [0 ];
720+ if ($ highest instanceof CompleteAliasPackage)
721+ {
722+ $ highest = $ highest ->getAliasOf ();
723+ }
724+
725+ // Pin to the resolved highest compatible version using its pretty version
726+ $ pins [$ name ] = $ highest ->getPrettyVersion ();
727+ }
728+
729+ return $ pins ;
592730 }
593731
594732 /**
0 commit comments