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
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;
|
|
}
|
|
}
|
|
|