Skip to content

Commit 1c4ae74

Browse files
author
Dmytro Lukianenko
committed
[ADD] Add update prompt to installer and support for global PHP installer updates.
1 parent 0183e6d commit 1c4ae74

2 files changed

Lines changed: 158 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ evo install my-project --composer-clear-cache # Clear Composer cache before ins
128128
- `--admin-password`: Admin password
129129
- `--admin-directory`: Admin directory name (default: `manager`)
130130
- `--language`: Installation language (default: `en`)
131-
- `--branch`: Install from specific Git branch (e.g., `develop`, `nightly`, `main`) instead of latest release
131+
- `--branch`: Install from specific Git branch (e.g., `3.5.x`, `develop`, `nightly`, `main`) instead of latest release
132132
- `--git`: Initialize a Git repository and create initial commit
133133
- `--force`: Force install even if directory exists
134134
- `--log`: Always write installer log to `log.md`

bin/evo

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ final class EvoBootstrapper
5050
return 0;
5151
}
5252

53+
if ($cmd === 'install') {
54+
$this->maybePromptUpdateBeforeInstall();
55+
}
56+
5357
if (!is_file($this->binaryPath)) {
5458
$this->installBinary(false);
5559
}
@@ -267,6 +271,54 @@ final class EvoBootstrapper
267271
$this->installBinary(true);
268272
}
269273

274+
private function maybePromptUpdateBeforeInstall(): void
275+
{
276+
if (!$this->isInteractiveSession()) {
277+
return;
278+
}
279+
280+
if ($this->hasArg('--no-interaction') || $this->hasArg('-n')) {
281+
return;
282+
}
283+
if ($this->hasArg('--quiet') || $this->hasArg('-q') || $this->hasArg('--silent')) {
284+
return;
285+
}
286+
if ($this->isCiEnvironment()) {
287+
return;
288+
}
289+
290+
$current = $this->getInstalledVersion();
291+
if (!is_string($current) || trim($current) === '') {
292+
return;
293+
}
294+
295+
$latest = null;
296+
try {
297+
[$tag] = $this->getLatestReleaseInfo();
298+
$latest = $tag;
299+
} catch (RuntimeException $e) {
300+
$latest = $this->resolveLatestReleaseTagFallback();
301+
}
302+
303+
if (!is_string($latest) || trim($latest) === '') {
304+
return;
305+
}
306+
307+
if (!$this->isNewerVersion($current, $latest)) {
308+
return;
309+
}
310+
311+
$this->out("Update available: {$current} -> {$latest}");
312+
if (!$this->promptYesNo('Update now? [Y/n]', true)) {
313+
$this->out('Continuing without update.');
314+
return;
315+
}
316+
317+
$this->out('Updating installer (Composer + binary)...');
318+
$this->updatePhpInstallerPackageGlobal();
319+
$this->installBinary(true);
320+
}
321+
270322
private function updatePhpInstallerPackageBestEffort(): void
271323
{
272324
$composer = $this->findComposerBinary();
@@ -326,6 +378,46 @@ final class EvoBootstrapper
326378
$this->out('PHP installer update: OK');
327379
}
328380

381+
private function updatePhpInstallerPackageGlobal(): void
382+
{
383+
$composer = $this->findComposerBinary();
384+
if ($composer === null) {
385+
$this->out('PHP installer update: skipped (Composer not found).');
386+
return;
387+
}
388+
389+
$home = $this->resolveHomeDir();
390+
$env = [
391+
'COMPOSER_ALLOW_SUPERUSER' => '1',
392+
'HOME' => $home,
393+
];
394+
$composerHome = getenv('COMPOSER_HOME');
395+
if (is_string($composerHome) && trim($composerHome) !== '') {
396+
$env['COMPOSER_HOME'] = $composerHome;
397+
}
398+
399+
$this->out('Updating PHP installer package via Composer (global)...');
400+
$code = $this->runProcess(
401+
[
402+
$composer,
403+
'global',
404+
'update',
405+
'evolution-cms/installer',
406+
'--no-interaction',
407+
'--no-ansi',
408+
'--no-progress',
409+
],
410+
null,
411+
$env
412+
);
413+
if ($code !== 0) {
414+
$this->out("PHP installer update: failed (exit code {$code}).");
415+
return;
416+
}
417+
418+
$this->out('PHP installer update: OK');
419+
}
420+
329421
private function findComposerBinary(): ?string
330422
{
331423
$bins = ['composer', 'composer2'];
@@ -356,6 +448,71 @@ final class EvoBootstrapper
356448
return null;
357449
}
358450

451+
private function isInteractiveSession(): bool
452+
{
453+
if (function_exists('stream_isatty')) {
454+
return @stream_isatty(STDIN);
455+
}
456+
if (function_exists('posix_isatty')) {
457+
return @posix_isatty(STDIN);
458+
}
459+
return false;
460+
}
461+
462+
private function isCiEnvironment(): bool
463+
{
464+
$ci = getenv('CI');
465+
return is_string($ci) && trim($ci) !== '';
466+
}
467+
468+
private function hasArg(string $needle): bool
469+
{
470+
foreach ($this->args as $arg) {
471+
if ($arg === $needle) {
472+
return true;
473+
}
474+
if (str_starts_with($needle, '--') && str_starts_with($arg, $needle . '=')) {
475+
return true;
476+
}
477+
}
478+
return false;
479+
}
480+
481+
private function normalizeVersion(string $version): string
482+
{
483+
$v = trim($version);
484+
$v = ltrim($v, "vV");
485+
return $v;
486+
}
487+
488+
private function isNewerVersion(string $current, string $latest): bool
489+
{
490+
$currentNorm = $this->normalizeVersion($current);
491+
$latestNorm = $this->normalizeVersion($latest);
492+
if ($currentNorm === '' || $latestNorm === '') {
493+
return $latest !== $current;
494+
}
495+
if (!preg_match('/^[0-9]+(\\.[0-9]+)*/', $currentNorm) || !preg_match('/^[0-9]+(\\.[0-9]+)*/', $latestNorm)) {
496+
return $latest !== $current;
497+
}
498+
return version_compare($latestNorm, $currentNorm, '>');
499+
}
500+
501+
private function promptYesNo(string $question, bool $defaultYes): bool
502+
{
503+
$suffix = $defaultYes ? ' ' : '';
504+
$this->out($question . $suffix);
505+
$line = fgets(STDIN);
506+
if ($line === false) {
507+
return $defaultYes;
508+
}
509+
$answer = strtolower(trim($line));
510+
if ($answer === '') {
511+
return $defaultYes;
512+
}
513+
return in_array($answer, ['y', 'yes'], true);
514+
}
515+
359516
private function findComposerWorkDir(): ?string
360517
{
361518
$candidate = $this->detectComposerRootFromVendorLayout();

0 commit comments

Comments
 (0)