From 547df1de93c95bc7c787798d48e5b49d79ffb86f Mon Sep 17 00:00:00 2001 From: PurHur Date: Tue, 28 Jul 2026 20:55:36 +0200 Subject: [PATCH] Release: declare extension dependencies and default-enabled on Module (Phase 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RELEASE-PLAN Phase 2.5 wants extensions to become separate, discoverable, side-loadable modules. The blocker measured in #24285: Module declares no dependencies and no default-enabled flag, so the set cannot be selected per build and the load order lives only in a hand-maintained list in Runtime::loadCoreModules(). Adds two declarations, with defaults in ModuleAbstract that preserve current behaviour exactly: getExtensionDependencies(): list default [] — ordering constraints, today implicit isDefaultEnabled(): bool default true — all 76 load unconditionally now Safe to add to the interface: all 76 ext/*/Module.php extend ModuleAbstract and it is the only implementor, so nothing needs updating. Declares the libxml family, where the constraint is real and currently only remembered — php-src ext/dom, simplexml, xml, xmlreader, xmlwriter build on ext/libxml, and xsl additionally on dom: dom, simplexml, xml, xmlreader, xmlwriter -> libxml xsl -> libxml, dom A declaration nobody verifies is worse than none — it reads as authoritative while being free to drift — so script/check-extension-dependencies.php asserts every declared dependency exists, is loaded, and loads BEFORE its dependent in Runtime::loadCoreModules(). It reorders nothing; its job is to prove the declarations describe reality, which is the precondition for later deriving the order from them instead of hand-maintaining it. Validated in both directions: as declared ok — 6 extensions, 7 edges, all satisfied with dom -> xsl (xsl loads later) FAILED: dom (load #6) declares dependency "xsl" but xsl loads later at #7 exit 1 Nothing consumes isDefaultEnabled() yet. The declaration comes first so the set can be made selectable without a flag day. Gate (touches lib/Module.php and lib/ModuleAbstract.php, which every extension implements), branch vs a clean master checkout, run sequentially, both cold, waiting for each sweep to COMPLETE before diffing: cold aot-smoke : 8/8 both sides VM --repeat 2 : 110/110 both sides, exit 0 AOT --repeat 3 : master 18 failing, branch 18 failing regressions (branch-only): NONE fixed (master-only) : NONE Co-Authored-By: Claude Opus 5 (1M context) --- ext/dom/Module.php | 13 +++ ext/simplexml/Module.php | 13 +++ ext/xml/Module.php | 13 +++ ext/xmlreader/Module.php | 13 +++ ext/xmlwriter/Module.php | 13 +++ ext/xsl/Module.php | 13 +++ lib/Module.php | 22 +++++ lib/ModuleAbstract.php | 25 ++++++ script/check-extension-dependencies.php | 107 ++++++++++++++++++++++++ 9 files changed, 232 insertions(+) create mode 100644 script/check-extension-dependencies.php diff --git a/ext/dom/Module.php b/ext/dom/Module.php index 93a23c05108..e0f955a3e40 100644 --- a/ext/dom/Module.php +++ b/ext/dom/Module.php @@ -15,6 +15,19 @@ */ class Module extends ModuleAbstract { + + /** + * php-src ext/dom builds on ext/libxml (libxml2). + * + * Runtime::loadCoreModules() already loads them in this order; declaring it makes the + * constraint checkable instead of remembered (RELEASE-PLAN Phase 2.5). + * + * @return list + */ + public function getExtensionDependencies(): array + { + return ['libxml']; + } /** php-src ext/dom/php_dom.h DOM_API_VERSION — libxml DOM module version (#15439). */ private const DOM_API_VERSION = '20031129'; diff --git a/ext/simplexml/Module.php b/ext/simplexml/Module.php index 06a4a9ce4a6..75b5940103f 100644 --- a/ext/simplexml/Module.php +++ b/ext/simplexml/Module.php @@ -14,6 +14,19 @@ */ class Module extends ModuleAbstract { + + /** + * php-src ext/simplexml builds on ext/libxml (libxml2). + * + * Runtime::loadCoreModules() already loads them in this order; declaring it makes the + * constraint checkable instead of remembered (RELEASE-PLAN Phase 2.5). + * + * @return list + */ + public function getExtensionDependencies(): array + { + return ['libxml']; + } public function init(Runtime $runtime): void { parent::init($runtime); diff --git a/ext/xml/Module.php b/ext/xml/Module.php index 3f0139a60e3..a7140984598 100644 --- a/ext/xml/Module.php +++ b/ext/xml/Module.php @@ -14,6 +14,19 @@ */ class Module extends ModuleAbstract { + + /** + * php-src ext/xml builds on ext/libxml (libxml2). + * + * Runtime::loadCoreModules() already loads them in this order; declaring it makes the + * constraint checkable instead of remembered (RELEASE-PLAN Phase 2.5). + * + * @return list + */ + public function getExtensionDependencies(): array + { + return ['libxml']; + } public function init(Runtime $runtime): void { parent::init($runtime); diff --git a/ext/xmlreader/Module.php b/ext/xmlreader/Module.php index f103425c97d..5d04ad6ff73 100644 --- a/ext/xmlreader/Module.php +++ b/ext/xmlreader/Module.php @@ -14,6 +14,19 @@ */ class Module extends ModuleAbstract { + + /** + * php-src ext/xmlreader builds on ext/libxml (libxml2). + * + * Runtime::loadCoreModules() already loads them in this order; declaring it makes the + * constraint checkable instead of remembered (RELEASE-PLAN Phase 2.5). + * + * @return list + */ + public function getExtensionDependencies(): array + { + return ['libxml']; + } public function init(Runtime $runtime): void { parent::init($runtime); diff --git a/ext/xmlwriter/Module.php b/ext/xmlwriter/Module.php index e1dee2a9b7b..c6c5c11222c 100644 --- a/ext/xmlwriter/Module.php +++ b/ext/xmlwriter/Module.php @@ -14,6 +14,19 @@ */ class Module extends ModuleAbstract { + + /** + * php-src ext/xmlwriter builds on ext/libxml (libxml2). + * + * Runtime::loadCoreModules() already loads them in this order; declaring it makes the + * constraint checkable instead of remembered (RELEASE-PLAN Phase 2.5). + * + * @return list + */ + public function getExtensionDependencies(): array + { + return ['libxml']; + } public function init(Runtime $runtime): void { parent::init($runtime); diff --git a/ext/xsl/Module.php b/ext/xsl/Module.php index 2ade8e19048..cd8a2f7fe46 100644 --- a/ext/xsl/Module.php +++ b/ext/xsl/Module.php @@ -16,6 +16,19 @@ */ class Module extends ModuleAbstract { + + /** + * php-src ext/xsl builds on ext/libxml (libxml2) and ext/dom. + * + * Runtime::loadCoreModules() already loads them in this order; declaring it makes the + * constraint checkable instead of remembered (RELEASE-PLAN Phase 2.5). + * + * @return list + */ + public function getExtensionDependencies(): array + { + return ['libxml', 'dom']; + } public function getExtensionVersion(): string { if (XsltHostBridge::available()) { diff --git a/lib/Module.php b/lib/Module.php index a90b05d4886..e291392050e 100755 --- a/lib/Module.php +++ b/lib/Module.php @@ -25,6 +25,28 @@ public function getExtensionVersion(): string; */ public function getAdditionalExtensionNames(): array; + /** + * Extensions that must be loaded before this one (RELEASE-PLAN Phase 2.5). + * + * Today the load order is a hand-maintained list in Runtime::loadCoreModules(), where the + * ordering constraints are real but implicit — libxml before dom, dom before xsl/simplexml. + * Declaring them here lets the order be derived and checked instead of remembered. + * + * Names are extension names as returned by getExtensionName() (e.g. 'libxml'), lowercase. + * + * @return list + */ + public function getExtensionDependencies(): array; + + /** + * Is this extension part of the default build set? + * + * Every extension returns true today, which is exactly the current behaviour: all 76 are loaded + * unconditionally and a script that never touches an extension still pays for it. This is the + * declaration a per-build extension set will select on; nothing consumes it for that yet. + */ + public function isDefaultEnabled(): bool; + /** * Logical extension versions bundled with this module (e.g. pcre in standard). * diff --git a/lib/ModuleAbstract.php b/lib/ModuleAbstract.php index 0254f598f01..5f280c7a118 100755 --- a/lib/ModuleAbstract.php +++ b/lib/ModuleAbstract.php @@ -45,6 +45,31 @@ public function getAdditionalExtensionNames(): array return []; } + /** + * No declared dependencies by default (RELEASE-PLAN Phase 2.5). + * + * Overriding this is how an extension states an ordering constraint that is currently only + * implicit in Runtime::loadCoreModules() — e.g. ext/dom depends on libxml. Defaulting to none + * keeps every existing module behaving exactly as before. + * + * @return list + */ + public function getExtensionDependencies(): array + { + return []; + } + + /** + * Default-enabled, matching today's behaviour: all 76 extensions load unconditionally. + * + * An extension that should be opt-in overrides this to false. Nothing selects on it yet — the + * declaration comes first so the set can be made selectable without a flag day. + */ + public function isDefaultEnabled(): bool + { + return true; + } + /** * @return array */ diff --git a/script/check-extension-dependencies.php b/script/check-extension-dependencies.php new file mode 100644 index 00000000000..4005fb22580 --- /dev/null +++ b/script/check-extension-dependencies.php @@ -0,0 +1,107 @@ +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"); + exit(2); +} +$position = array_flip($order); + +// Declared dependencies, read from source rather than by loading 76 modules (which would need a +// full Runtime and is far more than this check needs). +$declared = []; +foreach (glob($root.'/ext/*/Module.php') ?: [] as $path) { + $ext = basename(dirname($path)); + $src = (string) file_get_contents($path); + if (!preg_match('/function getExtensionDependencies\(\): array\s*\{\s*return \[(.*?)\];/s', $src, $dm)) { + continue; + } + if (!preg_match_all("/'([^']+)'/", $dm[1], $names)) { + continue; + } + $declared[$ext] = array_map('strtolower', $names[1]); +} + +$problems = []; +foreach ($declared as $ext => $deps) { + foreach ($deps as $dep) { + if (!is_dir($root.'/ext/'.$dep)) { + $problems[] = sprintf('%s declares dependency "%s" but ext/%s does not exist', $ext, $dep, $dep); + continue; + } + if (!isset($position[$dep])) { + $problems[] = sprintf('%s declares dependency "%s" but %s is never loaded', $ext, $dep, $dep); + continue; + } + if (!isset($position[$ext])) { + continue; // the dependent itself is not loaded; not this check's concern + } + if ($position[$dep] > $position[$ext]) { + $problems[] = sprintf( + '%s (load #%d) declares dependency "%s" but %s loads later at #%d', + $ext, + $position[$ext], + $dep, + $dep, + $position[$dep] + ); + } + } +} + +if ($json) { + echo json_encode([ + 'status' => [] === $problems ? 'ok' : 'fail', + 'declared_count' => count($declared), + 'edge_count' => array_sum(array_map('count', $declared)), + 'loaded_count' => count($order), + 'problems' => $problems, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n"; + exit([] === $problems ? 0 : 1); +} + +$edges = array_sum(array_map('count', $declared)); +if ([] === $problems) { + printf( + "check-extension-dependencies: ok — %d extension(s) declare %d dependency edge(s), all satisfied by the load order\n", + count($declared), + $edges + ); + exit(0); +} + +fwrite(STDERR, "check-extension-dependencies: FAILED\n"); +foreach ($problems as $p) { + fwrite(STDERR, ' '.$p."\n"); +} +fwrite(STDERR, "\nEither the declaration is wrong or Runtime::loadCoreModules() is ordered wrongly.\n"); +exit(1);