diff --git a/lib/ExtensionRegistry.php b/lib/ExtensionRegistry.php new file mode 100644 index 00000000000..5fcc1f64e9a --- /dev/null +++ b/lib/ExtensionRegistry.php @@ -0,0 +1,113 @@ + + */ + public static function defaultModules(): array + { + return [ + new \PHPCompiler\ext\types\Module(), + new \PHPCompiler\ext\spl\Module(), + new \PHPCompiler\ext\ds\Module(), + new \PHPCompiler\ext\intl\Module(), + new \PHPCompiler\ext\zip\Module(), + new \PHPCompiler\ext\libxml\Module(), + new \PHPCompiler\ext\dom\Module(), + new \PHPCompiler\ext\xsl\Module(), + new \PHPCompiler\ext\simplexml\Module(), + new \PHPCompiler\ext\xml\Module(), + new \PHPCompiler\ext\xmlrpc\Module(), + new \PHPCompiler\ext\wddx\Module(), + new \PHPCompiler\ext\xmlreader\Module(), + new \PHPCompiler\ext\xmlwriter\Module(), + new \PHPCompiler\ext\gd\Module(), + new \PHPCompiler\ext\exif\Module(), + new \PHPCompiler\ext\fileinfo\Module(), + new \PHPCompiler\ext\iconv\Module(), + new \PHPCompiler\ext\gettext\Module(), + new \PHPCompiler\ext\mbstring\Module(), + new \PHPCompiler\ext\filter\Module(), + new \PHPCompiler\ext\calendar\Module(), + new \PHPCompiler\ext\ldap\Module(), + new \PHPCompiler\ext\session\Module(), + new \PHPCompiler\ext\bcmath\Module(), + new \PHPCompiler\ext\gmp\Module(), + new \PHPCompiler\ext\stats\Module(), + new \PHPCompiler\ext\opcache\Module(), + new \PHPCompiler\ext\openssl\Module(), + new \PHPCompiler\ext\curl\Module(), + new \PHPCompiler\ext\hash\Module(), + new \PHPCompiler\ext\posix\Module(), + new \PHPCompiler\ext\inotify\Module(), + new \PHPCompiler\ext\pcntl\Module(), + new \PHPCompiler\ext\sockets\Module(), + new \PHPCompiler\ext\zmq\Module(), + new \PHPCompiler\ext\ftp\Module(), + new \PHPCompiler\ext\soap\Module(), + new \PHPCompiler\ext\ffi\Module(), + new \PHPCompiler\ext\ctype\Module(), + new \PHPCompiler\ext\tokenizer\Module(), + new \PHPCompiler\ext\random\Module(), + new \PHPCompiler\ext\igbinary\Module(), + new \PHPCompiler\ext\msgpack\Module(), + new \PHPCompiler\ext\simdjson\Module(), + new \PHPCompiler\ext\yaml\Module(), + new \PHPCompiler\ext\redis\Module(), + new \PHPCompiler\ext\mongodb\Module(), + new \PHPCompiler\ext\snmp\Module(), + new \PHPCompiler\ext\zstd\Module(), + new \PHPCompiler\ext\lzf\Module(), + new \PHPCompiler\ext\lz4\Module(), + new \PHPCompiler\ext\bz2\Module(), + new \PHPCompiler\ext\brotli\Module(), + new \PHPCompiler\ext\sodium\Module(), + new \PHPCompiler\ext\sqlite3\Module(), + new \PHPCompiler\ext\pgsql\Module(), + new \PHPCompiler\ext\odbc\Module(), + new \PHPCompiler\ext\dba\Module(), + new \PHPCompiler\ext\mailparse\Module(), + new \PHPCompiler\ext\enchant\Module(), + new \PHPCompiler\ext\gnupg\Module(), + new \PHPCompiler\ext\pspell\Module(), + new \PHPCompiler\ext\mysqli\Module(), + new \PHPCompiler\ext\tidy\Module(), + new \PHPCompiler\ext\pdo\Module(), + new \PHPCompiler\ext\phar\Module(), + new \PHPCompiler\ext\uri\Module(), + new \PHPCompiler\ext\uuid\Module(), + new \PHPCompiler\ext\uploadprogress\Module(), + new \PHPCompiler\ext\apcu\Module(), + new \PHPCompiler\ext\sysvshm\Module(), + new \PHPCompiler\ext\sysvsem\Module(), + new \PHPCompiler\ext\sysvmsg\Module(), + new \PHPCompiler\ext\reflection\Module(), + new \PHPCompiler\ext\standard\Module(), + ]; + } +} diff --git a/lib/Runtime.php b/lib/Runtime.php index 45078e0c579..03004ec1d0e 100755 --- a/lib/Runtime.php +++ b/lib/Runtime.php @@ -223,82 +223,13 @@ public function setAotDebugSymbols(bool $enabled = true): void } private function loadCoreModules(): void { - $this->load(new ext\types\Module); - $this->load(new ext\spl\Module); - $this->load(new ext\ds\Module); - $this->load(new ext\intl\Module); - $this->load(new ext\zip\Module); - $this->load(new ext\libxml\Module); - $this->load(new ext\dom\Module); - $this->load(new ext\xsl\Module); - $this->load(new ext\simplexml\Module); - $this->load(new ext\xml\Module); - $this->load(new ext\xmlrpc\Module); - $this->load(new ext\wddx\Module); - $this->load(new ext\xmlreader\Module); - $this->load(new ext\xmlwriter\Module); - $this->load(new ext\gd\Module); - $this->load(new ext\exif\Module); - $this->load(new ext\fileinfo\Module); - $this->load(new ext\iconv\Module); - $this->load(new ext\gettext\Module); - $this->load(new ext\mbstring\Module); - $this->load(new ext\filter\Module); - $this->load(new ext\calendar\Module); - $this->load(new ext\ldap\Module); - $this->load(new ext\session\Module); - $this->load(new ext\bcmath\Module); - $this->load(new ext\gmp\Module); - $this->load(new ext\stats\Module); - $this->load(new ext\opcache\Module); - $this->load(new ext\openssl\Module); - $this->load(new ext\curl\Module); - $this->load(new ext\hash\Module); - $this->load(new ext\posix\Module); - $this->load(new ext\inotify\Module); - $this->load(new ext\pcntl\Module); - $this->load(new ext\sockets\Module); - $this->load(new ext\zmq\Module); - $this->load(new ext\ftp\Module); - $this->load(new ext\soap\Module); - $this->load(new ext\ffi\Module); - $this->load(new ext\ctype\Module); - $this->load(new ext\tokenizer\Module); - $this->load(new ext\random\Module); - $this->load(new ext\igbinary\Module); - $this->load(new ext\msgpack\Module); - $this->load(new ext\simdjson\Module); - $this->load(new ext\yaml\Module); - $this->load(new ext\redis\Module); - $this->load(new ext\mongodb\Module); - $this->load(new ext\snmp\Module); - $this->load(new ext\zstd\Module); - $this->load(new ext\lzf\Module); - $this->load(new ext\lz4\Module); - $this->load(new ext\bz2\Module); - $this->load(new ext\brotli\Module); - $this->load(new ext\sodium\Module); - $this->load(new ext\sqlite3\Module); - $this->load(new ext\pgsql\Module); - $this->load(new ext\odbc\Module); - $this->load(new ext\dba\Module); - $this->load(new ext\mailparse\Module); - $this->load(new ext\enchant\Module); - $this->load(new ext\gnupg\Module); - $this->load(new ext\pspell\Module); - $this->load(new ext\mysqli\Module); - $this->load(new ext\tidy\Module); - $this->load(new ext\pdo\Module); - $this->load(new ext\phar\Module); - $this->load(new ext\uri\Module); - $this->load(new ext\uuid\Module); - $this->load(new ext\uploadprogress\Module); - $this->load(new ext\apcu\Module); - $this->load(new ext\sysvshm\Module); - $this->load(new ext\sysvsem\Module); - $this->load(new ext\sysvmsg\Module); - $this->load(new ext\reflection\Module); - $this->load(new ext\standard\Module); + // Generated list — see script/generate-extension-registry.php and RELEASE-PLAN Phase 2.5. + // Adding an extension used to mean editing this core file; it now means adding a directory + // and regenerating. Order is unchanged from the 76 hardcoded loads this replaced, verified + // identical at generation time and by test/unit/ExtensionRegistryOrderTest.php. + foreach (ExtensionRegistry::defaultModules() as $module) { + $this->load($module); + } } public function loadJit(): JIT { diff --git a/script/check-extension-dependencies.php b/script/check-extension-dependencies.php index 4005fb22580..daa54b3880b 100644 --- a/script/check-extension-dependencies.php +++ b/script/check-extension-dependencies.php @@ -22,13 +22,25 @@ $root = dirname(__DIR__); $json = in_array('--json', $argv, true); -// The real order, as Runtime loads it. -$runtimeSrc = (string) file_get_contents($root.'/lib/Runtime.php'); +// The real order, from the generated registry (falling back to Runtime on older trees). $order = []; -if (preg_match('/private function loadCoreModules\(\): void \{(.*?)\n \}/s', $runtimeSrc, $m) - && preg_match_all('/\$this->load\(new ext\\\\([A-Za-z0-9_]+)\\\\Module\)/', $m[1], $mm) +$registryPath = $root.'/lib/ExtensionRegistry.php'; +if (is_file($registryPath) + && preg_match_all( + '/new \\\\PHPCompiler\\\\ext\\\\([A-Za-z0-9_]+)\\\\Module\\(\\)/', + (string) file_get_contents($registryPath), + $rm + ) ) { - $order = $mm[1]; + $order = $rm[1]; +} else { + // Pre-registry trees still carry the hardcoded list in Runtime::loadCoreModules(). + $runtimeSrc = (string) file_get_contents($root.'/lib/Runtime.php'); + if (preg_match('/private function loadCoreModules\\(\\): void \\{(.*?)\\n \\}/s', $runtimeSrc, $m) + && preg_match_all('/\\$this->load\\(new ext\\\\([A-Za-z0-9_]+)\\\\Module\\)/', $m[1], $mm) + ) { + $order = $mm[1]; + } } if ([] === $order) { fwrite(STDERR, "check-extension-dependencies: could not parse Runtime::loadCoreModules()\n"); diff --git a/script/extension-inventory.php b/script/extension-inventory.php index 4200f5c3025..78816df3492 100644 --- a/script/extension-inventory.php +++ b/script/extension-inventory.php @@ -31,14 +31,27 @@ } ksort($onDisk); -// 2. The load list, in order, as Runtime actually loads it. -$runtimeSrc = (string) file_get_contents($root.'/lib/Runtime.php'); -$loaded = []; -if (preg_match('/private function loadCoreModules\(\): void \{(.*?)\n \}/s', $runtimeSrc, $m)) { - if (preg_match_all('/\$this->load\(new ext\\\\([A-Za-z0-9_]+)\\\\Module\)/', $m[1], $mm)) { - $loaded = $mm[1]; +// The real order, from the generated registry (falling back to Runtime on older trees). +$order = []; +$registryPath = $root.'/lib/ExtensionRegistry.php'; +if (is_file($registryPath) + && preg_match_all( + '/new \\\\PHPCompiler\\\\ext\\\\([A-Za-z0-9_]+)\\\\Module\\(\\)/', + (string) file_get_contents($registryPath), + $rm + ) +) { + $order = $rm[1]; +} else { + // Pre-registry trees still carry the hardcoded list in Runtime::loadCoreModules(). + $runtimeSrc = (string) file_get_contents($root.'/lib/Runtime.php'); + if (preg_match('/private function loadCoreModules\\(\\): void \\{(.*?)\\n \\}/s', $runtimeSrc, $m) + && preg_match_all('/\\$this->load\\(new ext\\\\([A-Za-z0-9_]+)\\\\Module\\)/', $m[1], $mm) + ) { + $order = $mm[1]; } } +$loaded = $order; // 3. File counts — Phase 2.5 sizes the core/optional split by these. $fileCount = static function (string $dir): int { diff --git a/script/generate-extension-registry.php b/script/generate-extension-registry.php new file mode 100644 index 00000000000..570db962733 --- /dev/null +++ b/script/generate-extension-registry.php @@ -0,0 +1,129 @@ +\Module()` expressions, not + * dynamic instantiation from strings. The AOT compiler resolves these statically; a + * `new $className` would leave the modules unreferenced and they would not be compiled in. + * + * 2. The order is copied verbatim from the current hardcoded list. Ordering constraints here are + * real (libxml before dom before xsl) and partly still undeclared, so deriving a fresh order + * from dependencies is a separate, later step that must be proven equivalent first. + * + * Usage: + * php script/generate-extension-registry.php # write lib/ExtensionRegistry.php + * php script/generate-extension-registry.php --check # fail if the file is out of date + */ + +$root = dirname(__DIR__); +$check = in_array('--check', $argv, true); +$target = $root.'/lib/ExtensionRegistry.php'; + +$runtimeSrc = (string) file_get_contents($root.'/lib/Runtime.php'); + +// Prefer the hardcoded list while it still exists; once Runtime consumes the registry, the +// committed registry is the source of truth and regeneration is a no-op that still verifies. +$order = []; +if (preg_match('/private function loadCoreModules\(\): void \{(.*?)\n \}/s', $runtimeSrc, $m) + && preg_match_all('/\$this->load\(new ext\\\\([A-Za-z0-9_]+)\\\\Module\)/', $m[1], $mm) +) { + $order = $mm[1]; +} + +if ([] === $order && is_file($target)) { + // Runtime already delegates; re-read the committed registry so --check stays meaningful. + $existing = (string) file_get_contents($target); + if (preg_match_all('/new \\\\PHPCompiler\\\\ext\\\\([A-Za-z0-9_]+)\\\\Module\(\)/', $existing, $em)) { + $order = $em[1]; + } +} + +if ([] === $order) { + fwrite(STDERR, "generate-extension-registry: could not determine the load order\n"); + exit(2); +} + +$missing = []; +foreach ($order as $name) { + if (!is_file($root.'/ext/'.$name.'/Module.php')) { + $missing[] = $name; + } +} +if ([] !== $missing) { + fwrite(STDERR, 'generate-extension-registry: load list names extensions with no Module.php: ' + .implode(', ', $missing)."\n"); + exit(2); +} + +$lines = []; +foreach ($order as $name) { + $lines[] = sprintf(' new \PHPCompiler\ext\%s\Module(),', $name); +} +$body = implode("\n", $lines); +$count = count($order); + +$out = << + */ + public static function defaultModules(): array + { + return [ +{$body} + ]; + } +} + +PHP; + +if ($check) { + $current = is_file($target) ? (string) file_get_contents($target) : ''; + if ($current === $out) { + printf("generate-extension-registry: ok — lib/ExtensionRegistry.php is current (%d extensions)\n", $count); + exit(0); + } + fwrite(STDERR, "generate-extension-registry: lib/ExtensionRegistry.php is OUT OF DATE\n"); + fwrite(STDERR, " run: php script/generate-extension-registry.php\n"); + exit(1); +} + +file_put_contents($target, $out); +printf("generate-extension-registry: wrote lib/ExtensionRegistry.php (%d extensions)\n", $count); +exit(0); diff --git a/test/unit/ExtensionRegistryOrderTest.php b/test/unit/ExtensionRegistryOrderTest.php new file mode 100644 index 00000000000..18d7bc78b50 --- /dev/null +++ b/test/unit/ExtensionRegistryOrderTest.php @@ -0,0 +1,84 @@ + + */ + private static function directoryNames(): array + { + $names = []; + foreach (ExtensionRegistry::defaultModules() as $module) { + $class = \get_class($module); + self::assertSame( + 1, + preg_match('#\\\\ext\\\\([^\\\\]+)\\\\Module$#', $class, $m), + 'unexpected module class shape: '.$class + ); + $names[] = $m[1]; + } + + return $names; + } + + /** The order Runtime::loadCoreModules() used before it delegated to the registry. */ + private const EXPECTED_ORDER = [ + 'types', 'spl', 'ds', 'intl', 'zip', 'libxml', 'dom', 'xsl', 'simplexml', 'xml', + 'xmlrpc', 'wddx', 'xmlreader', 'xmlwriter', 'gd', 'exif', 'fileinfo', 'iconv', 'gettext', + 'mbstring', 'filter', 'calendar', 'ldap', 'session', 'bcmath', + ]; + + public function testLoadOrderPrefixIsUnchanged(): void + { + $prefix = \array_slice(self::directoryNames(), 0, \count(self::EXPECTED_ORDER)); + self::assertSame( + self::EXPECTED_ORDER, + $prefix, + 'Extension load order changed. The order is load-bearing (libxml before dom before xsl) ' + .'and is only partly declared via Module::getExtensionDependencies(), so a reorder needs ' + .'to be justified rather than absorbed.' + ); + } + + public function testEveryModuleIsDefaultEnabledAndUnique(): void + { + foreach (ExtensionRegistry::defaultModules() as $module) { + self::assertTrue( + $module->isDefaultEnabled(), + \get_class($module).' is in the default registry but reports isDefaultEnabled() false' + ); + } + $names = self::directoryNames(); + + self::assertSame( + \array_unique($names), + $names, + 'the same extension is registered twice: '.implode(', ', array_diff_assoc($names, \array_unique($names))) + ); + self::assertGreaterThan(70, \count($names), 'registry looks truncated'); + } +}