From 62050f2407519fcfbfbca1a7710434baf533b616 Mon Sep 17 00:00:00 2001 From: Artur Kyryliuk Date: Mon, 21 Sep 2026 12:55:30 +0200 Subject: [PATCH] fix(installer): load ExecWithFallback before the CLI composer step cli-install.php runs composer update before the core bootstraps, so the vendor autoloader is not registered when ExecWithFallback::exec() is called and a fresh install without --skipComposer=y fataled with 'Class ExecWithFallback\ExecWithFallback not found'. Co-Authored-By: Claude Opus 5 --- .../Install/CliInstallExecAutoloadTest.php | 47 +++++++++++++++++++ install/cli-install.php | 6 +++ install/src/functions.php | 34 ++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 core/tests/Unit/Install/CliInstallExecAutoloadTest.php diff --git a/core/tests/Unit/Install/CliInstallExecAutoloadTest.php b/core/tests/Unit/Install/CliInstallExecAutoloadTest.php new file mode 100644 index 0000000000..c9e6371e81 --- /dev/null +++ b/core/tests/Unit/Install/CliInstallExecAutoloadTest.php @@ -0,0 +1,47 @@ +&1')); + unlink($probe); + + expect($out)->toBe('[true,true,true]'); +}); + +it('reports a missing package instead of fataling', function () { + $core = sys_get_temp_dir() . '/evo_exec_missing_' . uniqid() . '/'; + $probe = tempnam(sys_get_temp_dir(), 'evo_exec_probe_'); + file_put_contents($probe, '&1')); + unlink($probe); + + expect($out)->toBe('false'); +}); + +it('makes the package loadable before the installer calls it', function () use ($root) { + $installer = (string) file_get_contents($root . '/install/cli-install.php'); + + expect(strpos($installer, 'loadExecWithFallback()')) + ->toBeLessThan(strpos($installer, 'ExecWithFallback::exec($cmd')); +}); diff --git a/install/cli-install.php b/install/cli-install.php index 56d567901a..8f65db103b 100644 --- a/install/cli-install.php +++ b/install/cli-install.php @@ -53,6 +53,12 @@ class InstallEvo */ protected function runComposerUpdate(string $cmd): void { + // This runs before the core bootstraps, so nothing has registered the Composer autoloader yet. + if (!loadExecWithFallback()) { + warning('⚠ The exec-with-fallback package is missing. Run "composer update" manually.'); + return; + } + $out = []; $exitCode = 0; try { diff --git a/install/src/functions.php b/install/src/functions.php index 9b31d67873..a5c373d3d2 100644 --- a/install/src/functions.php +++ b/install/src/functions.php @@ -1065,6 +1065,40 @@ function seed($folder = 'install') } } +/** + * Make the ExecWithFallback classes loadable without the Composer autoloader. + * + * The CLI installer runs "composer update" before the core bootstraps, so the vendor autoloader + * is not registered yet when the installer wants the package. Only its own PSR-4 directory is + * mapped: requiring vendor/autoload.php that early would pull Laravel's helper files in over the + * installer's own. + * + * @since 3.5.9 + * @return bool whether the package is available + */ +function loadExecWithFallback(): bool +{ + if (class_exists('ExecWithFallback\ExecWithFallback')) { + return true; + } + + $directory = EVO_CORE_PATH . 'vendor/rosell-dk/exec-with-fallback/src/'; + if (!is_file($directory . 'ExecWithFallback.php')) { + return false; + } + + spl_autoload_register(static function (string $class) use ($directory): void { + if (strncmp($class, 'ExecWithFallback\\', 17) === 0) { + $file = $directory . substr($class, 17) . '.php'; + if (is_file($file)) { + require $file; + } + } + }); + + return class_exists('ExecWithFallback\ExecWithFallback'); +} + /** * Print a neutral info message (default color). *