Skip to content

Commit

Permalink
chore(composer): Add composer-updater script and options
Browse files Browse the repository at this point in the history
- Added composer-updater script to manage composer packages
- Included options for updating composer packages and normalizing composer json
- Provided options for excluding specific packages during updates
- Implemented dry-run option for testing updates without applying changes
  • Loading branch information
guanguans committed Mar 21, 2024
1 parent 094321b commit e089697
Show file tree
Hide file tree
Showing 4 changed files with 348 additions and 1 deletion.
12 changes: 11 additions & 1 deletion .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
'vendor/',
])
->append(glob(__DIR__.'/{.*,*}.php', GLOB_BRACE))
->append([__DIR__.'/bin/composer-fixer.php', __DIR__.'/.changelog'])
->append([
__DIR__.'/bin/composer-fixer.php',
__DIR__.'/.changelog',
__DIR__.'/composer-updater',
])
->notPath([
'bootstrap/*',
'storage/*',
Expand Down Expand Up @@ -289,6 +293,12 @@
],
// 'statement_indentation' => true,
'logical_operators' => false,
'class_definition' => [
// 'multi_line_extends_each_single_line' => false,
// 'single_item_single_line' => false,
// 'single_line' => false,
// 'space_before_parenthesis' => false,
],

// https://github.com/kubawerlos/php-cs-fixer-custom-fixers
PhpCsFixerCustomFixers\Fixer\CommentSurroundedBySpacesFixer::name() => true,
Expand Down
334 changes: 334 additions & 0 deletions composer-updater
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
* This file is part of the guanguans/dcat-login-captcha.
*
* (c) guanguans <[email protected]>
*
* This source file is subject to the MIT license that is bundled.
*/

use Composer\InstalledVersions;
use SebastianBergmann\Diff\Differ;
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)
->addOption('dry-run', null, InputOption::VALUE_NONE)
->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'),
$input->getOption('dry-run'),
new SymfonyStyle($input, $output),
new Differ()
) {
private $composerJsonPath;
private $composerJsonContents;
private $highestComposerBinary;
private $composerBinary;
private $exceptPackages;
private $exceptDependencyVersions;
private $dryRun;
private $symfonyStyle;
private $differ;

/**
* @noinspection ParameterDefaultsNullInspection
*/
public function __construct(
string $composerJsonPath,
?string $highestPhpBinary = null,
?string $composerBinary = null,
array $exceptPackages = [],
array $exceptDependencyVersions = [],
bool $dryRun = false,
?SymfonyStyle $symfonyStyle = null,
?Differ $differ = null
) {
assert_options(ASSERT_BAIL, 1);
assert((bool) $composerJsonPath);

$this->composerJsonPath = $composerJsonPath;
$this->composerJsonContents = file_get_contents($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->dryRun = $dryRun;
$this->symfonyStyle = $symfonyStyle ?? new SymfonyStyle(new ArgvInput(), new ConsoleOutput());
$this->differ = $differ ?? new Differ();
}

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
*
* @return $this|never-return
*/
private function updateOutdatedComposerPackages(): self
{
$outdatedComposerJsonContents = json_encode(
$this->getOutdatedDecodedComposerJson(),
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
).PHP_EOL;

if ($this->dryRun) {
$this->symfonyStyle->writeln($this->formatDiff($this->differ->diff(
$this->composerJsonContents,
$outdatedComposerJsonContents
)));

exit(0);
}

file_put_contents($this->composerJsonPath, $outdatedComposerJsonContents);

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);
(function () {
return self::reload(null);
})->call(new InstalledVersions());

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;
}

private function formatDiff(string $diff): string
{
$lines = explode(
"\n",
$diff,
);

$formatted = array_map(static function (string $line): string {
return preg_replace(
[
'/^(\+.*)$/',
'/^(-.*)$/',
],
[
'<fg=green>$1</>',
'<fg=red>$1</>',
],
$line,
);
}, $lines);

return implode(
"\n",
$formatted,
);
}
})();
})
->run();

exit($status);
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@
"composer-require-checker": "@php ./vendor/bin/composer-require-checker check --config-file=composer-require-checker.json composer.json --ansi -v",
"composer-unused": "@php ./vendor/bin/composer-unused --ansi -v",
"composer-unused-checker": "@php ./vendor/bin/composer-unused --ansi -v",
"composer-updater": "@php ./composer-updater --highest-php-binary=/opt/homebrew/opt/[email protected]/bin/php --except-packages=guanguans/notify --except-packages=laravel/lumen-framework --except-packages=orchestra/testbench --except-packages=pestphp/pest-plugin-laravel --ansi",
"composer-updater-dry-run": "@composer-updater --dry-run",
"composer-validate": "@composer validate --check-lock --strict --ansi -v",
"doctum": "@php ./vendor/bin/doctum.php update doctum.php --ansi -v",
"facade-lint": "@facade-update --lint",
Expand Down
1 change: 1 addition & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
__DIR__.'/updates',
__DIR__.'/.*.php',
__DIR__.'/*.php',
__DIR__.'/composer-updater',
]);

$rectorConfig->skip([
Expand Down

0 comments on commit e089697

Please sign in to comment.