mirror of
https://github.com/getgrav/grav.git
synced 2026-05-08 23:26:55 +02:00
Add compatibility: blueprint support and preflight handling for 1.7→1.8 upgrades
Enables the 1.7 SelfupgradeCommand to handle preflight reports from the 1.8 package's Install.php. When upgrading from 1.7 to 1.8, the command now: - Calls generatePreflightReport() on the new package's installer - Shows incompatible plugins with disable/continue/abort choices - Handles pending updates, PSR/log and Monolog conflicts interactively - Disables plugins via direct YAML config write (no RecoveryManager on 1.7) Also adds: - GPM Local/Package: computed compatibility property from blueprints - CLI badges: IndexCommand, InfoCommand, UpdateCommand show 1.7/1.8 badges
This commit is contained in:
@@ -39,6 +39,7 @@ class Package extends BasePackage
|
||||
$this->data->set('description_html', $html_description);
|
||||
$this->data->set('description_plain', strip_tags($html_description));
|
||||
$this->data->set('symlink', is_link(USER_DIR . $package_type . DS . $this->__get('slug')));
|
||||
$this->data->set('compatibility', $this->resolveCompatibility($data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,4 +49,52 @@ class Package extends BasePackage
|
||||
{
|
||||
return (bool)$this->settings['enabled'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the compatibility metadata for this package.
|
||||
*
|
||||
* @param Data $data Blueprint data
|
||||
* @return array{grav: string[], api: string[]}
|
||||
*/
|
||||
protected function resolveCompatibility(Data $data): array
|
||||
{
|
||||
$raw = $data->get('compatibility');
|
||||
|
||||
if (is_array($raw) && isset($raw['grav']) && is_array($raw['grav'])) {
|
||||
return [
|
||||
'grav' => array_map('strval', $raw['grav']),
|
||||
'api' => isset($raw['api']) && is_array($raw['api']) ? array_map('strval', $raw['api']) : [],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->inferCompatibility($data->get('dependencies') ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer Grav compatibility from the dependencies array.
|
||||
*
|
||||
* @param array $dependencies
|
||||
* @return array{grav: string[], api: string[]}
|
||||
*/
|
||||
protected function inferCompatibility(array $dependencies): array
|
||||
{
|
||||
foreach ($dependencies as $dep) {
|
||||
if (!is_array($dep) || ($dep['name'] ?? '') !== 'grav') {
|
||||
continue;
|
||||
}
|
||||
$version = $dep['version'] ?? '';
|
||||
|
||||
if (!preg_match('/(\d+\.\d+(?:\.\d+)?)/', $version, $m)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (version_compare($m[1], '1.8', '>=')) {
|
||||
return ['grav' => ['1.8'], 'api' => []];
|
||||
}
|
||||
|
||||
return ['grav' => ['1.7'], 'api' => []];
|
||||
}
|
||||
|
||||
return ['grav' => ['1.7'], 'api' => []];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class IndexCommand extends GpmCommand
|
||||
if (!empty($packages)) {
|
||||
$io->section('Packages table');
|
||||
$table = new Table($io);
|
||||
$table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Installed', 'Enabled']);
|
||||
$table->setHeaders(['Count', 'Name', 'Slug', 'Version', 'Compat', 'Installed', 'Enabled']);
|
||||
|
||||
$index = 0;
|
||||
foreach ($packages as $slug => $package) {
|
||||
@@ -150,6 +150,7 @@ class IndexCommand extends GpmCommand
|
||||
'Name' => '<cyan>' . Utils::truncate($package->name, 20, false, ' ', '...') . '</cyan> ',
|
||||
'Slug' => $slug,
|
||||
'Version'=> $this->version($package),
|
||||
'Compat' => $this->compatBadges($package),
|
||||
'Installed' => $this->installed($package),
|
||||
'Enabled' => $this->enabled($package),
|
||||
];
|
||||
@@ -231,6 +232,38 @@ class IndexCommand extends GpmCommand
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Package $package
|
||||
* @return string
|
||||
*/
|
||||
private function compatBadges(Package $package): string
|
||||
{
|
||||
$type = ucfirst(preg_replace('/s$/', '', $package->package_type));
|
||||
$method = 'is' . $type . 'Installed';
|
||||
$installed = $this->gpm->{$method}($package->slug);
|
||||
|
||||
if ($installed) {
|
||||
$local = $this->gpm->{'getInstalled' . $type}($package->slug);
|
||||
$compat = $local->compatibility ?? null;
|
||||
} else {
|
||||
$compat = $package->compatibility ?? null;
|
||||
}
|
||||
|
||||
if (!is_array($compat) || empty($compat['grav'])) {
|
||||
return '<blue>1.7</blue>';
|
||||
}
|
||||
|
||||
$badges = [];
|
||||
if (in_array('1.7', $compat['grav'], true)) {
|
||||
$badges[] = '<blue>1.7</blue>';
|
||||
}
|
||||
if (in_array('1.8', $compat['grav'], true)) {
|
||||
$badges[] = '<green>1.8</green>';
|
||||
}
|
||||
|
||||
return implode(' ', $badges);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Packages $data
|
||||
* @return Packages
|
||||
|
||||
@@ -131,9 +131,36 @@ class InfoCommand extends GpmCommand
|
||||
}
|
||||
}
|
||||
|
||||
// Display compatibility badges
|
||||
$type = rtrim($foundPackage->package_type, 's');
|
||||
$updatable = $this->gpm->{'is' . $type . 'Updatable'}($foundPackage->slug);
|
||||
$installed = $this->gpm->{'is' . $type . 'Installed'}($foundPackage->slug);
|
||||
if ($installed) {
|
||||
$local = $this->gpm->{'getInstalled' . $type}($foundPackage->slug);
|
||||
$compat = $local->compatibility ?? null;
|
||||
} else {
|
||||
$compat = $foundPackage->compatibility ?? null;
|
||||
}
|
||||
|
||||
$compatStr = '';
|
||||
if (is_array($compat) && !empty($compat['grav'])) {
|
||||
$badges = [];
|
||||
if (in_array('1.7', $compat['grav'], true)) {
|
||||
$badges[] = '<blue>1.7</blue>';
|
||||
}
|
||||
if (in_array('1.8', $compat['grav'], true)) {
|
||||
$badges[] = '<green>1.8</green>';
|
||||
}
|
||||
$compatStr = implode(' ', $badges);
|
||||
} else {
|
||||
$compatStr = '<blue>1.7</blue>';
|
||||
}
|
||||
$io->writeln('<green>' . str_pad('Compatibility', 12) . ':</green> ' . $compatStr);
|
||||
|
||||
if (is_array($compat) && !empty($compat['api'])) {
|
||||
$io->writeln('<green>' . str_pad('API', 12) . ':</green> ' . implode(', ', $compat['api']));
|
||||
}
|
||||
|
||||
$updatable = $this->gpm->{'is' . $type . 'Updatable'}($foundPackage->slug);
|
||||
|
||||
// display current version if installed and different
|
||||
if ($installed && $updatable) {
|
||||
|
||||
@@ -19,8 +19,11 @@ use Grav\Console\GpmCommand;
|
||||
use Grav\Installer\Install;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Grav\Common\Yaml;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Throwable;
|
||||
use ZipArchive;
|
||||
use function is_callable;
|
||||
use function strlen;
|
||||
@@ -333,6 +336,14 @@ class SelfupgradeCommand extends GpmCommand
|
||||
|
||||
$script = $folder . '/system/install.php';
|
||||
if ((file_exists($script) && $install = include $script) && is_callable($install)) {
|
||||
// Run preflight from the NEW package's installer if available
|
||||
if (is_object($install) && method_exists($install, 'generatePreflightReport')) {
|
||||
$report = $install->generatePreflightReport();
|
||||
if (!$this->handlePreflightReport($report)) {
|
||||
Installer::setError('Upgrade aborted due to preflight requirements.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
$install($zip);
|
||||
} else {
|
||||
throw new RuntimeException('Uploaded archive file is not a valid Grav update package');
|
||||
@@ -341,4 +352,254 @@ class SelfupgradeCommand extends GpmCommand
|
||||
Installer::setError($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a preflight report from the target package's installer.
|
||||
*
|
||||
* @param array $preflight
|
||||
* @return bool True to proceed, false to abort
|
||||
*/
|
||||
protected function handlePreflightReport(array $preflight): bool
|
||||
{
|
||||
$io = $this->getIO();
|
||||
$pending = $preflight['plugins_pending'] ?? [];
|
||||
$blocking = $preflight['blocking'] ?? [];
|
||||
$conflicts = $preflight['psr_log_conflicts'] ?? [];
|
||||
$monologConflicts = $preflight['monolog_conflicts'] ?? [];
|
||||
$warnings = $preflight['warnings'] ?? [];
|
||||
$incompatible = $preflight['incompatible_packages'] ?? [];
|
||||
$incompatibleBlocking = $incompatible['blocking'] ?? [];
|
||||
$incompatibleTarget = $incompatible['target'] ?? '';
|
||||
$isMajorMinorUpgrade = $preflight['is_major_minor_upgrade'] ?? false;
|
||||
|
||||
if ($warnings) {
|
||||
$io->newLine();
|
||||
$io->writeln('<magenta>Preflight warnings detected:</magenta>');
|
||||
foreach ($warnings as $warning) {
|
||||
$io->writeln(' • ' . $warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out the incompatible-packages blocker (handled separately below)
|
||||
$filteredBlocking = array_filter($blocking, static function ($reason) {
|
||||
return !stripos($reason, 'not been marked as compatible');
|
||||
});
|
||||
|
||||
if ($filteredBlocking && empty($pending)) {
|
||||
$io->newLine();
|
||||
$io->writeln('<red>Upgrade blocked:</red>');
|
||||
foreach ($filteredBlocking as $reason) {
|
||||
$io->writeln(' - ' . $reason);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($pending) && empty($conflicts) && empty($monologConflicts) && empty($incompatibleBlocking)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($pending && $isMajorMinorUpgrade) {
|
||||
$local = $this->upgrader ? $this->upgrader->getLocalVersion() : 'unknown';
|
||||
$remote = $this->upgrader ? $this->upgrader->getRemoteVersion() : 'unknown';
|
||||
|
||||
$io->newLine();
|
||||
$io->writeln('<yellow>The following packages need updating before Grav upgrade:</yellow>');
|
||||
foreach ($pending as $slug => $info) {
|
||||
$type = $info['type'] ?? 'plugin';
|
||||
$current = $info['current'] ?? 'unknown';
|
||||
$available = $info['available'] ?? 'unknown';
|
||||
$io->writeln(sprintf(' - %s (%s) %s → %s', $slug, $type, $current, $available));
|
||||
}
|
||||
|
||||
$io->writeln(' › For major version upgrades (v' . $local . ' → v' . $remote . '), plugins must be updated first.');
|
||||
$io->writeln(' Please run `bin/gpm update` to update these packages, then retry self-upgrade.');
|
||||
|
||||
$proceed = false;
|
||||
if (!$this->all_yes) {
|
||||
$question = new ConfirmationQuestion('Proceed anyway? [y|N] ', false);
|
||||
$proceed = $io->askQuestion($question);
|
||||
}
|
||||
|
||||
if (!$proceed) {
|
||||
$io->writeln('Aborting self-upgrade. Run `bin/gpm update` first.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (method_exists(Install::class, 'allowPendingPackageOverride')) {
|
||||
Install::allowPendingPackageOverride(true);
|
||||
}
|
||||
$io->writeln(' › Proceeding despite pending plugin/theme updates.');
|
||||
}
|
||||
|
||||
// Handle incompatible packages
|
||||
if ($incompatibleBlocking) {
|
||||
$io->newLine();
|
||||
$io->writeln('<yellow>The following enabled plugins/themes are not marked as compatible with Grav ' . $incompatibleTarget . ':</yellow>');
|
||||
foreach ($incompatibleBlocking as $slug => $info) {
|
||||
$type = $info['type'] ?? 'plugin';
|
||||
$ver = $info['version'] ?? 'unknown';
|
||||
$gravCompat = implode(', ', $info['compatibility']['grav'] ?? ['?']);
|
||||
$io->writeln(sprintf(' - %s (%s v%s) — compatible with: %s', $slug, $type, $ver, $gravCompat));
|
||||
}
|
||||
$io->writeln(' › Plugins/themes must be marked as compatible with Grav ' . $incompatibleTarget . ' before upgrading.');
|
||||
$io->writeln(' Either update the plugins, or disable them to proceed.');
|
||||
|
||||
$choice = $this->all_yes ? 'abort' : $io->choice(
|
||||
'How would you like to proceed?',
|
||||
['disable', 'continue', 'abort'],
|
||||
'abort'
|
||||
);
|
||||
|
||||
if ($choice === 'abort') {
|
||||
$io->writeln('Aborting self-upgrade. Update or disable incompatible plugins first.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($choice === 'disable') {
|
||||
foreach (array_keys($incompatibleBlocking) as $slug) {
|
||||
$this->disablePluginConfig($slug);
|
||||
$io->writeln(sprintf(' - Disabled %s.', $slug));
|
||||
}
|
||||
$io->writeln('Continuing with incompatible plugins disabled.');
|
||||
} else {
|
||||
if (method_exists(Install::class, 'allowIncompatibleOverride')) {
|
||||
Install::allowIncompatibleOverride(true);
|
||||
}
|
||||
$io->writeln(' › Proceeding despite incompatible plugins/themes.');
|
||||
}
|
||||
}
|
||||
|
||||
// Show incompatible warnings (disabled packages) — informational only
|
||||
$incompatibleWarnings = $incompatible['warnings'] ?? [];
|
||||
if ($incompatibleWarnings) {
|
||||
$io->newLine();
|
||||
$io->writeln('<cyan>Disabled plugins/themes not yet compatible with Grav ' . $incompatibleTarget . ' (will not block upgrade):</cyan>');
|
||||
foreach ($incompatibleWarnings as $slug => $info) {
|
||||
$type = $info['type'] ?? 'plugin';
|
||||
$ver = $info['version'] ?? 'unknown';
|
||||
$io->writeln(sprintf(' - %s (%s v%s)', $slug, $type, $ver));
|
||||
}
|
||||
}
|
||||
|
||||
$handled = $this->handleConflicts(
|
||||
$conflicts,
|
||||
static function (SymfonyStyle $io, array $conflicts): void {
|
||||
$io->newLine();
|
||||
$io->writeln('<yellow>Potential psr/log incompatibilities:</yellow>');
|
||||
foreach ($conflicts as $slug => $info) {
|
||||
$requires = $info['requires'] ?? '*';
|
||||
$io->writeln(sprintf(' - %s (requires psr/log %s)', $slug, $requires));
|
||||
}
|
||||
},
|
||||
'Update the plugin or add "replace": {"psr/log": "*"} to its composer.json.',
|
||||
'Aborting self-upgrade. Adjust composer requirements or update affected plugins.',
|
||||
'Proceeding with potential psr/log incompatibilities still active.'
|
||||
);
|
||||
|
||||
if (!$handled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$handledMonolog = $this->handleConflicts(
|
||||
$monologConflicts,
|
||||
static function (SymfonyStyle $io, array $conflicts): void {
|
||||
$io->newLine();
|
||||
$io->writeln('<yellow>Potential Monolog logger API incompatibilities:</yellow>');
|
||||
foreach ($conflicts as $slug => $entries) {
|
||||
foreach ($entries as $entry) {
|
||||
$file = $entry['file'] ?? 'unknown file';
|
||||
$method = $entry['method'] ?? 'add*';
|
||||
$io->writeln(sprintf(' - %s (%s in %s)', $slug, $method, $file));
|
||||
}
|
||||
}
|
||||
},
|
||||
'Update the plugin to use PSR-3 style logger methods before upgrading.',
|
||||
'Aborting self-upgrade. Update plugins to remove deprecated Monolog add* calls.',
|
||||
'Proceeding with potential Monolog API incompatibilities still active.'
|
||||
);
|
||||
|
||||
if (!$handledMonolog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a set of conflicts with user choice (disable/continue/abort).
|
||||
*
|
||||
* @param array $conflicts
|
||||
* @param callable $printer
|
||||
* @param string $advice
|
||||
* @param string $abortMessage
|
||||
* @param string $continueMessage
|
||||
* @return bool
|
||||
*/
|
||||
private function handleConflicts(array $conflicts, callable $printer, string $advice, string $abortMessage, string $continueMessage): bool
|
||||
{
|
||||
if (empty($conflicts)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$io = $this->getIO();
|
||||
$printer($io, $conflicts);
|
||||
$io->writeln(' › ' . $advice);
|
||||
|
||||
$choice = $this->all_yes ? 'abort' : $io->choice(
|
||||
'How would you like to proceed?',
|
||||
['disable', 'continue', 'abort'],
|
||||
'abort'
|
||||
);
|
||||
|
||||
if ($choice === 'abort') {
|
||||
$io->writeln($abortMessage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($choice === 'disable') {
|
||||
foreach (array_keys($conflicts) as $slug) {
|
||||
$this->disablePluginConfig($slug);
|
||||
$io->writeln(sprintf(' - Disabled plugin %s.', $slug));
|
||||
}
|
||||
$io->writeln('Continuing with conflicted plugins disabled.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$io->writeln($continueMessage);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a plugin by writing enabled: false to its config file.
|
||||
* Used on 1.7 where RecoveryManager may not be available.
|
||||
*
|
||||
* @param string $slug
|
||||
*/
|
||||
private function disablePluginConfig(string $slug): void
|
||||
{
|
||||
$configPath = GRAV_ROOT . '/user/config/plugins/' . $slug . '.yaml';
|
||||
|
||||
try {
|
||||
if (is_file($configPath)) {
|
||||
$contents = @file_get_contents($configPath);
|
||||
$data = $contents !== false ? Yaml::parse($contents) : [];
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$data['enabled'] = false;
|
||||
file_put_contents($configPath, Yaml::dump($data));
|
||||
} catch (Throwable $e) {
|
||||
// best effort — if config write fails, upgrade will be blocked by Install.php
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +187,29 @@ class UpdateCommand extends GpmCommand
|
||||
$package->available = $package->version;
|
||||
}
|
||||
|
||||
// Build compatibility badges
|
||||
$compat = $package->compatibility ?? null;
|
||||
$compatStr = '';
|
||||
if (is_array($compat) && !empty($compat['grav'])) {
|
||||
$badges = [];
|
||||
if (in_array('1.7', $compat['grav'], true)) {
|
||||
$badges[] = '<blue>1.7</blue>';
|
||||
}
|
||||
if (in_array('1.8', $compat['grav'], true)) {
|
||||
$badges[] = '<green>1.8</green>';
|
||||
}
|
||||
$compatStr = ' ' . implode(' ', $badges);
|
||||
}
|
||||
|
||||
$io->writeln(
|
||||
// index
|
||||
str_pad((string)$index++, 2, '0', STR_PAD_LEFT) . '. ' .
|
||||
// name
|
||||
'<cyan>' . str_pad($package->name, 15) . '</cyan> ' .
|
||||
// version
|
||||
"[v<magenta>{$package->version}</magenta> -> v<green>{$package->available}</green>]"
|
||||
"[v<magenta>{$package->version}</magenta> -> v<green>{$package->available}</green>]" .
|
||||
// compat badges
|
||||
$compatStr
|
||||
);
|
||||
$slugs[] = $slug;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user