From e1afe748b8bb3edcd7e1a6130ffa7ffb81f19eeb Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 21 Sep 2026 17:03:21 +0800 Subject: [PATCH] fix(build): disable PEAR and refine symlink scanning --- docs/en/LIBPHP_INSTALLER.md | 2 +- docs/zh-cn/LIBPHP_INSTALLER.md | 2 +- phpunit/src/Build/FileScannerTest.php | 52 +++++++++++++++++-- .../Installer/PhpBuildConfigurationTest.php | 17 ++++++ src/Build/FileScanner.php | 34 ++---------- src/Build/SourcePipelineTrait.php | 19 ++++--- src/Installer/LibPhpInstaller.php | 5 +- src/Installer/PhpBuildConfiguration.php | 4 ++ src/Translator.php | 19 +++++-- 9 files changed, 107 insertions(+), 47 deletions(-) diff --git a/docs/en/LIBPHP_INSTALLER.md b/docs/en/LIBPHP_INSTALLER.md index 74610b70..20f4645a 100644 --- a/docs/en/LIBPHP_INSTALLER.md +++ b/docs/en/LIBPHP_INSTALLER.md @@ -21,7 +21,7 @@ When `libphp.so` is missing, the installer asks in sequence: 3. the install directory, defaulting to `~/.typephp`; 4. whether to install missing development packages via the detected `apt-get`, `dnf`, or `yum`. -The installer reads the current `php-config --configure-options`, keeps the current PHP's extension configuration, replaces the install path, and adds `--enable-embed=shared`. PHP source is downloaded only from PHP.net, and verified using the SHA-256 from the official release information. +The installer reads the current `php-config --configure-options`, keeps the current PHP's supported extension configuration, replaces the install path, and adds `--enable-embed=shared`. It always uses `--without-pear` because PEAR is deprecated. PHP source is downloaded only from PHP.net, and verified using the SHA-256 from the official release information. After compilation, the main files are as follows: diff --git a/docs/zh-cn/LIBPHP_INSTALLER.md b/docs/zh-cn/LIBPHP_INSTALLER.md index 483be4a9..1ca18e1f 100644 --- a/docs/zh-cn/LIBPHP_INSTALLER.md +++ b/docs/zh-cn/LIBPHP_INSTALLER.md @@ -21,7 +21,7 @@ vendor/bin/tpc.php project.yml 3. 安装目录,默认为 `~/.typephp`; 4. 是否通过检测到的 `apt-get`、`dnf` 或 `yum` 安装缺失的开发包。 -安装器读取当前 `php-config --configure-options`,保留当前 PHP 的扩展配置,替换安装路径并加入 `--enable-embed=shared`。PHP 源码只从 PHP.net 下载,并使用官方发布信息中的 SHA-256 校验。 +安装器读取当前 `php-config --configure-options`,保留当前 PHP 支持的扩展配置,替换安装路径并加入 `--enable-embed=shared`。由于 PEAR 已废弃,构建始终使用 `--without-pear`。PHP 源码只从 PHP.net 下载,并使用官方发布信息中的 SHA-256 校验。 编译完成后主要文件如下: diff --git a/phpunit/src/Build/FileScannerTest.php b/phpunit/src/Build/FileScannerTest.php index 5f47a8c0..deb4ecea 100644 --- a/phpunit/src/Build/FileScannerTest.php +++ b/phpunit/src/Build/FileScannerTest.php @@ -68,12 +68,16 @@ final class FileScannerTest extends TestCase * Two links to one directory are one source file seen twice, and compiling * it twice would define its symbols twice. */ - public function testAliasesOfOneFileAreScannedOnce(): void + public function testAliasesOfOneFileAreCompiledOnce(): void { symlink($this->root . '/outside', $this->root . '/project/link-a'); symlink($this->root . '/outside', $this->root . '/project/link-b'); + file_put_contents( + $this->root . '/project/project.yml', + "name: demo\nsources:\n - .\n", + ); - $files = (new FileScanner($this->root . '/project'))->scan(); + $files = $this->scanProject($this->root . '/project/project.yml'); $this->assertSame([ $this->root . '/project/link-a/src/Linked.php', @@ -102,6 +106,45 @@ final class FileScannerTest extends TestCase } } + /** + * YAML ignore rules run outside FileScanner, so real-path deduplication must + * wait until the project-level filter has selected the surviving alias. + */ + public function testYamlIgnoreOfOneAliasKeepsTheOther(): void + { + symlink($this->root . '/outside', $this->root . '/project/link-a'); + symlink($this->root . '/outside', $this->root . '/project/link-b'); + + foreach (['link-a' => 'link-b', 'link-b' => 'link-a'] as $excluded => $kept) { + file_put_contents( + $this->root . '/project/project.yml', + "name: demo\nsources:\n - .\nignore:\n - {$excluded}\n", + ); + + $this->assertSame([ + $this->root . '/project/' . $kept . '/src/Linked.php', + $this->root . '/project/src/Own.php', + ], $this->scanProject($this->root . '/project/project.yml')); + } + } + + public function testLexicalPathNormalizationPreservesNetworkRoot(): void + { + $compiler = CompilerTest::create($this->root . '/project'); + $normalize = (new \ReflectionClass($compiler))->getMethod('normalizeLexicalPath'); + $separator = DIRECTORY_SEPARATOR; + $path = $separator . $separator . implode( + $separator, + ['server', 'share', 'project', '.', 'vendor', '..', '..', '..', 'package'], + ); + + $this->assertSame( + $separator . $separator . 'server' . $separator . 'share' + . $separator . 'package', + $normalize->invoke($compiler, $path), + ); + } + /** * `ignore` names the path that reaches a directory, not the path it points * at, so the scanned path is what an entry has to be compared against. @@ -144,10 +187,9 @@ final class FileScannerTest extends TestCase $compiler = CompilerTest::create(dirname($projectFile)); $reflection = new \ReflectionClass($compiler); - $parse = $reflection->getMethod('parseProjectYaml'); - $filter = $reflection->getMethod('filterIgnoredFiles'); + $parse = $reflection->getMethod('parseProjectYaml'); - return array_values($filter->invoke($compiler, $parse->invoke($compiler, $projectFile))); + return array_values($parse->invoke($compiler, $projectFile)); } private function removeDirectory(string $directory): void diff --git a/phpunit/src/Installer/PhpBuildConfigurationTest.php b/phpunit/src/Installer/PhpBuildConfigurationTest.php index d5eca6df..3c0e3be6 100644 --- a/phpunit/src/Installer/PhpBuildConfigurationTest.php +++ b/phpunit/src/Installer/PhpBuildConfigurationTest.php @@ -121,6 +121,23 @@ final class PhpBuildConfigurationTest extends TestCase self::assertContains('--enable-pcntl', $options); } + public function testDeriveAlwaysDisablesDeprecatedPearInstallation(): void + { + $options = PhpBuildConfiguration::derive( + "'--with-pear=/usr/share/php' '--enable-mbstring'", + '/home/test/.typephp' + ); + + self::assertSame( + ['--without-pear'], + array_values(array_filter( + $options, + static fn(string $option): bool => str_contains($option, 'pear') + )) + ); + self::assertContains('--enable-mbstring', $options); + } + public function testParseShellWordsRejectsIncompleteInput(): void { $this->expectException(\InvalidArgumentException::class); diff --git a/src/Build/FileScanner.php b/src/Build/FileScanner.php index 9baa6856..05415370 100644 --- a/src/Build/FileScanner.php +++ b/src/Build/FileScanner.php @@ -104,7 +104,11 @@ class FileScanner // keep both generated code and cache classification deterministic. sort($files, SORT_STRING); - return $this->deduplicateByRealPath($files); + // Keep aliases until the project-level ignore rules have run. If two + // links reach the same file and only one is ignored, removing aliases + // here would also remove the allowed path. The source pipeline performs + // real-path deduplication after filtering. + return $files; } /** @@ -166,34 +170,6 @@ class FileScanner } } - /** - * One file reachable through several links is still one source file, and - * compiling it twice would define its symbols twice. - * - * Exclusions have already run, so a path the project excluded can never be - * the alias that survives here. The list is sorted, so which alias survives - * does not depend on directory-entry order. - * - * @param list $files - * @return list - */ - private function deduplicateByRealPath(array $files): array - { - $unique = []; - $seen = []; - - foreach ($files as $file) { - $identity = realpath($file) ?: $file; - if (isset($seen[$identity])) { - continue; - } - $seen[$identity] = true; - $unique[] = $file; - } - - return $unique; - } - private function isExcluded(string $filePath): bool { $excluded = false; diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 1c3dd79d..be54833c 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -947,15 +947,22 @@ PHP protected function filterIgnoredFiles(array $files): array { - if (empty($this->ignorePaths)) { - return $files; - } - $filteredFiles = []; + $seenRealPaths = []; foreach ($files as $file) { - if (!$this->shouldIgnoreFile($file)) { - $filteredFiles[] = $file; + if ($this->shouldIgnoreFile($file)) { + continue; + } + + // Ignore rules describe the path used to reach a source. Resolve + // identity only after those rules have selected the surviving + // aliases, then compile each physical file once. + $identity = realpath($file) ?: $file; + if (isset($seenRealPaths[$identity])) { + continue; } + $seenRealPaths[$identity] = true; + $filteredFiles[] = $file; } return $filteredFiles; diff --git a/src/Installer/LibPhpInstaller.php b/src/Installer/LibPhpInstaller.php index e808e1a7..5bfda3a9 100644 --- a/src/Installer/LibPhpInstaller.php +++ b/src/Installer/LibPhpInstaller.php @@ -182,7 +182,10 @@ final class LibPhpInstaller $this->run(['tar', '-xJf', $archive, '-C', $workDir]); } - $this->console->write('Configuring PHP with the current installation options plus --enable-embed=shared'); + $this->console->write( + 'Configuring PHP with compatible current installation options, ' + . '--enable-embed=shared, and --without-pear' + ); $this->run([$sourceDir . '/configure', ...$options], $sourceDir); // PHP is a large build; capping parallelism avoids exhausting memory on // hosts that expose many CPUs (especially containers and CI runners). diff --git a/src/Installer/PhpBuildConfiguration.php b/src/Installer/PhpBuildConfiguration.php index e0de4504..2bb885b1 100644 --- a/src/Installer/PhpBuildConfiguration.php +++ b/src/Installer/PhpBuildConfiguration.php @@ -118,6 +118,9 @@ final class PhpBuildConfiguration $replace = [ '--prefix', '--with-config-file-path', '--with-config-file-scan-dir', '--enable-embed', '--enable-cli', '--disable-cli', '--with-libdir', + // PEAR is deprecated by PHP and PECL installation is being replaced + // by PIE. Do not inherit either form from the host PHP build. + '--with-pear', '--without-pear', ]; $drop = [ '--with-apxs', '--with-apxs2', '--enable-fpm', '--with-fpm-systemd', @@ -143,6 +146,7 @@ final class PhpBuildConfiguration '--with-config-file-scan-dir=' . $prefix . '/lib/conf.d', '--enable-embed=shared', '--enable-cli', + '--without-pear', ...$result, ]; } diff --git a/src/Translator.php b/src/Translator.php index 659c6631..417baad9 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -88,7 +88,7 @@ class Translator extends Preprocessor use ResourceCompilationTrait; use ClassConstantValueTrait; - public const string VERSION = '0.9.0'; + public const string VERSION = '0.9.1'; public const string APP_NAME = 'TypePHP Compiler (AOT)'; protected bool $hasExplicitOutput = false; @@ -3916,20 +3916,31 @@ CODE; $path = str_replace('\\', '/', $path); } + // Preserve the double leading separator of Windows UNC paths. Keeping + // it on POSIX as well makes the normalization rule deterministic and + // retains the implementation-defined // network-root form. + $networkRoot = str_starts_with($path, '//'); $absolute = str_starts_with($path, '/'); + // The server and share names form the root of a UNC path. `..` cannot + // cross that boundary into another share on the same server. + $rootDepth = $networkRoot ? 2 : 0; $segments = []; foreach (explode('/', $path) as $segment) { if ($segment === '' || $segment === '.') { continue; } - if ($segment === '..' && $segments !== [] && end($segments) !== '..') { - array_pop($segments); + if ($segment === '..') { + if (count($segments) > $rootDepth && end($segments) !== '..') { + array_pop($segments); + } elseif (!$absolute) { + $segments[] = $segment; + } continue; } $segments[] = $segment; } - $normalized = ($absolute ? '/' : '') . implode('/', $segments); + $normalized = ($networkRoot ? '//' : ($absolute ? '/' : '')) . implode('/', $segments); if ($prefix !== '') { $normalized = $prefix . $normalized; }