Skip to content

Commit b6e014b

Browse files
authored
Merge pull request #173 from asgrim/cleanup-vendor-after-install
Cleanup vendor after install
2 parents a53b255 + 4ae2f35 commit b6e014b

File tree

11 files changed

+427
-5
lines changed

11 files changed

+427
-5
lines changed

src/ComposerIntegration/ComposerIntegrationHandler.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class ComposerIntegrationHandler
2525
public function __construct(
2626
private readonly ContainerInterface $container,
2727
private readonly QuieterConsoleIO $arrayCollectionIo,
28+
private readonly VendorCleanup $vendorCleanup,
2829
) {
2930
}
3031

@@ -86,5 +87,7 @@ public function __invoke(
8687

8788
throw ComposerRunFailed::fromExitCode($resultCode);
8889
}
90+
91+
($this->vendorCleanup)($composer);
8992
}
9093
}

src/ComposerIntegration/PieComposerFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public static function createPieComposer(
7575
);
7676

7777
OverrideWindowsUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest);
78+
RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest);
7879

7980
$composer->getConfig()->merge(['config' => ['__PIE_REQUEST__' => $composerRequest]]);
8081
$io->loadConfiguration($composer->getConfig());

src/ComposerIntegration/PiePackageInstaller.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Composer\Repository\InstalledRepositoryInterface;
1313
use Composer\Util\Filesystem;
1414
use Php\Pie\ExtensionType;
15+
use Symfony\Component\Console\Output\OutputInterface;
1516

1617
use function sprintf;
1718

@@ -39,11 +40,14 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa
3940
$output = $this->composerRequest->pieOutput;
4041

4142
if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) {
42-
$output->writeln(sprintf(
43-
'<error>Not using PIE to install %s as it was not the expected package %s</error>',
44-
$composerPackage->getName(),
45-
$this->composerRequest->requestedPackage->package,
46-
));
43+
$output->writeln(
44+
sprintf(
45+
'<comment>Skipping %s install request from Composer as it was not the expected PIE package %s</comment>',
46+
$composerPackage->getName(),
47+
$this->composerRequest->requestedPackage->package,
48+
),
49+
OutputInterface::VERBOSITY_VERY_VERBOSE,
50+
);
4751

4852
return null;
4953
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\Pie\ComposerIntegration;
6+
7+
use Closure;
8+
use Composer\Composer;
9+
use Composer\DependencyResolver\Operation\InstallOperation;
10+
use Composer\DependencyResolver\Operation\OperationInterface;
11+
use Composer\DependencyResolver\Transaction;
12+
use Composer\Installer\InstallerEvent;
13+
use Composer\Installer\InstallerEvents;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
use function array_filter;
17+
use function assert;
18+
use function sprintf;
19+
20+
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
21+
class RemoveUnrelatedInstallOperations
22+
{
23+
public function __construct(
24+
private readonly PieComposerRequest $composerRequest,
25+
) {
26+
}
27+
28+
public static function selfRegister(
29+
Composer $composer,
30+
PieComposerRequest $composerRequest,
31+
): void {
32+
$composer
33+
->getEventDispatcher()
34+
->addListener(
35+
InstallerEvents::PRE_OPERATIONS_EXEC,
36+
new self($composerRequest),
37+
);
38+
}
39+
40+
/**
41+
* @psalm-suppress InternalProperty
42+
* @psalm-suppress InternalMethod
43+
*/
44+
public function __invoke(InstallerEvent $installerEvent): void
45+
{
46+
$pieOutput = $this->composerRequest->pieOutput;
47+
48+
$newOperations = array_filter(
49+
$installerEvent->getTransaction()?->getOperations() ?? [],
50+
function (OperationInterface $operation) use ($pieOutput): bool {
51+
if (! $operation instanceof InstallOperation) {
52+
$pieOutput->writeln(
53+
sprintf(
54+
'Unexpected operation during installer: %s',
55+
$operation::class,
56+
),
57+
OutputInterface::VERBOSITY_VERY_VERBOSE,
58+
);
59+
60+
return false;
61+
}
62+
63+
$isRequestedPiePackage = $this->composerRequest->requestedPackage->package === $operation->getPackage()->getName();
64+
65+
if (! $isRequestedPiePackage) {
66+
$pieOutput->writeln(
67+
sprintf(
68+
'Filtering package %s from install operations, as it was not the requested package',
69+
$operation->getPackage()->getName(),
70+
),
71+
OutputInterface::VERBOSITY_VERY_VERBOSE,
72+
);
73+
}
74+
75+
return $isRequestedPiePackage;
76+
},
77+
);
78+
79+
$overrideOperations = Closure::Bind(
80+
static function (Transaction $transaction) use ($newOperations): void {
81+
/** @psalm-suppress InaccessibleProperty */
82+
$transaction->operations = $newOperations;
83+
},
84+
null,
85+
Transaction::class,
86+
);
87+
assert($overrideOperations !== null);
88+
$overrideOperations($installerEvent->getTransaction());
89+
}
90+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\Pie\ComposerIntegration;
6+
7+
use Composer\Composer;
8+
use Composer\Util\Filesystem;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
11+
use function array_filter;
12+
use function array_walk;
13+
use function in_array;
14+
use function is_array;
15+
use function scandir;
16+
use function sprintf;
17+
18+
use const DIRECTORY_SEPARATOR;
19+
20+
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
21+
class VendorCleanup
22+
{
23+
public function __construct(
24+
private readonly OutputInterface $output,
25+
private readonly Filesystem $filesystem,
26+
) {
27+
}
28+
29+
public function __invoke(Composer $composer): void
30+
{
31+
$vendorDir = (string) $composer->getConfig()->get('vendor-dir');
32+
$vendorContents = scandir($vendorDir);
33+
34+
if (! is_array($vendorContents)) {
35+
$this->output->writeln(
36+
sprintf(
37+
'<comment>Vendor directory (vendor-dir config) %s seemed invalid?/comment>',
38+
$vendorDir,
39+
),
40+
OutputInterface::VERBOSITY_VERY_VERBOSE,
41+
);
42+
43+
return;
44+
}
45+
46+
$toRemove = array_filter(
47+
$vendorContents,
48+
static function (string $path): bool {
49+
return ! in_array(
50+
$path,
51+
[
52+
'.',
53+
'..',
54+
'autoload.php',
55+
'composer',
56+
],
57+
);
58+
},
59+
);
60+
61+
array_walk(
62+
$toRemove,
63+
function (string $pathToRemove) use ($vendorDir): void {
64+
$fullPathToRemove = $vendorDir . DIRECTORY_SEPARATOR . $pathToRemove;
65+
66+
$this->output->writeln(
67+
sprintf(
68+
'<comment>Removing: %s</comment>',
69+
$fullPathToRemove,
70+
),
71+
OutputInterface::VERBOSITY_VERY_VERBOSE,
72+
);
73+
74+
if ($this->filesystem->remove($fullPathToRemove)) {
75+
return;
76+
}
77+
78+
$this->output->writeln(
79+
sprintf(
80+
'<comment>Warning: failed to remove %s</comment>',
81+
$fullPathToRemove,
82+
),
83+
OutputInterface::VERBOSITY_VERBOSE,
84+
);
85+
},
86+
);
87+
}
88+
}

test/assets/vendor-cleanup-dir/autoload.php

Whitespace-only changes.

test/assets/vendor-cleanup-dir/composer/dummy

Whitespace-only changes.

test/assets/vendor-cleanup-dir/vendor1/dummy

Whitespace-only changes.

test/assets/vendor-cleanup-dir/vendor2/dummy

Whitespace-only changes.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Php\PieUnitTest\ComposerIntegration;
6+
7+
use Composer\Composer;
8+
use Composer\DependencyResolver\Operation\InstallOperation;
9+
use Composer\DependencyResolver\Operation\OperationInterface;
10+
use Composer\DependencyResolver\Transaction;
11+
use Composer\EventDispatcher\EventDispatcher;
12+
use Composer\Installer\InstallerEvent;
13+
use Composer\Installer\InstallerEvents;
14+
use Composer\IO\IOInterface;
15+
use Composer\Package\CompletePackage;
16+
use Php\Pie\ComposerIntegration\PieComposerRequest;
17+
use Php\Pie\ComposerIntegration\PieOperation;
18+
use Php\Pie\ComposerIntegration\RemoveUnrelatedInstallOperations;
19+
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
20+
use Php\Pie\Platform\Architecture;
21+
use Php\Pie\Platform\OperatingSystem;
22+
use Php\Pie\Platform\OperatingSystemFamily;
23+
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
24+
use Php\Pie\Platform\TargetPlatform;
25+
use Php\Pie\Platform\ThreadSafetyMode;
26+
use Php\Pie\Platform\WindowsCompiler;
27+
use PHPUnit\Framework\Attributes\CoversClass;
28+
use PHPUnit\Framework\MockObject\MockObject;
29+
use PHPUnit\Framework\TestCase;
30+
use Symfony\Component\Console\Output\OutputInterface;
31+
32+
use function array_filter;
33+
use function array_map;
34+
35+
#[CoversClass(RemoveUnrelatedInstallOperations::class)]
36+
final class RemoveUnrelatedInstallOperationsTest extends TestCase
37+
{
38+
private Composer&MockObject $composer;
39+
40+
public function setUp(): void
41+
{
42+
parent::setUp();
43+
44+
$this->composer = $this->createMock(Composer::class);
45+
}
46+
47+
public function testEventListenerRegistration(): void
48+
{
49+
$eventDispatcher = $this->createMock(EventDispatcher::class);
50+
$eventDispatcher
51+
->expects(self::once())
52+
->method('addListener')
53+
->with(
54+
InstallerEvents::PRE_OPERATIONS_EXEC,
55+
self::isInstanceOf(RemoveUnrelatedInstallOperations::class),
56+
);
57+
58+
$this->composer
59+
->expects(self::once())
60+
->method('getEventDispatcher')
61+
->willReturn($eventDispatcher);
62+
63+
RemoveUnrelatedInstallOperations::selfRegister(
64+
$this->composer,
65+
new PieComposerRequest(
66+
$this->createMock(OutputInterface::class),
67+
new TargetPlatform(
68+
OperatingSystem::NonWindows,
69+
OperatingSystemFamily::Linux,
70+
PhpBinaryPath::fromCurrentProcess(),
71+
Architecture::x86_64,
72+
ThreadSafetyMode::NonThreadSafe,
73+
1,
74+
WindowsCompiler::VC15,
75+
),
76+
new RequestedPackageAndVersion('foo/bar', '^1.1'),
77+
PieOperation::Install,
78+
[],
79+
null,
80+
false,
81+
),
82+
);
83+
}
84+
85+
/** @psalm-suppress InternalMethod */
86+
public function testUnrelatedInstallOperationsAreRemoved(): void
87+
{
88+
$composerPackage1 = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
89+
$composerPackage2 = new CompletePackage('bat/baz', '3.4.5.0', '3.4.5');
90+
$composerPackage3 = new CompletePackage('qux/quux', '5.6.7.0', '5.6.7');
91+
92+
/**
93+
* @psalm-suppress InternalClass
94+
* @psalm-suppress InternalMethod
95+
*/
96+
$installerEvent = new InstallerEvent(
97+
InstallerEvents::PRE_OPERATIONS_EXEC,
98+
$this->composer,
99+
$this->createMock(IOInterface::class),
100+
false,
101+
true,
102+
new Transaction([], [$composerPackage1, $composerPackage2, $composerPackage3]),
103+
);
104+
105+
(new RemoveUnrelatedInstallOperations(
106+
new PieComposerRequest(
107+
$this->createMock(OutputInterface::class),
108+
new TargetPlatform(
109+
OperatingSystem::Windows,
110+
OperatingSystemFamily::Linux,
111+
PhpBinaryPath::fromCurrentProcess(),
112+
Architecture::x86_64,
113+
ThreadSafetyMode::NonThreadSafe,
114+
1,
115+
WindowsCompiler::VC15,
116+
),
117+
new RequestedPackageAndVersion('bat/baz', '^3.2'),
118+
PieOperation::Install,
119+
[],
120+
null,
121+
false,
122+
),
123+
))($installerEvent);
124+
125+
self::assertSame(
126+
['bat/baz'],
127+
array_map(
128+
static fn (InstallOperation $operation): string => $operation->getPackage()->getName(),
129+
array_filter(
130+
$installerEvent->getTransaction()?->getOperations() ?? [],
131+
static fn (OperationInterface $operation): bool => $operation instanceof InstallOperation,
132+
),
133+
),
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)