- Replace ExtensionProvider::Keyword constant with direct '*' string usage - Add LibPhpxInstaller class to automatically build libphpx.so from source - Implement PHPX build dependency installation via system package managers - Update installer documentation to include libphpx.so auto-build instructions - Modify PHP directory detection to prioritize running PHP installation - Adjust CompilerBase to use Composer InstalledVersions for PHPX path resolution - Refactor LinuxPackageManager to separate build tool packages from configure options - Add comprehensive tests for LibPhpxInstaller shared library detection - Update platform warnings to guide users toward interactive auto-build - Revise ExtensionProvider parsing to handlepull/18/head
parent
c7bdfd883d
commit
bc8e4a685b
15 changed files with 242 additions and 24 deletions
@ -0,0 +1,29 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhp\Tests\Installer; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use TypePhp\Installer\LibPhpxInstaller; |
||||||
|
|
||||||
|
final class LibPhpxInstallerTest extends TestCase |
||||||
|
{ |
||||||
|
public function testDetectsSharedLibraryOnlyInPhpxLibDirectory(): void |
||||||
|
{ |
||||||
|
$root = sys_get_temp_dir() . '/typephp-phpx-' . bin2hex(random_bytes(6)); |
||||||
|
mkdir($root . '/lib', 0755, true); |
||||||
|
$installer = new LibPhpxInstaller(); |
||||||
|
|
||||||
|
try { |
||||||
|
self::assertFalse($installer->hasLibPhpx($root)); |
||||||
|
touch($root . '/lib/libphpx.a'); |
||||||
|
self::assertFalse($installer->hasLibPhpx($root)); |
||||||
|
touch($root . '/lib/libphpx.so'); |
||||||
|
self::assertTrue($installer->hasLibPhpx($root . '/')); |
||||||
|
} finally { |
||||||
|
@unlink($root . '/lib/libphpx.so'); |
||||||
|
@unlink($root . '/lib/libphpx.a'); |
||||||
|
@rmdir($root . '/lib'); |
||||||
|
@rmdir($root); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,124 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhp\Installer; |
||||||
|
|
||||||
|
final class LibPhpxInstaller |
||||||
|
{ |
||||||
|
public function __construct(private readonly InteractiveConsole $console = new InteractiveConsole()) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function ensure(string $phpxDir, string $phpDir): ?string |
||||||
|
{ |
||||||
|
$phpxDir = rtrim($phpxDir, '/'); |
||||||
|
$phpDir = rtrim($phpDir, '/'); |
||||||
|
if (PHP_OS_FAMILY !== 'Linux' || $this->hasLibPhpx($phpxDir)) { |
||||||
|
return $phpxDir; |
||||||
|
} |
||||||
|
if (!$this->console->isInteractive()) { |
||||||
|
$this->console->write('libphpx.so is missing. Run tpc.php in an interactive terminal to build it automatically, or set PHPX_HOME.'); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
$this->validateSourceTree($phpxDir); |
||||||
|
$this->validatePhpInstallation($phpDir); |
||||||
|
$this->console->write("The PHPX installation does not provide libphpx.so: {$phpxDir}"); |
||||||
|
if (!$this->console->confirm('Build libphpx.so now?', true)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
$this->installBuildDependencies($phpDir); |
||||||
|
$buildDir = $phpxDir . '/build'; |
||||||
|
$this->mkdir($buildDir); |
||||||
|
$this->run([ |
||||||
|
'cmake', |
||||||
|
'-S', $phpxDir, |
||||||
|
'-B', $buildDir, |
||||||
|
'-DCMAKE_BUILD_TYPE=Release', |
||||||
|
'-DBUILD_TESTS=OFF', |
||||||
|
'-Dphp_dir=' . $phpDir, |
||||||
|
], $phpDir); |
||||||
|
$this->run([ |
||||||
|
'cmake', |
||||||
|
'--build', $buildDir, |
||||||
|
'--parallel', (string) min(8, max(1, $this->cpuCount())), |
||||||
|
'--target', 'phpx', |
||||||
|
], $phpDir); |
||||||
|
|
||||||
|
if (!$this->hasLibPhpx($phpxDir)) { |
||||||
|
throw new \RuntimeException("Build completed but {$phpxDir}/lib/libphpx.so was not created"); |
||||||
|
} |
||||||
|
$this->console->write("libphpx.so built successfully in {$phpxDir}/lib"); |
||||||
|
return $phpxDir; |
||||||
|
} |
||||||
|
|
||||||
|
public function hasLibPhpx(string $phpxDir): bool |
||||||
|
{ |
||||||
|
return is_file(rtrim($phpxDir, '/') . '/lib/libphpx.so'); |
||||||
|
} |
||||||
|
|
||||||
|
private function validateSourceTree(string $phpxDir): void |
||||||
|
{ |
||||||
|
if (!is_file($phpxDir . '/CMakeLists.txt') || !is_dir($phpxDir . '/src') || !is_dir($phpxDir . '/include')) { |
||||||
|
throw new \RuntimeException("PHPX source tree is incomplete: {$phpxDir}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function validatePhpInstallation(string $phpDir): void |
||||||
|
{ |
||||||
|
$php = $phpDir . '/bin/php'; |
||||||
|
$phpConfig = $phpDir . '/bin/php-config'; |
||||||
|
if (!is_executable($php) || !is_executable($phpConfig)) { |
||||||
|
throw new \RuntimeException("PHP installation must provide bin/php and bin/php-config: {$phpDir}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function installBuildDependencies(string $phpDir): void |
||||||
|
{ |
||||||
|
$manager = LinuxPackageManager::detect(); |
||||||
|
if ($manager === null) { |
||||||
|
$this->console->write('No supported package manager (apt-get/dnf/yum) was found; continuing with existing build tools.'); |
||||||
|
return; |
||||||
|
} |
||||||
|
$packages = $manager->missingPackages($manager->packagesForBuildTools()); |
||||||
|
$this->console->write('Detected package manager: ' . $manager->command); |
||||||
|
if ($packages === []) { |
||||||
|
$this->console->write('All detected PHPX build dependencies are already installed.'); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!$this->console->confirm('Install missing PHPX build dependencies (' . implode(', ', $packages) . ')?', true)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$useSudo = function_exists('posix_geteuid') && posix_geteuid() !== 0; |
||||||
|
$refresh = $manager->refreshCommand($useSudo); |
||||||
|
if ($refresh !== null) { |
||||||
|
$this->run($refresh, $phpDir); |
||||||
|
} |
||||||
|
$this->run($manager->installCommand($packages, $useSudo), $phpDir); |
||||||
|
} |
||||||
|
|
||||||
|
private function run(array $command, string $phpDir): void |
||||||
|
{ |
||||||
|
$this->console->write('$ ' . implode(' ', array_map('escapeshellarg', $command))); |
||||||
|
$environment = getenv(); |
||||||
|
$environment['PHP_HOME'] = $phpDir; |
||||||
|
$environment['PATH'] = $phpDir . '/bin:' . ($environment['PATH'] ?? ''); |
||||||
|
$process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes, null, $environment); |
||||||
|
if (!is_resource($process) || proc_close($process) !== 0) { |
||||||
|
throw new \RuntimeException('Command failed: ' . implode(' ', $command)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function mkdir(string $directory): void |
||||||
|
{ |
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0755, true) && !is_dir($directory)) { |
||||||
|
throw new \RuntimeException("Unable to create directory {$directory}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function cpuCount(): int |
||||||
|
{ |
||||||
|
$count = (int) trim((string) shell_exec('getconf _NPROCESSORS_ONLN 2>/dev/null')); |
||||||
|
return $count > 0 ? $count : 1; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue