Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only have git patchers and freeform patcher? #472

Merged
merged 6 commits into from
Feb 13, 2023
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
2 changes: 1 addition & 1 deletion docs/api/patches-lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ weight: 40
```
* Each patch definition will look like the [expanded format]({{< relref "../usage/defining-patches.md#expanded-format" >}}) that users can put into their `composer.json` or external patches file.
* No _removals_ or _changes_ will be made to the patch definition object. _Additional_ keys may be created, so any JSON parsing you're doing should be tolerant of new keys.
* The `extra` object in each patch definition may contain a number of attributes set by other projects. The core plugin will not do anything with that data beyond reading/writing it to/from `patches.lock` and you probably shouldn't either (unless you were the one responsible for putting it there in the first place).
* The `extra` object in each patch definition may contain a number of attributes set by other projects or by the user and should be treated as free-form input.
5 changes: 0 additions & 5 deletions docs/troubleshooting/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ See the [system requirements]({{< relref "../getting-started/system-requirements
Composer Patches requires at least _some_ mechanism for applying patches. If you don't have any installed, you'll see a fair number of errors. You should install some combination of GNU `patch`, BSD `patch`, `git`, or other applicable software. macOS users commonly need to `brew install gpatch` to get a modern version of `patch` on their system.


## Set `preferred-install` to `source`

The Git patcher included with this plugin is the most reliable method of applying patches. However, the Git patcher won't even _attempt_ to apply a patch if the target directory isn't cloned from Git. To avoid this situation, you should either set `"preferred-install": "source"` or set specific packages to be installed from source in your Composer configuration. Patches may still be successfully applied without this setting, but if you're working on a team, you'll have much more consistent results with the Git patcher.


## Download patches securely

If you've been referred here, you're trying to download patches over HTTP without explicitly telling Composer that you want to do that. See the [`secure-http`]({{< relref "../usage/configuration.md#secure-http" >}}) documentation for more information.
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/commands.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Commands
weight: 40
weight: 50
---

## `composer patches-relock`
Expand Down
26 changes: 4 additions & 22 deletions docs/usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,9 @@ You probably don't need to change this value unless you're building a plugin tha
"extra": {
"composer-patches": {
"disable-patchers": [
"\\cweagans\\Composer\\Patcher\\BsdPatchPatcher",
"\\cweagans\\Composer\\Patcher\\FreeformPatcher",
"\\cweagans\\Composer\\Patcher\\GitPatcher",
"\\cweagans\\Composer\\Patcher\\GnuGPatchPatcher",
"\\cweagans\\Composer\\Patcher\\GnuPatchPatcher"
"\\cweagans\\Composer\\Patcher\\GitInitPatcher"
]
}
}
Expand All @@ -147,30 +146,13 @@ You probably don't need to change this value unless you're building a plugin tha

For completeness, all of the patchers that ship with the plugin are listed above, but you should _not_ list all of them. If no patchers are available, the plugin will throw an exception during `composer install`.

`GitPatcher` and `GitInitPatcher` should be enabled and disabled together -- don't disable one without the other.

After changing this value, you should re-lock and re-apply patches to your project.


## Relevant configuration provided by Composer

### `preferred-install`

```json
{
[...],
"config": {
"preferred-install": "source"
}
}
```

**Default value**: `"dist"`

The relevant Composer documentation for this parameter can be found [here](https://getcomposer.org/doc/06-config.md#preferred-install).

If you're applying patches that were generated with `git`, setting `preferred-install` to `"source"` is **highly recommended** (either by changing the setting for all packages or by setting that value on a per-package basis as shown in the Composer documentation. This will allow the `GitPatcher` to apply patches as often as possible (the `GitPatcher` won't even _attempt_ to apply a patch if the target directory isn't managed by `git`). Git is the most reliable patcher available to Composer Patches. _Not_ changing this setting will result in other patchers attempting to apply patches. Historically, these patchers have had varying degrees of success depending on a number of factors, so it's better to use the `GitPatcher` when you're able to do so.

---

### `secure-http`

```json
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/defining-patches.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Internally, the plugin uses the expanded format for _all_ patches. Similar to th
2. Any package-specific depth override set globally in the plugin (see `cweagans\Composer\Util::getDefaultPackagePatchDepth()` for details.)
3. The global [`default-patch-depth`]({{< relref "configuration.md#default-patch-depth" >}})

`extra` is primarily a place for developers to store extra data related to a patch, but if you just need a place to put some extra data about a patch, `extra` is a good place for it. No validation is performed on the contents of `extra` and no functionality other than the reading/writing of `patches.lock` uses the data.
`extra` is primarily a place for developers to store extra data related to a patch, but if you just need a place to put some extra data about a patch, `extra` is a good place for it. No validation is performed on the contents of `extra`. The [Freeform patcher]({{< relref "freeform-patcher.md" >}}) stores data here.


## Locations
Expand Down
94 changes: 94 additions & 0 deletions docs/usage/freeform-patcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Freeform Patcher
weight: 30
---

{{< lead text="The core patchers try to take care of as many cases as possible, but sometimes you need custom behavior." >}}

Composer Patches now includes a "freeform patcher", which essentially lets you define a patcher and its arguments in your patch definition.

## Usage

To use the freeform patcher, you must use the [expanded format]({{< relref "defining-patches.md#expanded-format" >}}) for your patch definition. You'll need to add a few extra values to the `extra` key in your patch definition like so:

```json
{
[...],
"extra": {
"patches": {
"the/project": [
{
"description": "This is another patch",
"url": "https://www.example.com/different/path/to/file.patch"
"depth": 123
"extra": {
"freeform": {
"executable": "/path/to/your/executable",
"args": "--verbose %s %s %s",
}
},
},
]
}
}
}
```

If the `executable` and `args` values are not populated, the freeform patcher will not perform any work.

The `%s` placeholders will be populated by escaped arguments that are always provided in this order:

1. Patch depth
2. Path to the location on disk where the dependency was installed
3. Local path to the patch file

It is not possible to change the order of these arguments, but you can always create a small wrapper script in your project and handle the arguments how you'd like.

In this example, the command that would be run by the patcher is
```shell
/path/to/your/executable --verbose '/full/path/to/vendor/the/project' '123' '/full/path/to/file.patch`
```

{{< callout title="$PATH will be searched for executables" >}}
If the executable you want to run is included in your `$PATH`, you do not have to specify the full path to the executable.
{{< /callout >}}

## Dry run

If your patcher is capable of testing whether or not a patch can be applied (for instance, `patch` can do this with the `--dry-run` argument), you can also supply a set of dry run arguments that will be run first:

```json
{
[...],
"extra": {
"patches": {
"the/project": [
{
"description": "This is another patch",
"url": "https://www.example.com/different/path/to/file.patch"
"depth": 123
"extra": {
"freeform": {
"executable": "/path/to/your/executable",
"args": "--verbose %s %s %s",
"dry_run_args": "--verbose --dry-run %s %s %s"
}
},
},
]
}
}
}
```

The arguments will be provided in the same order as the `args` value.

## Tips

### Exit codes matter

If your tool returns an exit code of `0`, Composer Patches will assume that the patch was applied correctly (or that the dry run was successful and the patch should be applied). If it returns anything other than `0`, Composer Patches will assume that the patch was unsuccessful (or that the dry run indicated that attempting to apply the patch would be unsuccessful).

### Always run in verbose mode

Any patcher you configure here should always include the `--verbose` flag (or whatever your patcher's equivalent is). The output will not be printed to the console during normal operation, but if you are running composer with the `--verbose` flag (for instance, with `composer install --verbose` or `composer install -v`), the output will be printed so that you can see what was happening.
2 changes: 1 addition & 1 deletion docs/usage/recommended-workflows.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Recommended Workflows
weight: 30
weight: 40
---

{{< lead text="Common workflows for working with Composer Patches on a team." >}}
Expand Down
10 changes: 4 additions & 6 deletions src/Capability/Patcher/CorePatcherProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

namespace cweagans\Composer\Capability\Patcher;

use cweagans\Composer\Patcher\BsdPatchPatcher;
use cweagans\Composer\Patcher\FreeformPatcher;
use cweagans\Composer\Patcher\GitPatcher;
use cweagans\Composer\Patcher\GnuGPatchPatcher;
use cweagans\Composer\Patcher\GnuPatchPatcher;
use cweagans\Composer\Patcher\GitInitPatcher;

class CorePatcherProvider extends BasePatcherProvider
{
Expand All @@ -16,9 +15,8 @@ public function getPatchers(): array
{
return [
new GitPatcher($this->composer, $this->io),
new GnuPatchPatcher($this->composer, $this->io),
new GnuGPatchPatcher($this->composer, $this->io),
new BsdPatchPatcher($this->composer, $this->io),
new GitInitPatcher($this->composer, $this->io),
new FreeformPatcher($this->composer, $this->io)
];
}
}
60 changes: 0 additions & 60 deletions src/Command/DoctorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,66 +109,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$io->write("");
$io->write("<info>Common configuration issues</info>");
$io->write("================================================================================");
$preferred_install_issues = false;
$pi = $composer->getConfig()->get('preferred-install');
$io->write(
str_pad("preferred-install is set:", 77) . (is_null($pi) ? " <warning>no</warning>" : "<info>yes</info>")
);

if (is_null($pi)) {
$preferred_install_issues = true;
}

if (is_string($pi)) {
$io->write(
str_pad("preferred-install set to 'source' for all/some packages:", 77) .
($pi === "source" ? "<info>yes</info>" : " <warning>no</warning>")
);
}

if (is_string($pi) && $pi !== "source") {
$preferred_install_issues = true;
}

if (is_array($pi)) {
$patched_packages = $plugin->getPatchCollection()->getPatchedPackages();
foreach ($patched_packages as $package) {
if (in_array($package, array_values($pi))) {
$io->write(
str_pad("preferred-install set to 'source' for $package:", 77) . "<info>yes</info>"
);
continue;
}

foreach ($pi as $pattern => $value) {
$pattern = strtr($pattern, ['*' => '.*', '/' => '\/']);
if (preg_match("/$pattern/", $package)) {
$io->write(
str_pad("preferred-install set to 'source' for $package:", 77) .
($value === "source" ? "<info>yes</info>" : " <warning>no</warning>")
);

if ($value !== "source") {
$preferred_install_issues = true;
}

break 2;
}
}

$preferred_install_issues = true;
}
}

if ($preferred_install_issues) {
$suggestions[] = [
"message" => "Setting 'preferred-install' to 'source' either globally or for each patched dependency " .
"is highly recommended for consistent results",
"link" =>
"https://docs.cweagans.net/composer-patches/troubleshooting/guide#set-preferred-install-to-source"
];
}

$has_http_urls = false;
foreach ($plugin->getPatchCollection()->getPatchedPackages() as $package) {
foreach ($plugin->getPatchCollection()->getPatchesForPackage($package) as $patch) {
Expand Down
2 changes: 1 addition & 1 deletion src/Patch.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class Patch implements JsonSerializable
*
* @var array
*/
public array $extra;
public array $extra = [];

/**
* Create a Patch from a serialized representation.
Expand Down
42 changes: 0 additions & 42 deletions src/Patcher/BsdPatchPatcher.php

This file was deleted.

59 changes: 59 additions & 0 deletions src/Patcher/FreeformPatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

namespace cweagans\Composer\Patcher;

use cweagans\Composer\Patch;
use Composer\IO\IOInterface;

class FreeformPatcher extends PatcherBase
{
public function apply(Patch $patch, string $path): bool
{
// Required.
$patchTool = $patch->extra['freeform']['executable'] ?? '';
$args = $patch->extra['freeform']['args'] ?? '';

// Optional.
$dryRunArgs = $patch->extra['freeform']['dry_run_args'] ?? '';

// If we don't have what we need, exit.
if (empty($patchTool) || empty($args)) {
$this->io->write(
'Required configuration for FreeformPatcher not present. Skipping.',
true,
IOInterface::VERBOSE
);
return false;
}

// If we have dry-run args, do a dry-run.
if (!empty($dryRunArgs)) {
$status = $this->executeCommand(
'%s ' . $args,
$patchTool,
$patch->depth,
$path,
$patch->localPath,
);
if (!$status) {
return false;
}
}

// Apply the patch.
return $this->executeCommand(
'%s ' . $args,
$patchTool,
$patch->depth,
$path,
$patch->localPath,
);
}

public function canUse(): bool
{
// Hardcoded to true because apply() will bail out if the freeform args are not set on the patch (or globally).
// Users who opt-in to this patcher are responsible for providing a valid executable and arguments.
return true;
}
}
Loading