-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(composer): Add composer-updater script and options
- 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
Showing
4 changed files
with
348 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters