Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ on:

jobs:
test:
name: PHP ${{ matrix.php }} / Symfony ${{ matrix.symfony }} / Composer Patches ${{ matrix.composer-patches }}
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.2', '8.3', '8.4']
symfony: ['^7.0', '^8.0']
composer-patches: ['^1.7', '^2.0']
exclude:
- php: '8.2'
symfony: '^8.0'
Expand All @@ -32,7 +34,7 @@ jobs:
# Install dependencies
- name: Install dependencies
run: |
composer require "symfony/console:${{ matrix.symfony }}" --dev --no-update --no-interaction
composer require "symfony/console:${{ matrix.symfony }}" "cweagans/composer-patches:${{ matrix.composer-patches }}" --dev --no-update --no-interaction
composer update --prefer-dist --no-progress

# Run PHPUnit tests
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"require": {
"composer-plugin-api": "^2.0",
"cweagans/composer-patches": "^1.7",
"cweagans/composer-patches": "^1.7 || ^2.0",
"php": "^8.0"
},
"require-dev": {
Expand Down
58 changes: 18 additions & 40 deletions src/Composer/PatchAddCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

namespace szeidler\ComposerPatchesCLI\Composer;

use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Composer\Json\JsonFile;
use Composer\Json\JsonManipulator;
use Composer\Installer;
use Composer\Plugin\PluginInterface;
use Composer\DependencyResolver\Request;

class PatchAddCommand extends PatchBaseCommand {

Expand Down Expand Up @@ -76,11 +74,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

if ($this->getPatchType() === self::PATCHTYPE_ROOT) {
$manipulator_filename = 'composer.json';
$json_node = 'extra';
$json_name = 'composer-patches.patches';
}
elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) {
$manipulator_filename = 'composer.json';
$json_node = 'extra';
$json_name = 'patches';
}
elseif ($this->getPatchType() === self::PATCHTYPE_FILE) {
$manipulator_filename = $extra['composer-patches']['patches-file'];
$json_node = null;
$json_name = 'patches';
}
elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) {
$manipulator_filename = $extra['patches-file'];
$json_node = null;
$json_name = 'patches';
Expand Down Expand Up @@ -131,47 +139,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int

// Store the manipulated JSON file.
if (!file_put_contents($manipulator_filename, $manipulator->getContents())) {
throw new \Exception($extra['patches-file'] . ' file could not be saved. Please check the permissions.');
throw new \Exception($manipulator_filename . ' file could not be saved. Please check the permissions.');
}
$output->writeln('The patch was successfully added.');

if (!$input->getOption('no-update')) {
// Trigger install command after adding a patch.
$install = Installer::create($this->getIO(), $this->requireComposer());

// We run an update, because the patch will otherwise not end up in the
// composer.lock. Beware: This could update the package unwanted.
// Support Composer 1 and Composer 2 methods.
switch (PluginInterface::PLUGIN_API_VERSION) {
case '1.1.0':
$install->setUpdate(TRUE)
// Forward the option
->setVerbose($input->getOption('verbose'))
// Only update the current package
->setUpdateWhitelist([$package])
// Don't update the dependencies of the patched package.
->setWhitelistTransitiveDependencies(FALSE)
->setWhitelistAllDependencies(FALSE)
// Patches are always considered to be applied in "dev mode".
// This is also required to prevent composer from removing all installed
// dev dependencies.
->setDevMode($updateDevMode)
->run();
break;
default:
$install->setUpdate(TRUE)
// Forward the option
->setVerbose($input->getOption('verbose'))
// Only update the current package
->setUpdateAllowList([$package])
// Don't update the dependencies of the patched package.
->setUpdateAllowTransitiveDependencies(Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE)
// Patches are always considered to be applied in "dev mode".
// This is also required to prevent composer from removing all installed
// dev dependencies.
->setDevMode($updateDevMode)
->run();
}
$application = $this->getApplication();
$application->setAutoExit(FALSE);
$output->writeln('<info>Relocking patches...</info>');
$application->run(new ArrayInput(['command' => 'patches-relock']), $output);
$output->writeln('<info>Repatching dependencies...</info>');
$application->run(new ArrayInput(['command' => 'patches-repatch']), $output);
}

return 0;
Expand Down
105 changes: 99 additions & 6 deletions src/Composer/PatchBaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

use Composer\Factory;
use Composer\Command\BaseCommand;
use Composer\Semver\Comparator;

class PatchBaseCommand extends BaseCommand {

const PATCHTYPE_ROOT = 1;
const PATCHTYPE_FILE = 2;
const PATCHTYPE_ROOT_CP1 = 3;
const PATCHTYPE_FILE_CP1 = 4;

protected function configure(): void {
parent::configure();
Expand All @@ -25,22 +28,71 @@ protected function configure(): void {
protected function getPatchType() {
$extra = $this->requireComposer()->getPackage()->getExtra();

if (isset($extra['patches'])) {
if (isset($extra['composer-patches']['patches'])) {
return self::PATCHTYPE_ROOT;
}
elseif (isset($extra['patches-file'])) {
elseif (isset($extra['patches'])) {
return self::PATCHTYPE_ROOT_CP1;
}
elseif (isset($extra['composer-patches']['patches-file'])) {
return self::PATCHTYPE_FILE;
}
elseif (isset($extra['patches-file'])) {
return self::PATCHTYPE_FILE_CP1;
}

return NULL;
}

/**
* Returns the version of cweagans/composer-patches if installed.
*
* @return string|null
*/
protected function getComposerPatchesVersion() {
$composer = $this->requireComposer();
$repositoryManager = $composer->getRepositoryManager();
$localRepository = $repositoryManager->getLocalRepository();
$packages = $localRepository->getPackages();

foreach ($packages as $package) {
if ($package->getName() === 'cweagans/composer-patches') {
return $package->getVersion();
}
}

// Fallback: check require in composer.json if not in local repo (e.g. during tests or before install)
$configPath = Factory::getComposerFile();
if (file_exists($configPath)) {
$config = json_decode(file_get_contents($configPath), true);
$allRequires = array_merge($config['require'] ?? [], $config['require-dev'] ?? []);
if (isset($allRequires['cweagans/composer-patches'])) {
$versionConstraint = $allRequires['cweagans/composer-patches'];
if (Comparator::lessThan($versionConstraint, '2.0.0') || strpos($versionConstraint, 'dev-') === 0) {
return '1.99.99'; // Simulated version for Composer Patches 1
}
}
}

return NULL;
}

/**
* Checks if the installed version of Composer Patches is version 1.
*
* @return bool
*/
protected function isComposerPatches1() {
$version = $this->getComposerPatchesVersion();
return $version && version_compare($version, '2.0.0', '<');
}

/**
* Get the patches from root composer or external file
*
* Currently directly extracted from the Composer Patches code base.
*
* @return Patches
* @return array
* @throws \Exception
* @see https://github.com/cweagans/composer-patches/blob/1.x/src/Patches.php
*/
Expand All @@ -49,13 +101,55 @@ protected function grabPatches() {
$extra = $this->requireComposer()->getPackage()->getExtra();
if ($this->getPatchType() === self::PATCHTYPE_ROOT) {
$this->getIO()->write('<info>Gathering patches from root composer.json.</info>');
$patches = $extra['composer-patches']['patches'];
return $patches;
}
elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) {
$this->getIO()->write('<info>Gathering patches from root composer.json (extra.patches).</info>');
$patches = $extra['patches'];
return $patches;
}
// If it's not specified there, look for a patches-file definition.
elseif ($this->getPatchType() === self::PATCHTYPE_FILE) {
$this->getIO()->write('<info>Gathering patches from patch file.</info>');
$patches = file_get_contents($extra['patches-file']);
$patchesFile = $extra['composer-patches']['patches-file'];
$patches = file_get_contents($patchesFile);
$patches = json_decode($patches, TRUE);
$error = json_last_error();
if ($error != 0) {
switch ($error) {
case JSON_ERROR_DEPTH:
$msg = ' - Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = ' - Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = ' - Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$msg = ' - Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$msg = ' - Unknown error';
break;
}
throw new \Exception('There was an error in the supplied patches file:' . $msg);
}
if (isset($patches['patches'])) {
return $patches['patches'];
}
elseif (!$patches) {
throw new \Exception('There was an error in the supplied patch file');
}
}
elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) {
$this->getIO()->write('<info>Gathering patches from patch file (extra.patches-file).</info>');
$patchesFile = $extra['patches-file'];
$patches = file_get_contents($patchesFile);
$patches = json_decode($patches, TRUE);
$error = json_last_error();
if ($error != 0) {
Expand All @@ -82,8 +176,7 @@ protected function grabPatches() {
throw new \Exception('There was an error in the supplied patches file:' . $msg);
}
if (isset($patches['patches'])) {
$patches = $patches['patches'];
return $patches;
return $patches['patches'];
}
elseif (!$patches) {
throw new \Exception('There was an error in the supplied patch file');
Expand Down
22 changes: 15 additions & 7 deletions src/Composer/PatchEnableCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$extra = $this->requireComposer()->getPackage()->getExtra();

// Check, if patch file is already defined.
if (!empty($extra['patches-file'])) {
if (!empty($extra['patches-file']) || !empty($extra['composer-patches']['patches-file'])) {
throw new \Exception('Patch file was already defined in your composer.json.');
}

Expand All @@ -49,18 +49,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int
throw new \Exception('Patch could not be created.');
}
}
$manipulator->addProperty('extra.patches-file', $patches_filename);

if ($this->isComposerPatches1()) {
$manipulator->addProperty('extra.patches-file', $patches_filename);
}
else {
$manipulator->addProperty('extra.composer-patches.patches-file', $patches_filename);
}
}
else {
// Create an empty patches definition in the root composer.json.
if (!isset($extra['patches'])) {
$manipulator->addProperty('extra.patches', []);
if (!isset($extra['patches']) && !isset($extra['composer-patches']['patches'])) {
if ($this->isComposerPatches1()) {
$manipulator->addProperty('extra.patches', []);
}
else {
$manipulator->addProperty('extra.composer-patches.patches', []);
}
}
}

// Enable patching.
$manipulator->addProperty('extra.enable-patching', TRUE);

// Store the manipulated JSON file.
if (!file_put_contents($composer_filename, $manipulator->getContents())) {
throw new \Exception('Composer file could not be saved. Please check the permissions.');
Expand Down
16 changes: 16 additions & 0 deletions src/Composer/PatchMoveToLocalCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace szeidler\ComposerPatchesCLI\Composer;

use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand Down Expand Up @@ -33,10 +34,18 @@ protected function execute(
$extra = $this->requireComposer()->getPackage()->getExtra();

if ($this->getPatchType() === self::PATCHTYPE_ROOT) {
$manipulator_filename = 'composer.json';
$json_node = 'extra';
$json_name = 'composer-patches.patches';
} elseif ($this->getPatchType() === self::PATCHTYPE_ROOT_CP1) {
$manipulator_filename = 'composer.json';
$json_node = 'extra';
$json_name = 'patches';
} elseif ($this->getPatchType() === self::PATCHTYPE_FILE) {
$manipulator_filename = $extra['composer-patches']['patches-file'];
$json_node = null;
$json_name = 'patches';
} elseif ($this->getPatchType() === self::PATCHTYPE_FILE_CP1) {
$manipulator_filename = $extra['patches-file'];
$json_node = null;
$json_name = 'patches';
Expand Down Expand Up @@ -137,6 +146,13 @@ protected function execute(
$this->getIO()->write(
'<info>Remote Composer patches got successfully moved to local files and got updated in the composer.json or composer.patches.json.</info>'
);

$application = $this->getApplication();
$application->setAutoExit(FALSE);
$output->writeln('<info>Relocking patches...</info>');
$application->run(new ArrayInput(['command' => 'patches-relock']), $output);
$output->writeln('<info>Repatching dependencies...</info>');
$application->run(new ArrayInput(['command' => 'patches-repatch']), $output);
} else {
throw new \Exception(
'Composer patches file could not be saved. Please check the permissions.'
Expand Down
Loading