diff --git a/src/QuickInstall/Sandbox/ComposerMetadataCompatibility.php b/src/QuickInstall/Sandbox/ComposerMetadataCompatibility.php new file mode 100644 index 00000000..e471e7fe --- /dev/null +++ b/src/QuickInstall/Sandbox/ComposerMetadataCompatibility.php @@ -0,0 +1,75 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + */ + +namespace QuickInstall\Sandbox; + +use RuntimeException; + +/** Normalizes Composer metadata required by legacy phpBB dependencies. */ +class ComposerMetadataCompatibility +{ + /** + * Restores the flat installed.json format expected by package-versions 1.x. + * + * Composer 2 uses installed.php itself, so retaining the legacy JSON shape is + * safe until Composer next regenerates the vendor metadata. + */ + public static function normalizePackageVersions(string $root): bool + { + $root = rtrim(str_replace('\\', '/', $root), '/') . '/'; + $versionsPath = $root . 'vendor/ocramius/package-versions/src/PackageVersions/Versions.php'; + $installedPath = $root . 'vendor/composer/installed.json'; + if (!is_file($versionsPath) || !is_file($installedPath)) + { + return false; + } + + $versions = file_get_contents($versionsPath); + if (!is_string($versions) || !preg_match('/const\s+VERSIONS\s*=\s*\[\s*\]\s*;/', $versions)) + { + return false; + } + + $installed = file_get_contents($installedPath); + $data = json_decode((string) $installed, true); + if (!is_array($data)) + { + throw new RuntimeException("Invalid legacy Composer metadata: $installedPath"); + } + if (!isset($data['packages'])) + { + if (is_string($installed) && substr(ltrim($installed), 0, 1) === '[') + { + // Composer 1 metadata already uses the format expected by the fallback. + return false; + } + + throw new RuntimeException("Unsupported Composer metadata format: $installedPath"); + } + if (!is_array($data['packages'])) + { + throw new RuntimeException("Invalid Composer package metadata: $installedPath"); + } + + $json = json_encode($data['packages'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($json === false) + { + throw new RuntimeException("Unable to encode legacy Composer metadata: $installedPath"); + } + + $contents = $json . "\n"; + if (file_put_contents($installedPath, $contents, LOCK_EX) !== strlen($contents)) + { + throw new RuntimeException("Unable to normalize legacy Composer metadata: $installedPath"); + } + + return true; + } +} diff --git a/src/QuickInstall/Sandbox/SourceProvider.php b/src/QuickInstall/Sandbox/SourceProvider.php index 2b0f5576..7232b963 100644 --- a/src/QuickInstall/Sandbox/SourceProvider.php +++ b/src/QuickInstall/Sandbox/SourceProvider.php @@ -359,6 +359,7 @@ public function fetch(array $source): void $this->normalizeGitSourceRoot($path); $this->run($this->composerCommand(['install', '--no-interaction', '--ignore-platform-reqs']), $path); + ComposerMetadataCompatibility::normalizePackageVersions($path); return; } @@ -385,6 +386,7 @@ public function fetch(array $source): void } $this->run($command, dirname($path)); + ComposerMetadataCompatibility::normalizePackageVersions($path); } protected function normalizeGitSourceRoot(string $path): void @@ -470,6 +472,7 @@ protected function installedPhpbbVersion(string $path): string protected function withInstalledSourceMetadata(array $source, ?string $defaultPhp): array { + ComposerMetadataCompatibility::normalizePackageVersions($source['path'] ?? ''); $detectedVersion = $this->detectedPhpbbVersion($source['path'] ?? ''); if ($detectedVersion !== null) { diff --git a/src/QuickInstall/Sandbox/bootstrap.php b/src/QuickInstall/Sandbox/bootstrap.php index 39cb8dca..c21b6487 100644 --- a/src/QuickInstall/Sandbox/bootstrap.php +++ b/src/QuickInstall/Sandbox/bootstrap.php @@ -19,6 +19,7 @@ require_once __DIR__ . '/DoctorService.php'; require_once __DIR__ . '/VersionMatrix.php'; require_once __DIR__ . '/UpdateService.php'; +require_once __DIR__ . '/ComposerMetadataCompatibility.php'; require_once __DIR__ . '/SourceProvider.php'; require_once __DIR__ . '/SourceService.php'; require_once __DIR__ . '/BoardService.php'; diff --git a/tests/Unit/SourceProviderTest.php b/tests/Unit/SourceProviderTest.php index ab7e169a..6e95425c 100644 --- a/tests/Unit/SourceProviderTest.php +++ b/tests/Unit/SourceProviderTest.php @@ -106,6 +106,23 @@ public function testFetchComposerBuildsCreateProjectCommand(): void self::assertSame(dirname($project->sourcePath('3.3.14')), $provider->runs[0]['cwd']); } + public function testFetchComposerNormalizesLegacyPackageVersionsMetadata(): void + { + $project = $this->project(); + $provider = new CompatibilitySourceProvider($project); + $path = $project->sourcePath('3.3.2'); + + $provider->fetch([ + 'type' => 'composer', + 'constraint' => '3.3.2', + 'path' => $path, + ]); + + self::assertSame([ + ['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'], + ], json_decode((string) file_get_contents($path . '/vendor/composer/installed.json'), true)); + } + public function testFetchGitNormalizesPhpbbSubdirectoryAndRunsComposerInstall(): void { $project = $this->project(); @@ -126,6 +143,66 @@ public function testFetchGitNormalizesPhpbbSubdirectoryAndRunsComposerInstall(): self::assertSame(['composer-bin', 'install', '--no-interaction', '--ignore-platform-reqs'], $provider->runs[1]['command']); } + public function testFetchGitNormalizesLegacyPackageVersionsMetadata(): void + { + $project = $this->project(); + $provider = new CompatibilitySourceProvider($project); + $path = $project->sourcePath('custom'); + + $provider->fetch([ + 'type' => 'git', + 'url' => 'https://github.com/phpbb/phpbb.git', + 'branch' => 'release-3.3.2', + 'version' => 'release-3.3.2', + 'path' => $path, + ]); + + self::assertSame([ + ['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'], + ], json_decode((string) file_get_contents($path . '/vendor/composer/installed.json'), true)); + } + + public function testEnsureNormalizesReusedComposerSource(): void + { + $project = $this->project(); + $this->addDownloadedSource($project, '3.3.2'); + $path = $project->sourcePath('3.3.2'); + $this->addLegacyComposerMetadata($path); + + (new TestSourceProvider($project))->ensure('3.3.2'); + + self::assertSame([ + ['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'], + ], json_decode((string) file_get_contents($path . '/vendor/composer/installed.json'), true)); + } + + public function testEnsureLeavesGeneratedPackageVersionsMetadataUntouched(): void + { + $project = $this->project(); + $this->addDownloadedSource($project, '3.3.2'); + $path = $project->sourcePath('3.3.2'); + $this->addLegacyComposerMetadata($path, false); + $installedPath = $path . '/vendor/composer/installed.json'; + $metadata = file_get_contents($installedPath); + + (new TestSourceProvider($project))->ensure('3.3.2'); + + self::assertSame($metadata, file_get_contents($installedPath)); + } + + public function testEnsureRejectsUnsupportedFallbackMetadata(): void + { + $project = $this->project(); + $this->addDownloadedSource($project, '3.3.2'); + $path = $project->sourcePath('3.3.2'); + $this->addLegacyComposerMetadata($path); + file_put_contents($path . '/vendor/composer/installed.json', '{}'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unsupported Composer metadata format'); + (new TestSourceProvider($project))->ensure('3.3.2'); + } + public function testAddGitSourceAcceptsCloneUrlEndingInGit(): void { $project = $this->project(); @@ -265,6 +342,22 @@ private function addDownloadedSource(Project $project, string $key, string $phpR ]; $project->writeJson('sources.json', $sources); } + + private function addLegacyComposerMetadata(string $path, bool $fallback = true): void + { + $versionsDirectory = $path . '/vendor/ocramius/package-versions/src/PackageVersions'; + $composerDirectory = $path . '/vendor/composer'; + mkdir($versionsDirectory, 0775, true); + mkdir($composerDirectory, 0775, true); + $versions = $fallback ? '[]' : "['phpbb/phpbb' => '3.3.2']"; + file_put_contents($versionsDirectory . '/Versions.php', " [ + ['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'], + ], + 'dev' => true, + ])); + } } class TestSourceProvider extends SourceProvider @@ -296,6 +389,43 @@ protected function capture(array $command, string $cwd): array } } +class CompatibilitySourceProvider extends TestSourceProvider +{ + protected function run(array $command, string $cwd): void + { + parent::run($command, $cwd); + if (($command[1] ?? '') === 'create-project') + { + $this->addLegacyComposerMetadata($command[3]); + } + else if (($command[1] ?? '') === 'install') + { + $this->addLegacyComposerMetadata($cwd); + } + } + + private function addLegacyComposerMetadata(string $path): void + { + $versionsDirectory = $path . '/vendor/ocramius/package-versions/src/PackageVersions'; + $composerDirectory = $path . '/vendor/composer'; + if (!is_dir($versionsDirectory)) + { + mkdir($versionsDirectory, 0775, true); + } + if (!is_dir($composerDirectory)) + { + mkdir($composerDirectory, 0775, true); + } + file_put_contents($versionsDirectory . '/Versions.php', ' [ + ['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'], + ], + 'dev' => true, + ])); + } +} + class RefreshingSourceProvider extends SourceProvider { private int $refresh = 0;