From cf1f6fb97db28c0557bb3df069ffa028519c98fe Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 21 Sep 2026 13:53:10 +0800 Subject: [PATCH] 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 --- README-CN.md | 6 ++ README.md | 6 ++ docs/en/COMPILER_CLI.md | 19 ++++++ docs/zh-cn/COMPILER_CLI.md | 18 ++++++ examples/myext/project.yml | 12 ++++ phpunit/src/CompilerBaseApiTest.php | 91 +++++++++++++++++++++++++++++ src/CompilerBase.php | 3 + src/Translator.php | 53 ++++++++++++++++- 8 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 examples/myext/project.yml diff --git a/README-CN.md b/README-CN.md index a61bbf02..069727fc 100644 --- a/README-CN.md +++ b/README-CN.md @@ -290,6 +290,10 @@ bin/tpc.php lib/ -m lib -o mylib ```yaml name: myapp mode: bin +version: 1.0.0 +info: + Author: TypePHP Team + Description: My TypePHP application php-version: "8.5" optimize: 2 job: 8 @@ -337,6 +341,8 @@ ext-deps: `PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML 中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成 `ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。 +`version` 用于设置 Zend 模块版本。`info` 映射可配置任意标签和值,并显示在模块 +独立的 `phpinfo()` 区块中。 `embedded-files` 支持与 `sources` 相同的文件、目录及条件写法,仅在显式配置时启用。 所列文件全部打包进二进制;未通过 `sources` 成功原生编译的 PHP 文件由 OPcache 生成字节码。用于 API 声明的 `.stub.php` 文件仍保留在原始文件包中,不生成可执行字节码。 diff --git a/README.md b/README.md index 46bf51ec..fbbfba45 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,10 @@ For multi-file projects, keep repeatable build settings in `project.yml`: ```yaml name: myapp mode: bin +version: 1.0.0 +info: + Author: TypePHP Team + Description: My TypePHP application php-version: "8.5" optimize: 2 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 dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED` 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. 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 diff --git a/docs/en/COMPILER_CLI.md b/docs/en/COMPILER_CLI.md index a71df34f..783ac439 100644 --- a/docs/en/COMPILER_CLI.md +++ b/docs/en/COMPILER_CLI.md @@ -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++, 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 When a program depends on other PHP extensions, the required modules can be written into the Zend module dependency table: diff --git a/docs/zh-cn/COMPILER_CLI.md b/docs/zh-cn/COMPILER_CLI.md index d52f0998..f0416dc0 100644 --- a/docs/zh-cn/COMPILER_CLI.md +++ b/docs/zh-cn/COMPILER_CLI.md @@ -136,6 +136,24 @@ tpc 产出 ELF 后由项目自己的构建流程另行封装。 使用 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别设置项目级 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 扩展时,可以将必需模块写入 Zend 模块依赖表: diff --git a/examples/myext/project.yml b/examples/myext/project.yml new file mode 100644 index 00000000..e9fca425 --- /dev/null +++ b/examples/myext/project.yml @@ -0,0 +1,12 @@ +name: myext +type: ext + +sources: + - ./test.php + +info: + version: 1.1.3 + author: Tianfeng.Han + email: rango@swoole.com + + diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index d8da01a3..c5ca23a7 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -559,6 +559,10 @@ extension-dependencies: - pdo_mysql - curl - curl +version: 1.2.3 +info: + Author: TypePHP Team + Description: Native PHP extension YAML); $this->invokeMethod('parseProjectYaml', $projectFile); @@ -578,10 +582,29 @@ YAML); $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('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->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 { $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 { if (!extension_loaded('mbstring')) { diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 14ef9c87..5b2a3552 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -523,6 +523,9 @@ class CompilerBase implements PropertyAccessContext protected array $projectObjectFiles = []; /** @var list Required PHP modules recorded in zend_module_entry.deps. */ protected array $extensionDependencies = []; + protected string $extensionVersion = ''; + /** @var array Custom rows shown in the module's phpinfo section. */ + protected array $extensionInfo = []; protected bool $debug = false; protected bool $formatCode = false; // --format: enable clang-format (disabled by default) protected bool $printBacktraceOnError = true; diff --git a/src/Translator.php b/src/Translator.php index 0e2e233c..1b034181 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1775,6 +1775,25 @@ PHP_RSHUTDOWN_FUNCTION({$moduleName}) { } 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(); if ($extensionDependencies === []) { $moduleHeader = ' STANDARD_MODULE_HEADER,'; @@ -1798,8 +1817,8 @@ zend_module_entry {$moduleName}_module_entry = { PHP_MSHUTDOWN({$moduleName}), PHP_RINIT({$moduleName}), PHP_RSHUTDOWN({$moduleName}), - nullptr, - nullptr, + {$moduleInfoFunction}, + {$moduleVersion}, STANDARD_MODULE_PROPERTIES, }; CODE; @@ -3894,6 +3913,36 @@ CODE; $cfg = $this->getProjectYamlLoader()->load($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'] ?? []; if (!is_array($objects)) { $this->error('`objects` must be an array');