Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/modules/store/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
45 changes: 37 additions & 8 deletions core/src/Console/Packages/InstallPackageRequireCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ class InstallPackageRequireCommand extends Command
*/
protected $composer = EVO_CORE_PATH . 'custom/composer.json';

/**
* Packages touched by updateArray(); scopes the composer update to them.
* @var array<int,string>
*/
protected $affectedPackages = [];

/**
* @var array
*/
Expand Down Expand Up @@ -76,6 +82,7 @@ public function checkFile()
public function updateArray()
{
$this->composerArray['require'][$this->argument('key')] = $this->argument('value');
$this->affectedPackages[] = (string) $this->argument('key');
}

public function putComposer()
Expand All @@ -95,14 +102,7 @@ public function putComposer()
public function runComposer()
{
putenv('COMPOSER_HOME=' . EVO_CORE_PATH . 'composer');
$arguments = ['command' => 'update'];
if ($this->hasCommandOption('no-dev') && $this->option('no-dev')) {
$arguments['--no-dev'] = true;
}
if ($this->hasCommandOption('optimize-autoloader') && $this->option('optimize-autoloader')) {
$arguments['--optimize-autoloader'] = true;
}
$input = new ArrayInput($arguments);
$input = new ArrayInput($this->buildComposerArguments());
$application = new Application();
$application->setAutoExit(false);
$originalCwd = function_exists('getcwd') ? getcwd() : false;
Expand All @@ -121,6 +121,35 @@ public function runComposer()

}

/**
* Build the composer update arguments.
*
* The update is limited to the changed packages and their dependencies,
* like `composer require`/`remove` do. A bare `update` would also bump every
* core dependency, including composer/composer running this very process.
*
* @return array<string,mixed>
*/
public function buildComposerArguments(): array
{
$arguments = ['command' => 'update'];

$packages = array_values(array_unique(array_filter(array_map('trim', $this->affectedPackages))));
if ($packages !== []) {
$arguments['packages'] = $packages;
$arguments['--with-dependencies'] = true;
}

if ($this->hasCommandOption('no-dev') && $this->option('no-dev')) {
$arguments['--no-dev'] = true;
}
if ($this->hasCommandOption('optimize-autoloader') && $this->option('optimize-autoloader')) {
$arguments['--optimize-autoloader'] = true;
}

return $arguments;
}

protected function hasCommandOption(string $name): bool
{
return $this->getDefinition()->hasOption($name);
Expand Down
1 change: 1 addition & 0 deletions core/src/Console/Packages/RemovePackageRequireCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function updateArray()
foreach (array_keys($this->composerArray['require']) as $requireKey) {
if ($this->matchesRequirementKey((string) $requireKey, $target)) {
unset($this->composerArray['require'][$requireKey]);
$this->affectedPackages[] = (string) $requireKey;
$this->info('Removed package requirement: ' . $requireKey);
return true;
}
Expand Down
28 changes: 25 additions & 3 deletions core/src/Services/SystemTasks/ConsoleInstallFlowService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class ConsoleInstallFlowService implements SystemTaskHandlerInterface
{
use ReportsProcessFailure;

protected CatalogService $catalogService;

public function __construct(?CatalogService $catalogService = null)
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}
Expand All @@ -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];
Expand Down Expand Up @@ -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) {
Expand All @@ -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 '';
}
Expand Down
10 changes: 7 additions & 3 deletions core/src/Services/SystemTasks/ConsoleUninstallFlowService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

class ConsoleUninstallFlowService implements SystemTaskHandlerInterface
{
use ReportsProcessFailure;

protected string $corePath;
protected string $providersDir;
protected string $aliasesDir;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions core/src/Services/SystemTasks/ReportsProcessFailure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php namespace EvolutionCMS\Services\SystemTasks;

/**
* Puts the tail of a failed child process into the task log.
*
* Composer and artisan print their banner first and the error last, so the
* head of the output names the step and the tail names the failure. The
* manager renders log messages only, so the tail goes in there whole.
*
* @since 3.5.8
*/
trait ReportsProcessFailure
{
protected function reportProcessFailure(?callable $report, $step, $progress, $label, $exitCode, $output, array $context = [])
{
$lines = preg_split('/\r\n|\r|\n/', trim((string) $output));
$tail = trim(implode("\n", array_slice($lines, -40)));
if ($tail === '') {
$tail = $label . ' produced no output.';
}

$this->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);
}
}
10 changes: 7 additions & 3 deletions core/src/Services/SystemTasks/SiteUpdateFlowService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class SiteUpdateFlowService implements SystemTaskHandlerInterface
{
use ReportsProcessFailure;

protected string $corePath;

public function __construct(?string $corePath = null)
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 = [])
Expand Down
58 changes: 58 additions & 0 deletions core/tests/Unit/Console/PackageRequireCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,61 @@ function setPackageRequireComposerPath(InstallPackageRequireCommand $command, st
->toContain("hasCommandOption('no-dev')")
->toContain("hasCommandOption('optimize-autoloader')");
});

test('package install require scopes composer update to the installed package', function () {
$composer = tempnam(sys_get_temp_dir(), 'evo-composer-');
file_put_contents($composer, json_encode(['name' => 'evolutioncms/custom', 'require' => []]));

$command = new InstallPackageRequireCommand();
setPackageRequireComposerPath($command, $composer);

$tester = new CommandTester($command);
$tester->execute([
'key' => 'evolution-cms/emcp',
'value' => '*',
'composer_run' => '0',
'--no-dev' => true,
]);

expect($command->buildComposerArguments())->toBe([
'command' => 'update',
'packages' => ['evolution-cms/emcp'],
'--with-dependencies' => true,
'--no-dev' => true,
]);

@unlink($composer);
});

test('package remove require scopes composer update to the removed package', function () {
$composer = tempnam(sys_get_temp_dir(), 'evo-composer-');
file_put_contents($composer, json_encode([
'name' => 'evolutioncms/custom',
'require' => ['Seiger/sCommerce' => '*', 'seiger/stask' => '*'],
]));

$command = new RemovePackageRequireCommand();
setPackageRequireComposerPath($command, $composer);

$tester = new CommandTester($command);
$tester->execute(['key' => 'sCommerce', 'composer_run' => '0']);

expect($command->buildComposerArguments())->toBe([
'command' => 'update',
'packages' => ['Seiger/sCommerce'],
'--with-dependencies' => true,
]);

@unlink($composer);
});

test('composer update falls back to a full update when no package was changed', function () {
$command = new InstallPackageRequireCommand();
$command->setLaravel(new PackageRequireTestContainer());

$input = new ReflectionProperty($command, 'input');
$input->setAccessible(true);
$input->setValue($command, new \Symfony\Component\Console\Input\ArrayInput(['key' => 'vendor/pkg', 'value' => '*'], $command->getDefinition()));

expect($command->buildComposerArguments())->toBe(['command' => 'update']);
});
17 changes: 17 additions & 0 deletions core/tests/Unit/SystemTasks/ConsoleInstallFlowServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Loading