mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-14 20:35:35 +08:00
feat(manifest): implement BuildManifestDumper for generating build manifest (#1210)
This commit is contained in:
3
.github/workflows/build-unix.yml
vendored
3
.github/workflows/build-unix.yml
vendored
@@ -331,5 +331,4 @@ jobs:
|
||||
with:
|
||||
name: build-meta-${{ inputs.php-version }}-${{ inputs.os }}
|
||||
path: |
|
||||
buildroot/build-extensions.json
|
||||
buildroot/build-libraries.json
|
||||
buildroot/build-manifest.json
|
||||
|
||||
3
.github/workflows/build-windows-x86_64.yml
vendored
3
.github/workflows/build-windows-x86_64.yml
vendored
@@ -115,5 +115,4 @@ jobs:
|
||||
with:
|
||||
name: build-meta
|
||||
path: |
|
||||
buildroot/build-extensions.json
|
||||
buildroot/build-libraries.json
|
||||
buildroot/build-manifest.json
|
||||
|
||||
@@ -328,6 +328,49 @@ class php extends TargetPackage
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function getBuildManifestData(PackageInstaller $installer): array
|
||||
{
|
||||
if ($this->getName() !== 'php') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$all_extensions = [];
|
||||
$static_extensions = [];
|
||||
$shared_extensions = [];
|
||||
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $extension) {
|
||||
$extension_name = $extension->getExtensionName();
|
||||
if ($extension->isBuildStatic() || $extension->isBuildShared()) {
|
||||
$all_extensions[] = $extension_name;
|
||||
}
|
||||
if ($extension->isBuildStatic()) {
|
||||
$static_extensions[] = $extension_name;
|
||||
}
|
||||
if ($extension->isBuildShared()) {
|
||||
$shared_extensions[] = $extension_name;
|
||||
}
|
||||
}
|
||||
|
||||
$sapis = array_values(array_filter([
|
||||
$installer->isPackageResolved('php-cli') ? 'cli' : null,
|
||||
$installer->isPackageResolved('php-fpm') ? 'fpm' : null,
|
||||
$installer->isPackageResolved('php-micro') ? 'micro' : null,
|
||||
$installer->isPackageResolved('php-cgi') ? 'cgi' : null,
|
||||
$installer->isPackageResolved('php-embed') ? 'embed' : null,
|
||||
$installer->isPackageResolved('frankenphp') ? 'frankenphp' : null,
|
||||
]));
|
||||
|
||||
return [
|
||||
'version' => static::getPHPVersion(return_null_if_failed: true),
|
||||
'thread-safety' => $this->getBuildOption('enable-zts', false) ? 'zts' : 'nts',
|
||||
'sapis' => $sapis,
|
||||
'extensions' => [
|
||||
'all' => $all_extensions,
|
||||
'static' => $static_extensions,
|
||||
'shared' => $shared_extensions,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[BeforeStage('php', 'build')]
|
||||
public function beforeBuild(PackageBuilder $builder, Package $package): void
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ use StaticPHP\Exception\EnvironmentException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\BuildManifestDumper;
|
||||
use StaticPHP\Util\BuildRootTracker;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\DirDiff;
|
||||
@@ -282,6 +283,11 @@ class PackageInstaller
|
||||
// pipeline above (they were merged into $this->packages in resolvePackages()). This only
|
||||
// throws if a tool genuinely failed to become available (e.g. no artifact for this platform).
|
||||
$this->ensureRequiredTools();
|
||||
|
||||
// Pure install operations must not overwrite the manifest of an existing build.
|
||||
if ($this->build_packages !== []) {
|
||||
new BuildManifestDumper()->dump($this->packages, $this->build_packages, $this->install_packages, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function isBuildPackage(Package|string $package): bool
|
||||
|
||||
@@ -117,6 +117,19 @@ class TargetPackage extends LibraryPackage
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target-specific structured data for the build manifest.
|
||||
*
|
||||
* Target implementations may override this method to expose build facts that
|
||||
* cannot be represented by the generic resolved package list.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getBuildManifestData(PackageInstaller $installer): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all build options for the target package.
|
||||
*
|
||||
|
||||
82
src/StaticPHP/Util/BuildManifestDumper.php
Normal file
82
src/StaticPHP/Util/BuildManifestDumper.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Util;
|
||||
|
||||
use StaticPHP\ConsoleApplication;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Package\Package;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
final class BuildManifestDumper
|
||||
{
|
||||
public const int SCHEMA_VERSION = 1;
|
||||
|
||||
/**
|
||||
* @param array<string, Package> $packages Resolved packages in dependency order
|
||||
* @param array<string, Package> $buildPackages Packages explicitly scheduled for building
|
||||
* @param array<string, Package> $installPackages Packages explicitly scheduled for installation
|
||||
*/
|
||||
public function dump(
|
||||
array $packages,
|
||||
array $buildPackages,
|
||||
array $installPackages,
|
||||
PackageInstaller $installer,
|
||||
string $outputPath = BUILD_ROOT_PATH . '/build-manifest.json',
|
||||
): void {
|
||||
$targets = [];
|
||||
foreach ($packages as $package) {
|
||||
if (!$package instanceof TargetPackage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $package->getBuildManifestData($installer);
|
||||
if ($data !== []) {
|
||||
$targets[$package->getName()] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
$manifest = [
|
||||
'schema-version' => self::SCHEMA_VERSION,
|
||||
'spc-version' => ConsoleApplication::VERSION,
|
||||
'target' => [
|
||||
'platform' => SystemTarget::getCurrentPlatformString(),
|
||||
'os' => SystemTarget::getTargetOS(),
|
||||
'architecture' => SystemTarget::getTargetArch(),
|
||||
'libc' => SystemTarget::getLibc(),
|
||||
'libc-version' => SystemTarget::getLibcVersion(),
|
||||
'spc-target' => getenv('SPC_TARGET') ?: null,
|
||||
'toolchain' => getenv('SPC_TOOLCHAIN') ?: null,
|
||||
],
|
||||
'requested-packages' => [
|
||||
'build' => array_keys($buildPackages),
|
||||
'install' => array_keys($installPackages),
|
||||
],
|
||||
'packages' => array_values(array_map(
|
||||
static function (Package $package): array {
|
||||
$artifact = $package->getArtifact();
|
||||
return [
|
||||
'name' => $package->getName(),
|
||||
'type' => $package->getType(),
|
||||
'artifact' => $artifact?->getName(),
|
||||
];
|
||||
},
|
||||
$packages,
|
||||
)),
|
||||
'targets' => $targets,
|
||||
];
|
||||
|
||||
$json = json_encode(
|
||||
$manifest,
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
|
||||
);
|
||||
if (FileSystem::writeFile($outputPath, $json . PHP_EOL) === false) {
|
||||
throw new SPCInternalException("Failed to write build manifest: {$outputPath}");
|
||||
}
|
||||
|
||||
logger()->info('Generated build manifest with ' . count($packages) . " package(s): {$outputPath}");
|
||||
}
|
||||
}
|
||||
187
tests/StaticPHP/Util/BuildManifestDumperTest.php
Normal file
187
tests/StaticPHP/Util/BuildManifestDumperTest.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\StaticPHP\Util;
|
||||
|
||||
use Package\Target\php as PhpTargetPackage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use StaticPHP\Artifact\Artifact;
|
||||
use StaticPHP\ConsoleApplication;
|
||||
use StaticPHP\Package\Package;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\PhpExtensionPackage;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\BuildManifestDumper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BuildManifestDumperTest extends TestCase
|
||||
{
|
||||
private string $tempDir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->tempDir = sys_get_temp_dir() . '/build_manifest_dumper_test_' . uniqid();
|
||||
mkdir($this->tempDir, 0755, true);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
if (is_dir($this->tempDir)) {
|
||||
$this->removeDirectory($this->tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDumpWritesResolvedPackagesAndBuildContext(): void
|
||||
{
|
||||
$phpArtifact = new Artifact('php-src', ['source' => ['type' => 'url', 'url' => 'https://example.com/php.tar.gz']]);
|
||||
$phpManifestData = [
|
||||
'thread-safety' => 'nts',
|
||||
'extensions' => ['all' => ['curl'], 'static' => ['curl'], 'shared' => []],
|
||||
];
|
||||
$php = $this->createTargetPackage('php', $phpArtifact, $phpManifestData);
|
||||
$openssl = $this->createPackage('openssl', 'library');
|
||||
$curl = $this->createPackage('ext-curl', 'php-extension');
|
||||
$cli = $this->createPackage('php-cli', 'target');
|
||||
$installer = $this->createMock(PackageInstaller::class);
|
||||
|
||||
$outputPath = $this->tempDir . '/nested/build-manifest.json';
|
||||
(new BuildManifestDumper())->dump(
|
||||
[
|
||||
'openssl' => $openssl,
|
||||
'ext-curl' => $curl,
|
||||
'php' => $php,
|
||||
'php-cli' => $cli,
|
||||
],
|
||||
['php' => $php],
|
||||
['php-cli' => $cli],
|
||||
$installer,
|
||||
$outputPath,
|
||||
);
|
||||
|
||||
$this->assertFileExists($outputPath);
|
||||
$manifest = json_decode((string) file_get_contents($outputPath), true, flags: JSON_THROW_ON_ERROR);
|
||||
|
||||
$this->assertSame(BuildManifestDumper::SCHEMA_VERSION, $manifest['schema-version']);
|
||||
$this->assertSame(ConsoleApplication::VERSION, $manifest['spc-version']);
|
||||
$this->assertSame(SystemTarget::getCurrentPlatformString(), $manifest['target']['platform']);
|
||||
$this->assertSame(SystemTarget::getTargetOS(), $manifest['target']['os']);
|
||||
$this->assertSame(SystemTarget::getTargetArch(), $manifest['target']['architecture']);
|
||||
$this->assertSame(['php'], $manifest['requested-packages']['build']);
|
||||
$this->assertSame(['php-cli'], $manifest['requested-packages']['install']);
|
||||
$this->assertSame([
|
||||
['name' => 'openssl', 'type' => 'library', 'artifact' => null],
|
||||
['name' => 'ext-curl', 'type' => 'php-extension', 'artifact' => null],
|
||||
['name' => 'php', 'type' => 'target', 'artifact' => 'php-src'],
|
||||
['name' => 'php-cli', 'type' => 'target', 'artifact' => null],
|
||||
], $manifest['packages']);
|
||||
$this->assertSame(['php' => $phpManifestData], $manifest['targets']);
|
||||
}
|
||||
|
||||
public function testPhpTargetExportsPhpSpecificBuildFacts(): void
|
||||
{
|
||||
$curl = new PhpExtensionPackage('curl', extension_config: ['arg-type' => 'with']);
|
||||
$curl->setBuildStatic();
|
||||
$pdo = new PhpExtensionPackage('pdo', extension_config: ['arg-type' => 'enable']);
|
||||
$pdo->setBuildStatic();
|
||||
$pdo->setBuildShared();
|
||||
$xdebug = new PhpExtensionPackage('xdebug', extension_config: ['arg-type' => 'enable']);
|
||||
$xdebug->setBuildShared();
|
||||
$unused = new PhpExtensionPackage('unused', extension_config: ['arg-type' => 'enable']);
|
||||
|
||||
$installer = $this->createMock(PackageInstaller::class);
|
||||
$installer->method('getResolvedPackages')->willReturn([$curl, $pdo, $xdebug, $unused]);
|
||||
$installer->method('isPackageResolved')->willReturnCallback(
|
||||
static fn (string $name): bool => in_array($name, ['php-cli', 'php-micro', 'php-embed'], true),
|
||||
);
|
||||
|
||||
$php = new class('php', 'target') extends PhpTargetPackage {
|
||||
public static function getPHPVersion(?string $from_custom_source = null, bool $return_null_if_failed = false): ?string
|
||||
{
|
||||
return '8.4.10';
|
||||
}
|
||||
|
||||
public function getBuildOption(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $key === 'enable-zts' ? true : $default;
|
||||
}
|
||||
};
|
||||
|
||||
$data = $php->getBuildManifestData($installer);
|
||||
|
||||
$this->assertSame('8.4.10', $data['version']);
|
||||
$this->assertSame('zts', $data['thread-safety']);
|
||||
$this->assertSame(['cli', 'micro', 'embed'], $data['sapis']);
|
||||
$this->assertSame([
|
||||
'all' => ['curl', 'pdo', 'xdebug'],
|
||||
'static' => ['curl', 'pdo'],
|
||||
'shared' => ['pdo', 'xdebug'],
|
||||
], $data['extensions']);
|
||||
}
|
||||
|
||||
private function createPackage(string $name, string $type, ?Artifact $artifact = null): Package
|
||||
{
|
||||
return new class($name, $type, $artifact) extends Package {
|
||||
public function __construct(string $name, string $type, private readonly ?Artifact $artifact)
|
||||
{
|
||||
parent::__construct($name, $type);
|
||||
}
|
||||
|
||||
public function getArtifact(): ?Artifact
|
||||
{
|
||||
return $this->artifact;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $manifestData */
|
||||
private function createTargetPackage(string $name, ?Artifact $artifact, array $manifestData): TargetPackage
|
||||
{
|
||||
return new class($name, 'target', $artifact, $manifestData) extends TargetPackage {
|
||||
/** @param array<string, mixed> $manifestData */
|
||||
public function __construct(
|
||||
string $name,
|
||||
string $type,
|
||||
private readonly ?Artifact $artifact,
|
||||
private readonly array $manifestData,
|
||||
) {
|
||||
parent::__construct($name, $type);
|
||||
}
|
||||
|
||||
public function getArtifact(): ?Artifact
|
||||
{
|
||||
return $this->artifact;
|
||||
}
|
||||
|
||||
public function getBuildManifestData(PackageInstaller $installer): array
|
||||
{
|
||||
return $this->manifestData;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function removeDirectory(string $directory): void
|
||||
{
|
||||
$items = scandir($directory);
|
||||
if ($items === false) {
|
||||
return;
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
if ($item === '.' || $item === '..') {
|
||||
continue;
|
||||
}
|
||||
$path = $directory . DIRECTORY_SEPARATOR . $item;
|
||||
if (is_dir($path)) {
|
||||
$this->removeDirectory($path);
|
||||
} else {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
rmdir($directory);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user