diff --git a/assets/modules/store/css/style.css b/assets/modules/store/css/style.css index 4b17223629..67784e6cf1 100644 --- a/assets/modules/store/css/style.css +++ b/assets/modules/store/css/style.css @@ -1064,6 +1064,7 @@ input#store_search:focus { display: block; min-width: 0; overflow-wrap: anywhere; + white-space: pre-wrap; color: #d8dee6; font-size: 13px; line-height: 1.35; diff --git a/core/src/Console/Packages/InstallPackageRequireCommand.php b/core/src/Console/Packages/InstallPackageRequireCommand.php index de2a9c65ef..a3ef3345a1 100644 --- a/core/src/Console/Packages/InstallPackageRequireCommand.php +++ b/core/src/Console/Packages/InstallPackageRequireCommand.php @@ -95,7 +95,15 @@ public function putComposer() public function runComposer() { putenv('COMPOSER_HOME=' . EVO_CORE_PATH . 'composer'); - $arguments = ['command' => 'update']; + // A partial update: only the requested package and what it pulls in may + // move. A bare `update` re-resolves the whole lock for the running PHP, + // which on a container whose vendor tree was built on 8.3 and runs on + // 8.4 rewrote 62 core packages before the extra was even touched. + $arguments = [ + 'command' => 'update', + 'packages' => [(string) $this->argument('key')], + '--with-all-dependencies' => true, + ]; if ($this->hasCommandOption('no-dev') && $this->option('no-dev')) { $arguments['--no-dev'] = true; } diff --git a/core/src/Services/SystemTasks/ConsoleInstallFlowService.php b/core/src/Services/SystemTasks/ConsoleInstallFlowService.php index 1baceed04d..c10c3ac103 100644 --- a/core/src/Services/SystemTasks/ConsoleInstallFlowService.php +++ b/core/src/Services/SystemTasks/ConsoleInstallFlowService.php @@ -7,6 +7,8 @@ class ConsoleInstallFlowService implements SystemTaskHandlerInterface { + use ReportsProcessFailure; + protected CatalogService $catalogService; public function __construct(?CatalogService $catalogService = null) @@ -33,6 +35,7 @@ public function execute(SystemCliTask $task, ?callable $report = null) 'key' => $composerName, 'value' => $composerVersion, 'composer_run' => 1, + '--no-dev' => !$this->vendorHasDevPackages(), ], 'install_require', 30, @@ -107,7 +110,9 @@ protected function runArtisanCommand($command, array $arguments, $step, $progres } if ((int) $exitCode !== 0) { - $reason = $this->summarizeOutput($output); + $this->reportProcessFailure($report, $step, $progress, $command, $exitCode, $output); + + $reason = $this->summarizeOutput($output, true); if ($reason !== '') { throw new \RuntimeException($command . ' failed with exit code ' . (int) $exitCode . '. ' . $reason); } @@ -116,6 +121,23 @@ protected function runArtisanCommand($command, array $arguments, $step, $progres } } + /** + * Does vendor/ carry require-dev packages? Mirrors that state into the + * composer run so an install neither strips a dev checkout nor pulls the + * dev tree into a --no-dev production build. + */ + protected function vendorHasDevPackages(): bool + { + $installed = EVO_CORE_PATH . 'vendor/composer/installed.json'; + if (!file_exists($installed)) { + return true; + } + + $data = json_decode((string) file_get_contents($installed), true); + + return !is_array($data) || !array_key_exists('dev', $data) || (bool) $data['dev']; + } + protected function buildArtisanProcessArguments($command, array $arguments) { $parts = [PHP_BINARY, EVO_CORE_PATH . 'artisan', $command]; @@ -276,7 +298,7 @@ protected function isComposerDependencyName(string $name): bool return true; } - protected function summarizeOutput($output) + protected function summarizeOutput($output, bool $fromEnd = false) { $lines = preg_split('/\r\n|\r|\n/', trim((string) $output)); $lines = array_values(array_filter(array_map(function ($line) { @@ -303,7 +325,7 @@ protected function summarizeOutput($output) return true; })); - $output = implode(' ', array_slice($lines, 0, 3)); + $output = implode(' ', $this->pickSummaryLines($lines, $fromEnd, $fromEnd ? 5 : 3)); if ($output === '') { return ''; } diff --git a/core/src/Services/SystemTasks/ConsoleUninstallFlowService.php b/core/src/Services/SystemTasks/ConsoleUninstallFlowService.php index 77afcd1b34..08fb9b7334 100644 --- a/core/src/Services/SystemTasks/ConsoleUninstallFlowService.php +++ b/core/src/Services/SystemTasks/ConsoleUninstallFlowService.php @@ -6,6 +6,8 @@ class ConsoleUninstallFlowService implements SystemTaskHandlerInterface { + use ReportsProcessFailure; + protected string $corePath; protected string $providersDir; protected string $aliasesDir; @@ -100,7 +102,9 @@ protected function runArtisanCommand($command, array $arguments, $step, $progres } if ((int) $exitCode !== 0) { - $reason = $this->summarizeOutput($output); + $this->reportProcessFailure($report, $step, $progress, $command, $exitCode, $output); + + $reason = $this->summarizeOutput($output, true); if ($reason !== '') { throw new \RuntimeException($command . ' failed with exit code ' . (int) $exitCode . '. ' . $reason); } @@ -134,7 +138,7 @@ protected function buildArtisanProcessArguments($command, array $arguments) return $parts; } - protected function summarizeOutput($output) + protected function summarizeOutput($output, bool $fromEnd = false) { $lines = preg_split('/\r\n|\r|\n/', trim((string) $output)); $lines = array_values(array_filter(array_map(function ($line) { @@ -165,7 +169,7 @@ protected function summarizeOutput($output) return ''; } - return implode(' ', array_slice($lines, 0, 3)); + return implode(' ', $this->pickSummaryLines($lines, $fromEnd, $fromEnd ? 5 : 3)); } protected function purgeInvalidDiscoveryArtifacts(?callable $report = null) diff --git a/core/src/Services/SystemTasks/ReportsProcessFailure.php b/core/src/Services/SystemTasks/ReportsProcessFailure.php new file mode 100644 index 0000000000..009a14874e --- /dev/null +++ b/core/src/Services/SystemTasks/ReportsProcessFailure.php @@ -0,0 +1,36 @@ +report($report, $step, $progress, $tail, 'error', $context + [ + 'command' => $label, + 'exit_code' => (int) $exitCode, + 'output' => mb_substr((string) $output, -16000), + ]); + } + + /** + * The one-line reason for the exception: last lines on failure, first lines otherwise. + */ + protected function pickSummaryLines(array $lines, bool $fromEnd, int $count) + { + return $fromEnd ? array_slice($lines, -$count) : array_slice($lines, 0, $count); + } +} diff --git a/core/src/Services/SystemTasks/SiteUpdateFlowService.php b/core/src/Services/SystemTasks/SiteUpdateFlowService.php index a81c87c53e..bbc5904def 100644 --- a/core/src/Services/SystemTasks/SiteUpdateFlowService.php +++ b/core/src/Services/SystemTasks/SiteUpdateFlowService.php @@ -7,6 +7,8 @@ class SiteUpdateFlowService implements SystemTaskHandlerInterface { + use ReportsProcessFailure; + protected string $corePath; public function __construct(?string $corePath = null) @@ -79,7 +81,9 @@ public function execute(SystemCliTask $task, ?callable $report = null) } if ((int) $exitCode !== 0) { - $reason = $this->summarizeOutput($output); + $this->reportProcessFailure($report, 'site_update', 80, 'make:site', $exitCode, $output, $reportContext); + + $reason = $this->summarizeOutput($output, true); if ($reason !== '') { throw new \RuntimeException('Site update failed with exit code ' . (int) $exitCode . '. ' . $reason); } @@ -158,7 +162,7 @@ protected function buildArtisanProcessArguments($command, array $arguments) return $parts; } - protected function summarizeOutput($output) + protected function summarizeOutput($output, bool $fromEnd = false) { $lines = preg_split('/\r\n|\r|\n/', trim((string) $output)); $lines = array_values(array_filter(array_map(function ($line) { @@ -175,7 +179,7 @@ protected function summarizeOutput($output) return ''; } - return implode(' ', array_slice($lines, 0, 5)); + return implode(' ', $this->pickSummaryLines($lines, $fromEnd, 5)); } protected function report(?callable $report, $step, $progress, $message, $level = 'info', array $context = []) diff --git a/core/tests/Unit/SystemTasks/ConsoleInstallFlowServiceTest.php b/core/tests/Unit/SystemTasks/ConsoleInstallFlowServiceTest.php index bfb0bcd83a..9792082452 100644 --- a/core/tests/Unit/SystemTasks/ConsoleInstallFlowServiceTest.php +++ b/core/tests/Unit/SystemTasks/ConsoleInstallFlowServiceTest.php @@ -68,3 +68,20 @@ function invokeConsoleInstallFlowMethod(ConsoleInstallFlowService $service, stri 'Vendor\\Package\\SecondaryServiceProvider', ]); }); + +test('summarizeOutput reads the failure from the tail when asked', function () { + $service = new ConsoleInstallFlowService(); + + $output = implode("\n", [ + 'Evolution CMS 3.5.8', + 'Lock file operations: 1 install, 62 updates, 0 removals', + ' - Upgrading composer/ca-bundle (1.5.13 => 1.5.14)', + 'Your requirements could not be resolved to an installable set of packages.', + ' - elcreator/aimage 1.0.0 requires ext-imagick * -> it is missing from your system.', + ]); + + expect(invokeConsoleInstallFlowMethod($service, 'summarizeOutput', [$output])) + ->toStartWith('Lock file operations') + ->and(invokeConsoleInstallFlowMethod($service, 'summarizeOutput', [$output, true])) + ->toContain('ext-imagick'); +});