TypePHP 编译器 https://swoole.com/aot/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

317 lines
13 KiB

<?php
namespace TypePhp\Build;
use Composer\InstalledVersions;
use RuntimeException;
/** A Composer package that contributes C or C++ sources to a TypePHP native target. */
final readonly class ComposerNativePackage
{
/**
* @param list<string> $includeDirs
* @param list<string> $sources
* @param array<string, ComposerNativeComponent> $components
*/
private function __construct(
public string $name,
public string $installPath,
public string $kind,
public int $abi,
public int $cStandard,
public int $cxxStandard,
public array $includeDirs,
public array $sources,
public array $components,
public ?string $extensionName,
public ?string $extensionModuleEntry,
) {
}
/** @return list<self> */
public static function discover(?string $compilerRoot = null): array
{
$packages = [];
foreach (InstalledVersions::getInstalledPackages() as $package) {
$installPath = InstalledVersions::getInstallPath($package);
if (!is_string($installPath)) {
continue;
}
$manifestPath = $installPath . DIRECTORY_SEPARATOR . 'composer.json';
$manifest = json_decode((string) @file_get_contents($manifestPath), true);
if (!is_array($manifest)
|| !is_array($manifest['extra']['typephp-native'] ?? null)) {
continue;
}
$packages[] = self::load($package, $compilerRoot);
}
usort(
$packages,
static function (self $left, self $right): int {
$priority = ['runtime' => 0, 'bridge' => 1, 'library' => 2, 'extension' => 3];
return [$priority[$left->kind], $left->name]
<=> [$priority[$right->kind], $right->name];
},
);
return $packages;
}
public static function load(string $package, ?string $compilerRoot = null): self
{
if ($package === 'swoole/php-ext-standard') {
throw new RuntimeException(
'The standard extension is built into swoole/php-nano; remove obsolete package `swoole/php-ext-standard`'
);
}
$root = self::resolveLocalPackage($package, $compilerRoot);
if ($root === null) {
$installPath = class_exists(InstalledVersions::class) && InstalledVersions::isInstalled($package)
? InstalledVersions::getInstallPath($package)
: null;
$resolvedInstallPath = is_string($installPath) ? realpath($installPath) : false;
$root = $resolvedInstallPath !== false ? $resolvedInstallPath : null;
}
if ($root === null) {
throw new RuntimeException(
"Native dependency `{$package}` is not installed; run `composer require {$package}`"
);
}
if (!is_dir($root)) {
throw new RuntimeException("Unable to resolve Composer install path for `{$package}`");
}
$manifestPath = $root . DIRECTORY_SEPARATOR . 'composer.json';
$manifest = json_decode((string) file_get_contents($manifestPath), true);
$native = is_array($manifest) ? ($manifest['extra']['typephp-native'] ?? null) : null;
if (!is_array($native)) {
throw new RuntimeException(
"Composer package `{$package}` does not publish extra.typephp-native"
);
}
$kind = $native['kind'] ?? 'library';
if (!is_string($kind) || !in_array($kind, ['runtime', 'bridge', 'extension', 'library'], true)) {
throw new RuntimeException("Invalid native package kind in `{$package}`");
}
$extensionName = null;
$extensionModuleEntry = null;
if ($kind === 'extension') {
if (!str_starts_with($package, 'swoole/php-ext-')) {
throw new RuntimeException(
"Native extension package `{$package}` must use the `swoole/php-ext-*` naming convention"
);
}
$extension = $native['extension'] ?? null;
$extensionName = is_array($extension) ? ($extension['name'] ?? null) : null;
$extensionModuleEntry = is_array($extension) ? ($extension['module-entry'] ?? null) : null;
if (!is_string($extensionName)
|| preg_match('/^[a-z][a-z0-9_]*$/', $extensionName) !== 1
|| !is_string($extensionModuleEntry)
|| preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $extensionModuleEntry) !== 1) {
throw new RuntimeException("Invalid native extension metadata in `{$package}`");
}
$expectedPackage = 'swoole/php-ext-' . str_replace('_', '-', $extensionName);
if ($package !== $expectedPackage) {
throw new RuntimeException(
"Native extension `{$extensionName}` must use Composer package `{$expectedPackage}`"
);
}
$runtimeConstraint = $manifest['require']['swoole/php-nano'] ?? null;
if (!is_string($runtimeConstraint) || $runtimeConstraint === '') {
throw new RuntimeException(
"Native extension package `{$package}` must require `swoole/php-nano`"
);
}
}
$abi = $native['abi'] ?? null;
$cStandard = $native['c-standard'] ?? null;
$cxxStandard = $native['cxx-standard'] ?? null;
if (!is_int($abi) || !is_int($cStandard) || !is_int($cxxStandard)) {
throw new RuntimeException("Invalid native ABI metadata in `{$package}`");
}
if ($cStandard !== 11) {
throw new RuntimeException(
"Native dependency `{$package}` requires unsupported C{$cStandard}; expected C11"
);
}
if ($cxxStandard !== 17) {
throw new RuntimeException(
"Native dependency `{$package}` requires unsupported C++{$cxxStandard}; expected C++17"
);
}
$sources = self::resolveEntries($root, $native['sources'] ?? null, false, $package);
foreach ($sources as $source) {
$extension = strtolower(pathinfo($source, PATHINFO_EXTENSION));
if (!in_array($extension, ['c', 'cc', 'cpp', 'cxx'], true)) {
throw new RuntimeException(
"Native package `{$package}` contains unsupported source type: {$source}"
);
}
}
$components = [];
foreach (($native['components'] ?? []) as $componentName => $component) {
if (!is_string($componentName)
|| preg_match('/^[a-z][a-z0-9_.-]*$/', $componentName) !== 1
|| !is_array($component)) {
throw new RuntimeException("Invalid native component metadata in `{$package}`");
}
$componentExtension = $component['extension'] ?? strstr($componentName, '.', true);
if ($componentExtension === false) {
$componentExtension = $componentName;
}
if (!is_string($componentExtension)
|| preg_match('/^[a-z][a-z0-9_]*$/', $componentExtension) !== 1) {
throw new RuntimeException("Invalid extension name for component `{$componentName}`");
}
$componentSourceEntries = $component['sources'] ?? [];
if (!is_array($componentSourceEntries)) {
throw new RuntimeException(
"Invalid source list in component `{$package}:{$componentName}`"
);
}
$componentSources = $componentSourceEntries === []
? []
: self::resolveEntries(
$root,
$componentSourceEntries,
false,
"{$package}:{$componentName}",
);
foreach ($componentSources as $source) {
if (!in_array($source, $sources, true)) {
throw new RuntimeException(
"Component `{$package}:{$componentName}` owns a source not published by the package"
);
}
}
$defines = self::stringList($component['defines'] ?? [], 'define', $package, $componentName);
foreach ($defines as $define) {
if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*(?:=.*)?$/', $define) !== 1) {
throw new RuntimeException("Invalid define in component `{$package}:{$componentName}`");
}
}
$requires = self::stringList($component['requires'] ?? [], 'requirement', $package, $componentName);
$moduleEntry = $component['module-entry'] ?? null;
if ($moduleEntry !== null
&& (!is_string($moduleEntry)
|| preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $moduleEntry) !== 1)) {
throw new RuntimeException("Invalid module entry in component `{$package}:{$componentName}`");
}
$components[$componentName] = new ComposerNativeComponent(
$componentName,
$componentExtension,
$componentSources,
$defines,
$requires,
$moduleEntry,
);
}
return new self(
$package,
$root,
$kind,
$abi,
$cStandard,
$cxxStandard,
self::resolveEntries($root, $native['include-dirs'] ?? null, true, $package),
$sources,
$components,
$extensionName,
$extensionModuleEntry,
);
}
/** @return list<string> */
private static function stringList(
mixed $values,
string $label,
string $package,
string $component,
): array {
if (!is_array($values)) {
throw new RuntimeException("Invalid {$label} list in component `{$package}:{$component}`");
}
foreach ($values as $value) {
if (!is_string($value) || $value === '') {
throw new RuntimeException("Invalid {$label} in component `{$package}:{$component}`");
}
}
return array_values(array_unique($values));
}
/**
* Composer installs all three core packages as siblings below vendor/swoole.
* The same layout is used by the monorepo checkout, where a package may not
* yet be present in the checkout's generated InstalledVersions metadata.
*/
private static function resolveLocalPackage(string $package, ?string $compilerRoot): ?string
{
if ($compilerRoot === null || !str_starts_with($package, 'swoole/')) {
return null;
}
$packageDirectory = substr($package, strlen('swoole/'));
$candidates = [
$compilerRoot . '/' . $packageDirectory,
$compilerRoot . '/vendor/swoole/' . $packageDirectory,
dirname($compilerRoot) . '/' . $packageDirectory,
];
foreach ($candidates as $path) {
$candidate = realpath($path);
if ($candidate === false || !is_dir($candidate)) {
continue;
}
$manifest = json_decode((string) @file_get_contents($candidate . '/composer.json'), true);
if (is_array($manifest) && ($manifest['name'] ?? null) === $package) {
return $candidate;
}
}
return null;
}
/** @return list<string> */
private static function resolveEntries(
string $root,
mixed $entries,
bool $directories,
string $package,
): array {
if (!is_array($entries) || $entries === []) {
throw new RuntimeException("Native source metadata is empty in `{$package}`");
}
$resolved = [];
foreach ($entries as $entry) {
if (!is_string($entry) || $entry === '' || self::isAbsolutePath($entry)) {
throw new RuntimeException("Invalid native source path in `{$package}`");
}
$path = realpath($root . DIRECTORY_SEPARATOR . $entry);
$validType = $path !== false && ($directories ? is_dir($path) : is_file($path));
if (!$validType || !self::isInside($root, (string) $path)) {
throw new RuntimeException(
"Native source entry `{$entry}` is missing or escapes package `{$package}`"
);
}
$resolved[] = $path;
}
return $resolved;
}
private static function isInside(string $root, string $path): bool
{
return $path === $root
|| str_starts_with($path, rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR);
}
private static function isAbsolutePath(string $path): bool
{
return $path[0] === '/' || $path[0] === '\\'
|| preg_match('/^[A-Za-z]:[\\\\\/]/', $path) === 1;
}
}