From 91a069f5b8ea34673460d51598606a3b11c32eac Mon Sep 17 00:00:00 2001 From: yaozm Date: Thu, 22 Feb 2024 13:09:56 +0800 Subject: [PATCH] chore(composer): Update composer-fixer to composer-updater - Renamed `composer-fixer` to `composer-updater` in `.php-cs-fixer.php` - Updated the file references accordingly - Updated the composer packages using the new `composer-updater` script --- .php-cs-fixer.php | 2 +- composer-fixer | 193 ------------------------------- composer-updater | 282 ++++++++++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- rector.php | 1 + 5 files changed, 285 insertions(+), 195 deletions(-) delete mode 100644 composer-fixer create mode 100755 composer-updater diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index e2c9ef6..fc69e48 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -34,7 +34,7 @@ ]) ->append(glob(__DIR__.'/{*,.*}.php', GLOB_BRACE)) ->append([ - __DIR__.'/composer-fixer', + __DIR__.'/composer-updater', ]) ->notPath([ 'bootstrap/*', diff --git a/composer-fixer b/composer-fixer deleted file mode 100644 index 789be7b..0000000 --- a/composer-fixer +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env php - - * - * This source file is subject to the MIT license that is bundled. - */ - -use Composer\InstalledVersions; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\PhpExecutableFinder; -use Symfony\Component\Process\Process; - -require __DIR__.'/vendor/autoload.php'; - -(new class() { - /** @var string */ - private $composerJsonFile = __DIR__.'/composer.json'; - - /** @var SymfonyStyle */ - private $symfonyStyle; - - public function __construct() - { - $this->symfonyStyle = new SymfonyStyle(new ArgvInput(), new ConsoleOutput()); - } - - public function __invoke(): void - { - $this - ->updateComposerPackages() - ->updateComposerJsonFile($this->updateComposerDecodedJson()) - ->updateComposerPackages() - ->normalizeComposerJsonFile() - ->success(); - } - - private function getComposerDecodedJson(): array - { - /** @noinspection JsonEncodingApiUsageInspection */ - return json_decode(file_get_contents($this->composerJsonFile), true); - } - - private function updateComposerPackages(): self - { - $this->createAndMustRunProcess("{$this->findComposerBinary()} update -W --ansi -v"); - - return $this; - } - - private function updateComposerJsonFile(array $composerDecodedJson): self - { - /** @noinspection JsonEncodingApiUsageInspection */ - file_put_contents( - $this->composerJsonFile, - json_encode( - $composerDecodedJson, - JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES - ).PHP_EOL - ); - - return $this; - } - - private function updateComposerDecodedJson(): array - { - $composerOutdatedPackages = $this->getComposerOutdatedPackages(); - $composerDecodedJson = $this->getComposerDecodedJson(); - - foreach ($composerDecodedJson as $option => &$value) { - if (! in_array($option, ['require', 'require-dev'], true)) { - continue; - } - - foreach ($value as $package => &$version) { - if ( - 'php' === $package - || '*' === $version - || str_starts_with($package, 'ext-') - || str_contains($version, '-') - || str_contains($version, '@') - || str_contains($version, '|') - ) { - continue; - } - - // Update composer outdated package version - if (in_array($package, $composerOutdatedPackages, true)) { - $this->updateComposerOutdatedPackage($package, $option); - } - - // Update composer package version - $version = '^'.implode( - '.', - array_slice( - explode('.', InstalledVersions::getVersion($package), 3), - 0, - 2 - ) - ); - } - } - - return $composerDecodedJson; - } - - private function normalizeComposerJsonFile(): self - { - $this->createAndMustRunProcess("{$this->findComposerBinary()} normalize --diff --ansi -v"); - - return $this; - } - - /** - * @param null|array|string $message - */ - private function success($message = null): void - { - $this->symfonyStyle->success($message ?: 'Composer packages updated successfully!'); - - exit(0); - } - - private function getComposerOutdatedPackages(): array - { - /** @noinspection JsonEncodingApiUsageInspection */ - return array_map( - static function (array $package): string { - return $package['name']; - }, - json_decode( - $this - ->createAndMustRunProcess("{$this->findComposerBinary()} outdated --format=json --direct --ansi -v") - ->getOutput(), - true - )['installed'] - ); - } - - private function updateComposerOutdatedPackage(string $composerOutdatedPackage, string $environment): void - { - try { - $this->createAndMustRunProcess(sprintf( - "{$this->findComposerBinary()} require $composerOutdatedPackage %s --no-scripts -W --ansi -v", - 'require-dev' === $environment ? '--dev' : '' - )); - } catch (ProcessFailedException $processFailedException) { - $this->symfonyStyle->error("Failed to update composer outdated [$composerOutdatedPackage] package version."); - } - } - - private function findComposerBinary(): string - { - return sprintf( - '%s %s', - (new PhpExecutableFinder())->find(), - (new Symfony\Component\Process\ExecutableFinder())->find('composer') - ); - } - - /** - * @param array|string $command - * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input - */ - private function createAndMustRunProcess( - $command, - ?string $cwd = null, - ?array $env = null, - $input = null, - ?float $timeout = 60 - ): Process { - $process = is_string($command) - ? Process::fromShellCommandline($command, $cwd, $env, $input, $timeout) - : new Process($command, $cwd, $env, $input, $timeout); - - $this->symfonyStyle->warning($process->getCommandLine()); - - return $process - ->setWorkingDirectory(dirname($this->composerJsonFile)) - ->setEnv(['COMPOSER_MEMORY_LIMIT' => -1]) - ->mustRun(function (string $type, string $buffer) { - /** @noinspection PhpVoidFunctionResultUsedInspection */ - return $this->symfonyStyle->write($buffer); - }); - } -})(); diff --git a/composer-updater b/composer-updater new file mode 100755 index 0000000..2e99be4 --- /dev/null +++ b/composer-updater @@ -0,0 +1,282 @@ +#!/usr/bin/env php + + * + * This source file is subject to the MIT license that is bundled. + */ + +use Composer\InstalledVersions; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SingleCommandApplication; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Process\ExecutableFinder; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +require __DIR__.'/vendor/autoload.php'; + +/** @noinspection PhpUnhandledExceptionInspection */ +$status = (new SingleCommandApplication()) + ->setName('Composer Updater') + ->addOption('composer-json-path', null, InputOption::VALUE_OPTIONAL) + ->addOption('highest-php-binary', null, InputOption::VALUE_REQUIRED) + ->addOption('composer-binary', null, InputOption::VALUE_OPTIONAL) + ->addOption('except-packages', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL) + ->addOption('except-dependency-versions', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL) + ->setCode(function (InputInterface $input, OutputInterface $output): void { + assert_options(ASSERT_BAIL, 1); + assert($this instanceof SingleCommandApplication); + assert((bool) $input->getOption('highest-php-binary')); + + (new class($input->getOption('composer-json-path') ?: __DIR__.'/composer.json', $input->getOption('highest-php-binary'), $input->getOption('composer-binary'), $input->getOption('except-packages'), $input->getOption('except-dependency-versions'), new SymfonyStyle($input, $output)) { + /** @var string */ + private $composerJsonPath; + /** @var string */ + private $highestComposerBinary; + /** @var string */ + private $composerBinary; + /** @var array */ + private $exceptPackages; + /** @var array */ + private $exceptDependencyVersions; + /** @var SymfonyStyle */ + private $symfonyStyle; + + /** + * @noinspection ParameterDefaultsNullInspection + */ + public function __construct( + string $composerJsonPath, + ?string $highestPhpBinary = null, + ?string $composerBinary = null, + array $exceptPackages = [], + array $exceptDependencyVersions = [], + ?SymfonyStyle $symfonyStyle = null + ) { + assert_options(ASSERT_BAIL, 1); + assert((bool) $composerJsonPath); + + $this->composerJsonPath = $composerJsonPath; + $this->highestComposerBinary = $this->getComposerBinary($composerBinary, $highestPhpBinary); + $this->composerBinary = $this->getComposerBinary($composerBinary); + $this->exceptPackages = array_merge([ + 'php', + 'ext-*', + ], $exceptPackages); + $this->exceptDependencyVersions = array_merge([ + '\*', + '*-*', + '*@*', + // '*|*', + ], $exceptDependencyVersions); + $this->symfonyStyle = $symfonyStyle ?? new SymfonyStyle(new ArgvInput(), new ConsoleOutput()); + } + + public function __invoke(): void + { + $this + ->updateComposerPackages() + ->updateOutdatedComposerPackages() + ->updateComposerPackages() + ->updateOutdatedComposerPackages() + ->updateComposerPackages() + ->normalizeComposerJson() + ->success(); + } + + private function updateComposerPackages(): self + { + $this->mustRunCommand("$this->composerBinary update -W --ansi"); + + return $this; + } + + /** + * @noinspection JsonEncodingApiUsageInspection + */ + private function updateOutdatedComposerPackages(): self + { + file_put_contents( + $this->composerJsonPath, + json_encode( + $this->getOutdatedDecodedComposerJson(), + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ).PHP_EOL + ); + + return $this; + } + + private function normalizeComposerJson(): self + { + $this->mustRunCommand("$this->composerBinary normalize --diff --ansi"); + + return $this; + } + + private function success(): void + { + $this->symfonyStyle->success('Composer packages updated successfully!'); + } + + /** + * @noinspection JsonEncodingApiUsageInspection + */ + private function getOutdatedDecodedComposerJson(): array + { + $outdatedComposerPackages = $this->getOutdatedComposerPackages(); + $decodedComposerJson = json_decode(file_get_contents($this->composerJsonPath), true); + InstalledVersions::reload([]); + + foreach ($decodedComposerJson as $name => &$value) { + if (! in_array($name, ['require', 'require-dev'], true)) { + continue; + } + + foreach ($value as $package => &$dependencyVersion) { + if ( + $this->strIs($this->exceptPackages, $package) + || $this->strIs($this->exceptDependencyVersions, $dependencyVersion) + ) { + continue; + } + + if ($version = InstalledVersions::getVersion($package)) { + $dependencyVersion = $this->toDependencyVersion($version); + } + + if (isset($outdatedComposerPackages[$package])) { + $dependencyVersion = $outdatedComposerPackages[$package]['dependency_version']; + } + } + } + + return $decodedComposerJson; + } + + /** + * @noinspection JsonEncodingApiUsageInspection + */ + private function getOutdatedComposerPackages(): array + { + return array_reduce( + json_decode( + $this + ->mustRunCommand("$this->highestComposerBinary outdated --format=json --direct --ansi") + ->getOutput(), + true + )['installed'], + function (array $carry, array $package): array { + $lowestArrayVersion = $this->toArrayVersion($package['version']); + $highestArrayVersion = $this->toArrayVersion($package['latest']); + $dependencyVersions = [$this->toDependencyVersion($package['version'])]; + + if ($lowestArrayVersion[0] !== $highestArrayVersion[0]) { + $dependencyVersions = array_merge($dependencyVersions, array_map( + static function (string $major): string { + return "^$major.0"; + }, + range($lowestArrayVersion[0] + 1, $highestArrayVersion[0]) + )); + } + + $package['dependency_version'] = implode(' || ', $dependencyVersions); + $carry[$package['name']] = $package; + + return $carry; + }, + [] + ); + } + + private function getComposerBinary(?string $composerBinary = null, ?string $phpBinary = null): string + { + return sprintf( + '%s %s', + $phpBinary ?? (new PhpExecutableFinder())->find(), + $composerBinary ?? (new ExecutableFinder())->find('composer') + ); + } + + /** + * @param array|string $command + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input + * + * @noinspection MissingParameterTypeDeclarationInspection + * @noinspection PhpSameParameterValueInspection + */ + private function mustRunCommand( + $command, + ?string $cwd = null, + ?array $env = null, + $input = null, + ?float $timeout = 300 + ): Process { + $process = is_string($command) + ? Process::fromShellCommandline($command, $cwd, $env, $input, $timeout) + : new Process($command, $cwd, $env, $input, $timeout); + + $this->symfonyStyle->warning($process->getCommandLine()); + + return $process + ->setWorkingDirectory(dirname($this->composerJsonPath)) + ->setEnv(['COMPOSER_MEMORY_LIMIT' => -1]) + ->mustRun(function (string $type, string $buffer): void { + $this->symfonyStyle->isVerbose() and $this->symfonyStyle->write($buffer); + }); + } + + private function toDependencyVersion(string $version): string + { + return '^'.implode('.', array_slice($this->toArrayVersion($version), 0, 2)); + } + + private function toArrayVersion(string $version): array + { + return explode('.', ltrim($version, 'v')); + } + + /** + * @param array|string $pattern + * + * @noinspection SuspiciousLoopInspection + * @noinspection ComparisonScalarOrderInspection + * @noinspection MissingParameterTypeDeclarationInspection + */ + private function strIs($pattern, string $value): bool + { + $patterns = (array) $pattern; + if (empty($patterns)) { + return false; + } + + foreach ($patterns as $pattern) { + $pattern = (string) $pattern; + if ($pattern === $value) { + return true; + } + + $pattern = preg_quote($pattern, '#'); + $pattern = str_replace('\*', '.*', $pattern); + if (1 === preg_match('#^'.$pattern.'\z#u', $value)) { + return true; + } + } + + return false; + } + })(); + }) + ->run(); + +exit($status); diff --git a/composer.json b/composer.json index d596110..98f9087 100644 --- a/composer.json +++ b/composer.json @@ -174,7 +174,7 @@ ], "psalm": "@php ./vendor/bin/psalm", "psalm-baseline": "@psalm --update-baseline", - "rector": "@php ./vendor/bin/rector --clear-cache --ansi -v", + "rector": "@php ./vendor/bin/rector --ansi -v", "rector-dry-run": "@rector --dry-run", "release": "/opt/homebrew/opt/php@7.3/bin/php ./vendor/bin/monorepo-builder release --ansi -v", "release-major": "@release major", diff --git a/rector.php b/rector.php index 1c26161..58c2a12 100644 --- a/rector.php +++ b/rector.php @@ -74,6 +74,7 @@ __DIR__.'/tests', __DIR__.'/.*.php', __DIR__.'/*.php', + __DIR__.'/composer-updater', ]); $rectorConfig->skip([