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
75 changes: 75 additions & 0 deletions src/QuickInstall/Sandbox/ComposerMetadataCompatibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/**
*
* QuickInstall CLI
*
* @copyright (c) 2026 phpBB Limited <https://www.phpbb.com>
* @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;
}
}
3 changes: 3 additions & 0 deletions src/QuickInstall/Sandbox/SourceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -385,6 +386,7 @@ public function fetch(array $source): void
}

$this->run($command, dirname($path));
ComposerMetadataCompatibility::normalizePackageVersions($path);
}

protected function normalizeGitSourceRoot(string $path): void
Expand Down Expand Up @@ -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)
{
Expand Down
1 change: 1 addition & 0 deletions src/QuickInstall/Sandbox/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
130 changes: 130 additions & 0 deletions tests/Unit/SourceProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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', "<?php final class Versions { const VERSIONS = $versions; }");
file_put_contents($composerDirectory . '/installed.json', json_encode([
'packages' => [
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
],
'dev' => true,
]));
}
}

class TestSourceProvider extends SourceProvider
Expand Down Expand Up @@ -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', '<?php final class Versions { const VERSIONS = []; }');
file_put_contents($composerDirectory . '/installed.json', json_encode([
'packages' => [
['name' => 'ocramius/proxy-manager', 'version' => '2.1.1'],
],
'dev' => true,
]));
}
}

class RefreshingSourceProvider extends SourceProvider
{
private int $refresh = 0;
Expand Down