feat(compiler): add PHP extension dependencies support

- Add extension-dependencies configuration option to project YAML
- Generate ZEND_MOD_REQUIRED entries for specified PHP modules
- Implement zend_module_dep array generation in extension output
- Add getExtensionDependencies method to CompilerBase class
- Validate extension-dependencies as array type in parser
- Support duplicate dependency filtering in configuration
- Document PHP extension dependencies feature in compiler CLI docs
master
韩天峰 19 hours ago
parent 02fc0fb319
commit 1118c1e8ec
  1. 12
      docs/COMPILER_CLI.md
  2. 56
      phpunit/src/CompilerBaseApiTest.php
  3. 8
      src/CompilerBase.php
  4. 38
      src/Translator.php

@ -117,6 +117,18 @@ TypePHP 和 PHPX 的最低运行时版本均为 PHP 8.4。`--php-version` 与实
传入 `project.yml` 时,命令行参数优先于 YAML 中的同名配置。项目文件格式参见用户文档及代码中的项目配置解析器。
### PHP 扩展依赖
程序依赖其他 PHP 扩展时,可以将必需模块写入 Zend 模块依赖表:
```yaml
extension-dependencies:
- pdo_mysql
- curl
```
编译器会为每一项生成 `ZEND_MOD_REQUIRED`。Zend 在加载 TypePHP 模块时检查这些扩展是否已加载。该配置不表示原生链接库;C/C++ 链接依赖仍使用 `link-libs`
## 查看权威帮助
命令行实现可能继续演进,发布版本的实际参数以以下命令为准:

@ -431,6 +431,10 @@ link-libs:
link-paths:
- /usr/local/lib
- /opt/custom/lib
extension-dependencies:
- pdo_mysql
- curl
- curl
YAML);
$this->invokeMethod('parseProjectYaml', $projectFile);
@ -449,10 +453,62 @@ YAML);
$this->assertTrue($this->compiler->isLtoEnabled());
$this->assertSame(['curl', 'ssl'], $this->compiler->getLinkLibs());
$this->assertSame(['/usr/local/lib', '/opt/custom/lib'], $this->compiler->getLinkPaths());
$this->assertSame(['pdo_mysql', 'curl'], $this->compiler->getExtensionDependencies());
$this->assertSame('/tmp/project-build', $this->compiler->getBuildDir());
$this->assertTrue($this->getPropertyValue('formatCode'));
}
public function testParseProjectYamlRejectsInvalidExtensionDependencies(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
extension-dependencies: curl
YAML);
$this->expectException(TestError::class);
$this->expectExceptionMessage('`extension-dependencies` must be array');
$this->invokeMethod('parseProjectYaml', $projectFile);
}
public function testExtensionDependenciesAreWrittenToZendModuleEntry(): void
{
global $translator;
$translator = $this->compiler;
$this->compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT);
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
extension-dependencies:
- pdo_mysql
- curl
YAML);
$files = $this->invokeMethod('parseProjectYaml', $projectFile);
$this->compiler->addFiles($files);
foreach ($files as $file) {
$this->compiler->prepareFile($file);
$this->compiler->convertFile($file);
}
$extension = file_get_contents($this->compiler->genExtension());
$this->assertStringContainsString(
"static const zend_module_dep typephp_app_module_deps[] = {\n"
. " ZEND_MOD_REQUIRED(\"pdo_mysql\")\n"
. " ZEND_MOD_REQUIRED(\"curl\")\n"
. " ZEND_MOD_END\n"
. '};',
$extension,
);
$this->assertStringContainsString(
"zend_module_entry typephp_app_module_entry = {\n"
. " STANDARD_MODULE_HEADER_EX,\n"
. " nullptr,\n"
. " typephp_app_module_deps,",
$extension,
);
}
public function testParseProjectYamlSupportsCustomFilenameAndRelativeBuildDir(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'

@ -373,6 +373,8 @@ class CompilerBase implements PropertyAccessContext
protected string $ldflags = '';
protected array $linkLibs = []; // --link-lib / -l: user-specified libraries to link
protected array $linkPaths = []; // --link-path / -L: user-specified library search paths
/** @var list<string> Required PHP modules recorded in zend_module_entry.deps. */
protected array $extensionDependencies = [];
protected int $floatPrecision = 17;
protected bool $debug = false;
protected bool $formatCode = false; // --format: enable clang-format (disabled by default)
@ -997,6 +999,12 @@ class CompilerBase implements PropertyAccessContext
return $this->linkPaths;
}
/** @return list<string> */
public function getExtensionDependencies(): array
{
return $this->extensionDependencies;
}
public function getMarch(): string
{
return $this->march;

@ -1243,9 +1243,24 @@ PHP_RSHUTDOWN_FUNCTION({$moduleName}) {
module_clean();
return SUCCESS;
}
CODE;
if ($this->extensionDependencies === []) {
$moduleHeader = ' STANDARD_MODULE_HEADER,';
} else {
$dependencyArray = $moduleName . '_module_deps';
$code .= PHP_EOL . 'static const zend_module_dep ' . $dependencyArray . '[] = {' . PHP_EOL;
foreach ($this->extensionDependencies as $dependency) {
$code .= ' ZEND_MOD_REQUIRED(' . $this->genCharPtr($dependency, true) . ')' . PHP_EOL;
}
$code .= ' ZEND_MOD_END' . PHP_EOL . '};' . PHP_EOL;
$moduleHeader = " STANDARD_MODULE_HEADER_EX,\n nullptr,\n {$dependencyArray},";
}
$code .= <<<CODE
zend_module_entry {$moduleName}_module_entry = {
STANDARD_MODULE_HEADER,
{$moduleHeader}
"{$moduleName}",
ext_functions,
PHP_MINIT({$moduleName}),
@ -2563,6 +2578,27 @@ CODE;
}
}
// Required PHP modules. These are emitted as zend_module_dep entries;
// they are unrelated to native libraries configured through link-libs.
if (array_key_exists('extension-dependencies', $cfg)) {
$dependencies = $cfg['extension-dependencies'];
if (!is_array($dependencies)) {
$this->error('`extension-dependencies` must be array');
}
foreach ($dependencies as $dependency) {
if (!is_string($dependency) || trim($dependency) === '') {
$this->error('Each `extension-dependencies` entry must be a non-empty string');
}
$dependency = trim($dependency);
if (str_contains($dependency, "\0")) {
$this->error('Extension dependency names must not contain NUL bytes');
}
if (!in_array($dependency, $this->extensionDependencies, true)) {
$this->extensionDependencies[] = $dependency;
}
}
}
// 读取 output/name。name 只表示目标名,不能按 YAML 目录解析成输出路径。
$output = $cfg['output'] ?? null;
if (!empty($output)) {

Loading…
Cancel
Save