feat(compiler): add extension metadata support with version and info fields

- Add version field to expose extension version through Zend module entry
- Add info mapping to accept arbitrary labels and scalar values for phpinfo()
- Generate PHP_MINFO_FUNCTION to display custom extension information
- Preserve info mapping order in module's phpinfo() section
- Validate version as non-empty string without NUL bytes
- Validate info as mapping of non-empty string labels to scalar values
- Add comprehensive tests for metadata parsing and generation
- Update documentation with extension metadata configuration examples
master
韩天峰 3 weeks ago
parent db305c11f5
commit cf1f6fb97d
  1. 6
      README-CN.md
  2. 6
      README.md
  3. 19
      docs/en/COMPILER_CLI.md
  4. 18
      docs/zh-cn/COMPILER_CLI.md
  5. 12
      examples/myext/project.yml
  6. 91
      phpunit/src/CompilerBaseApiTest.php
  7. 3
      src/CompilerBase.php
  8. 53
      src/Translator.php

@ -290,6 +290,10 @@ bin/tpc.php lib/ -m lib -o mylib
```yaml ```yaml
name: myapp name: myapp
mode: bin mode: bin
version: 1.0.0
info:
Author: TypePHP Team
Description: My TypePHP application
php-version: "8.5" php-version: "8.5"
optimize: 2 optimize: 2
job: 8 job: 8
@ -337,6 +341,8 @@ ext-deps:
`PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML `PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML
中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成 中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成
`ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。 `ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。
`version` 用于设置 Zend 模块版本。`info` 映射可配置任意标签和值,并显示在模块
独立的 `phpinfo()` 区块中。
`embedded-files` 支持与 `sources` 相同的文件、目录及条件写法,仅在显式配置时启用。 `embedded-files` 支持与 `sources` 相同的文件、目录及条件写法,仅在显式配置时启用。
所列文件全部打包进二进制;未通过 `sources` 成功原生编译的 PHP 文件由 OPcache 所列文件全部打包进二进制;未通过 `sources` 成功原生编译的 PHP 文件由 OPcache
生成字节码。用于 API 声明的 `.stub.php` 文件仍保留在原始文件包中,不生成可执行字节码。 生成字节码。用于 API 声明的 `.stub.php` 文件仍保留在原始文件包中,不生成可执行字节码。

@ -322,6 +322,10 @@ For multi-file projects, keep repeatable build settings in `project.yml`:
```yaml ```yaml
name: myapp name: myapp
mode: bin mode: bin
version: 1.0.0
info:
Author: TypePHP Team
Description: My TypePHP application
php-version: "8.5" php-version: "8.5"
optimize: 2 optimize: 2
job: 8 job: 8
@ -370,6 +374,8 @@ directory; conditional entries support `PHP_VERSION`, `PHP_VERSION_ID`, and
`PHP_OS_FAMILY`. CLI arguments override their YAML counterparts. Native linker `PHP_OS_FAMILY`. CLI arguments override their YAML counterparts. Native linker
dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED` dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED`
entries so Zend can reject loading when a required PHP extension is missing. entries so Zend can reject loading when a required PHP extension is missing.
`version` provides the Zend module version. The `info` mapping accepts arbitrary
labels and values for the module's dedicated `phpinfo()` section.
`embedded-files` accepts files or directories with the same conditional syntax. `embedded-files` accepts files or directories with the same conditional syntax.
It is opt-in for embedded binary builds: all listed files are packed into the It is opt-in for embedded binary builds: all listed files are packed into the
binary, and PHP files not successfully compiled from `sources` are stored as binary, and PHP files not successfully compiled from `sources` are stored as

@ -139,6 +139,25 @@ project-owned packaging step after tpc emits its ELF.
Use `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` for project-wide C++, Use `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` for project-wide C++,
C, assembler, and linker options. C, assembler, and linker options.
### Extension metadata
An extension project can declare metadata in `project.yml`:
```yaml
name: my_extension
mode: ext
version: 1.0.0
info:
Author: Example Team
Description: Example native extension
License: Apache-2.0
```
`version` is exposed through the Zend module entry, including
`ReflectionExtension::getVersion()`. The `info` mapping accepts arbitrary row
labels and scalar values. TypePHP preserves their order in the module's
dedicated `phpinfo()` section.
### PHP Extension Dependencies ### PHP Extension Dependencies
When a program depends on other PHP extensions, the required modules can be written into the Zend module dependency table: When a program depends on other PHP extensions, the required modules can be written into the Zend module dependency table:

@ -136,6 +136,24 @@ tpc 产出 ELF 后由项目自己的构建流程另行封装。
使用 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别设置项目级 使用 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别设置项目级
C++、C、汇编和链接参数。 C++、C、汇编和链接参数。
### 扩展元数据
扩展项目可以在 `project.yml` 中声明元数据:
```yaml
name: my_extension
mode: ext
version: 1.0.0
info:
Author: Example Team
Description: Example native extension
License: Apache-2.0
```
`version` 会写入 Zend 模块入口,并可通过 `ReflectionExtension::getVersion()` 获取。
`info` 映射接受任意行标签和标量值,TypePHP 会保持配置顺序,并将它们显示在模块独立的
`phpinfo()` 区块中。
### PHP 扩展依赖 ### PHP 扩展依赖
程序依赖其他 PHP 扩展时,可以将必需模块写入 Zend 模块依赖表: 程序依赖其他 PHP 扩展时,可以将必需模块写入 Zend 模块依赖表:

@ -0,0 +1,12 @@
name: myext
type: ext
sources:
- ./test.php
info:
version: 1.1.3
author: Tianfeng.Han
email: rango@swoole.com

@ -559,6 +559,10 @@ extension-dependencies:
- pdo_mysql - pdo_mysql
- curl - curl
- curl - curl
version: 1.2.3
info:
Author: TypePHP Team
Description: Native PHP extension
YAML); YAML);
$this->invokeMethod('parseProjectYaml', $projectFile); $this->invokeMethod('parseProjectYaml', $projectFile);
@ -578,10 +582,29 @@ YAML);
$this->assertSame(['curl', 'ssl'], $this->compiler->getLinkLibs()); $this->assertSame(['curl', 'ssl'], $this->compiler->getLinkLibs());
$this->assertSame(['/usr/local/lib', '/opt/custom/lib'], $this->compiler->getLinkPaths()); $this->assertSame(['/usr/local/lib', '/opt/custom/lib'], $this->compiler->getLinkPaths());
$this->assertSame(['pdo_mysql', 'curl'], $this->compiler->getExtensionDependencies()); $this->assertSame(['pdo_mysql', 'curl'], $this->compiler->getExtensionDependencies());
$this->assertSame('1.2.3', $this->getPropertyValue('extensionVersion'));
$this->assertSame([
'Author' => 'TypePHP Team',
'Description' => 'Native PHP extension',
], $this->getPropertyValue('extensionInfo'));
$this->assertSame('/tmp/project-build', $this->compiler->getBuildDir()); $this->assertSame('/tmp/project-build', $this->compiler->getBuildDir());
$this->assertTrue($this->getPropertyValue('formatCode')); $this->assertTrue($this->getPropertyValue('formatCode'));
} }
public function testParseProjectYamlRejectsInvalidExtensionMetadata(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
info:
- TypePHP Team
YAML);
$this->expectException(TestError::class);
$this->expectExceptionMessage('`info` must be a mapping of labels to values');
$this->invokeMethod('parseProjectYaml', $projectFile);
}
public function testParseProjectYamlRejectsInvalidExtensionDependencies(): void public function testParseProjectYamlRejectsInvalidExtensionDependencies(): void
{ {
$projectFile = $this->createProjectFile(<<<'YAML' $projectFile = $this->createProjectFile(<<<'YAML'
@ -667,6 +690,74 @@ YAML);
); );
} }
public function testExtensionMetadataIsWrittenToModuleEntryAndPhpInfo(): void
{
global $translator;
$translator = $this->compiler;
$this->compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT);
$projectFile = $this->createProjectFile(<<<'YAML'
name: metadata_demo
sources:
- main.php
version: 2.4.1
info:
Maintainer: 'TypePHP "Core" Team'
Description: 'Native PHP\C++ extension'
Stable: true
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('PHP_MINFO_FUNCTION(typephp_metadata_demo)', $extension);
$this->assertStringContainsString(
'php_info_print_table_header(2, "typephp_metadata_demo support", "enabled");',
$extension,
);
$this->assertStringContainsString('php_info_print_table_row(2, "Maintainer", "TypePHP \\"Core\\" Team");', $extension);
$this->assertStringContainsString('php_info_print_table_row(2, "Description", "Native PHP\\\\C++ extension");', $extension);
$this->assertStringContainsString('php_info_print_table_row(2, "Stable", "true");', $extension);
$this->assertStringContainsString(
" PHP_MINFO(typephp_metadata_demo),\n"
. " \"2.4.1\",\n"
. ' STANDARD_MODULE_PROPERTIES,',
$extension,
);
}
public function testExtensionWithoutMetadataKeepsNullInfoAndVersion(): void
{
global $translator;
$translator = $this->compiler;
$this->compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT);
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
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->assertStringNotContainsString('PHP_MINFO_FUNCTION(typephp_app)', $extension);
$this->assertStringContainsString(
" PHP_RSHUTDOWN(typephp_app),\n"
. " nullptr,\n"
. " nullptr,\n"
. ' STANDARD_MODULE_PROPERTIES,',
$extension,
);
}
public function testInternalSymbolExtensionsAreDetectedAsModuleDependencies(): void public function testInternalSymbolExtensionsAreDetectedAsModuleDependencies(): void
{ {
if (!extension_loaded('mbstring')) { if (!extension_loaded('mbstring')) {

@ -523,6 +523,9 @@ class CompilerBase implements PropertyAccessContext
protected array $projectObjectFiles = []; protected array $projectObjectFiles = [];
/** @var list<string> Required PHP modules recorded in zend_module_entry.deps. */ /** @var list<string> Required PHP modules recorded in zend_module_entry.deps. */
protected array $extensionDependencies = []; protected array $extensionDependencies = [];
protected string $extensionVersion = '';
/** @var array<string, string> Custom rows shown in the module's phpinfo section. */
protected array $extensionInfo = [];
protected bool $debug = false; protected bool $debug = false;
protected bool $formatCode = false; // --format: enable clang-format (disabled by default) protected bool $formatCode = false; // --format: enable clang-format (disabled by default)
protected bool $printBacktraceOnError = true; protected bool $printBacktraceOnError = true;

@ -1775,6 +1775,25 @@ PHP_RSHUTDOWN_FUNCTION({$moduleName}) {
} }
CODE; CODE;
$moduleInfoFunction = 'nullptr';
$moduleVersion = $this->extensionVersion === ''
? 'nullptr'
: $this->genCharPtr($this->extensionVersion, true);
if ($this->extensionVersion !== '' || $this->extensionInfo !== []) {
$moduleInfoFunction = 'PHP_MINFO(' . $moduleName . ')';
$code .= PHP_EOL . 'PHP_MINFO_FUNCTION(' . $moduleName . ') {' . PHP_EOL;
$code .= ' php_info_print_table_start();' . PHP_EOL;
$code .= ' php_info_print_table_header(2, '
. $this->genCharPtr($moduleName . ' support', true) . ', "enabled");' . PHP_EOL;
foreach ($this->extensionInfo as $label => $value) {
$code .= ' php_info_print_table_row(2, '
. $this->genCharPtr($label, true) . ', '
. $this->genCharPtr($value, true) . ');' . PHP_EOL;
}
$code .= ' php_info_print_table_end();' . PHP_EOL;
$code .= '}' . PHP_EOL;
}
$extensionDependencies = $this->resolveExtensionDependencies(); $extensionDependencies = $this->resolveExtensionDependencies();
if ($extensionDependencies === []) { if ($extensionDependencies === []) {
$moduleHeader = ' STANDARD_MODULE_HEADER,'; $moduleHeader = ' STANDARD_MODULE_HEADER,';
@ -1798,8 +1817,8 @@ zend_module_entry {$moduleName}_module_entry = {
PHP_MSHUTDOWN({$moduleName}), PHP_MSHUTDOWN({$moduleName}),
PHP_RINIT({$moduleName}), PHP_RINIT({$moduleName}),
PHP_RSHUTDOWN({$moduleName}), PHP_RSHUTDOWN({$moduleName}),
nullptr, {$moduleInfoFunction},
nullptr, {$moduleVersion},
STANDARD_MODULE_PROPERTIES, STANDARD_MODULE_PROPERTIES,
}; };
CODE; CODE;
@ -3894,6 +3913,36 @@ CODE;
$cfg = $this->getProjectYamlLoader()->load($path); $cfg = $this->getProjectYamlLoader()->load($path);
$projectDir = dirname($path); $projectDir = dirname($path);
if (array_key_exists('version', $cfg)) {
if (!is_string($cfg['version']) || trim($cfg['version']) === '') {
$this->error('`version` must be a non-empty string');
}
$this->extensionVersion = trim($cfg['version']);
if (str_contains($this->extensionVersion, "\0")) {
$this->error('`version` must not contain NUL bytes');
}
}
if (array_key_exists('info', $cfg)) {
if (!is_array($cfg['info']) || ($cfg['info'] !== [] && array_is_list($cfg['info']))) {
$this->error('`info` must be a mapping of labels to values');
}
foreach ($cfg['info'] as $label => $value) {
if (!is_string($label) || trim($label) === '') {
$this->error('Each `info` label must be a non-empty string');
}
if (!is_scalar($value)) {
$this->error("The `info` value for `{$label}` must be a scalar");
}
$label = trim($label);
$value = is_bool($value) ? ($value ? 'true' : 'false') : (string) $value;
if (str_contains($label, "\0") || str_contains($value, "\0")) {
$this->error('`info` labels and values must not contain NUL bytes');
}
$this->extensionInfo[$label] = $value;
}
}
$objects = $cfg['objects'] ?? []; $objects = $cfg['objects'] ?? [];
if (!is_array($objects)) { if (!is_array($objects)) {
$this->error('`objects` must be an array'); $this->error('`objects` must be an array');

Loading…
Cancel
Save