From 458af6ac783bb6dab4d4ab57c8a03e8800e4f1e7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 10 Dec 2025 13:38:25 +0800 Subject: [PATCH 01/21] Add build function check for current OS and update validation logic --- captainhook.json | 2 +- src/Package/Target/php.php | 7 +++ src/StaticPHP/Package/Package.php | 8 ++++ src/StaticPHP/Registry/PackageLoader.php | 4 +- .../StaticPHP/Registry/PackageLoaderTest.php | 45 ++++++++++++++++--- 5 files changed, 57 insertions(+), 9 deletions(-) diff --git a/captainhook.json b/captainhook.json index 77be1d571..8af0df3e5 100644 --- a/captainhook.json +++ b/captainhook.json @@ -11,7 +11,7 @@ "enabled": true, "actions": [ { - "action": "composer cs-fix -- --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}", + "action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}", "conditions": [ { "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 03b50073b..88e018587 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -15,6 +15,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Exception\SPCException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\Package; @@ -529,6 +530,12 @@ public function build(TargetPackage $package): void $package->runStage([$this, 'unixBuildSharedExt']); } + #[BuildFor('Windows')] + public function buildWin(TargetPackage $package): void + { + throw new EnvironmentException('Not implemented'); + } + /** * Patch phpize and php-config if needed */ diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 6cad1fabd..aa8ab6f00 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -127,6 +127,14 @@ public function hasStage(mixed $name): bool return false; } + /** + * Check if the package has a build function for the current OS. + */ + public function hasBuildFunctionForCurrentOS(): bool + { + return isset($this->build_functions[PHP_OS_FAMILY]); + } + /** * Get the name of the package. */ diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 0ef3fb8ee..29ce0a96b 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -306,7 +306,9 @@ public static function checkLoadedStageEvents(): void } } // check stage exists - if (!$pkg->hasStage($stage_name)) { + // Skip validation if the package has no build function for current OS + // (e.g., libedit has BeforeStage for 'build' but only BuildFor('Darwin'/'Linux')) + if (!$pkg->hasStage($stage_name) && $pkg->hasBuildFunctionForCurrentOS()) { throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}]."); } } diff --git a/tests/StaticPHP/Registry/PackageLoaderTest.php b/tests/StaticPHP/Registry/PackageLoaderTest.php index 7228b5ae8..a40c79faf 100644 --- a/tests/StaticPHP/Registry/PackageLoaderTest.php +++ b/tests/StaticPHP/Registry/PackageLoaderTest.php @@ -377,6 +377,10 @@ public function testCheckLoadedStageEventsThrowsExceptionForUnknownStage(): void $this->createTestPackageConfig('test-lib', 'library'); PackageLoader::initPackageInstances(); + // Add a build function for current OS so the stage validation is triggered + $package = PackageLoader::getPackage('test-lib'); + $package->addBuildFunction(PHP_OS_FAMILY, fn () => null); + // Manually add a before_stage for non-existent stage $reflection = new \ReflectionClass(PackageLoader::class); $property = $reflection->getProperty('before_stages'); @@ -417,6 +421,33 @@ public function testCheckLoadedStageEventsThrowsExceptionForUnknownOnlyWhenPacka PackageLoader::checkLoadedStageEvents(); } + public function testCheckLoadedStageEventsDoesNotThrowForNonCurrentOSPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + // Add a build function for a different OS (not current OS) + $package = PackageLoader::getPackage('test-lib'); + $otherOS = PHP_OS_FAMILY === 'Windows' ? 'Linux' : 'Windows'; + $package->addBuildFunction($otherOS, fn () => null); + + // Manually add a before_stage for 'build' stage + // This should NOT throw an exception because the package has no build function for current OS + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-lib' => [ + 'build' => [[fn () => null, null]], + ], + ]); + + // This should not throw an exception + PackageLoader::checkLoadedStageEvents(); + + $this->assertTrue(true); // If we get here, the test passed + } + public function testGetBeforeStageCallbacksReturnsCallbacks(): void { PackageLoader::initPackageInstances(); @@ -502,13 +533,13 @@ public function testLoadFromPsr4DirLoadsAllClasses(): void mkdir($psr4Dir, 0755, true); // Create test class file - $classContent = ' Date: Wed, 10 Dec 2025 13:41:36 +0800 Subject: [PATCH 02/21] Update captain hook for windows --- captainhook.json | 88 ++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/captainhook.json b/captainhook.json index 8af0df3e5..c4c1d8b9d 100644 --- a/captainhook.json +++ b/captainhook.json @@ -1,44 +1,44 @@ -{ - "pre-push": { - "enabled": true, - "actions": [ - { - "action": "composer analyse" - } - ] - }, - "pre-commit": { - "enabled": true, - "actions": [ - { - "action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}", - "conditions": [ - { - "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", - "args": ["php"] - } - ] - } - ] - }, - "post-change": { - "enabled": true, - "actions": [ - { - "action": "composer install", - "options": [], - "conditions": [ - { - "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any", - "args": [ - [ - "composer.json", - "composer.lock" - ] - ] - } - ] - } - ] - } -} +{ + "pre-push": { + "enabled": true, + "actions": [ + { + "action": ".\\vendor\\bin\\phpstan analyse --memory-limit 300M" + } + ] + }, + "pre-commit": { + "enabled": true, + "actions": [ + { + "action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}", + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", + "args": ["php"] + } + ] + } + ] + }, + "post-change": { + "enabled": true, + "actions": [ + { + "action": "composer install", + "options": [], + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any", + "args": [ + [ + "composer.json", + "composer.lock" + ] + ] + } + ] + } + ] + } +} From 2080407283be1a314174065571b58a505ce81386 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 11:35:12 +0800 Subject: [PATCH 03/21] Enhance Windows support by updating artifact configuration and improving extraction logic --- captainhook.json | 2 +- config/artifact.json | 6 +- config/pkg.target.json | 5 +- src/StaticPHP/Artifact/Artifact.php | 19 ++- src/StaticPHP/Artifact/ArtifactExtractor.php | 63 ++++---- .../Artifact/Downloader/DownloadResult.php | 30 +++- src/StaticPHP/Command/Dev/EnvCommand.php | 37 +++++ src/StaticPHP/Command/DoctorCommand.php | 1 + src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Doctor/Doctor.php | 1 + .../Doctor/Item/WindowsToolCheck.php | 142 ++++++++++++++++++ src/StaticPHP/Package/PackageBuilder.php | 4 - src/StaticPHP/Package/PackageInstaller.php | 4 + src/StaticPHP/Runtime/Shell/DefaultShell.php | 40 ++--- src/StaticPHP/Runtime/Shell/Shell.php | 12 +- src/StaticPHP/Runtime/Shell/UnixShell.php | 7 +- src/StaticPHP/Runtime/Shell/WindowsCmd.php | 86 +---------- src/StaticPHP/Toolchain/MSVCToolchain.php | 45 +++++- src/StaticPHP/Util/FileSystem.php | 12 +- src/StaticPHP/Util/GlobalEnvManager.php | 2 + src/StaticPHP/Util/System/WindowsUtil.php | 47 +++--- 21 files changed, 384 insertions(+), 183 deletions(-) create mode 100644 src/StaticPHP/Command/Dev/EnvCommand.php create mode 100644 src/StaticPHP/Doctor/Item/WindowsToolCheck.php diff --git a/captainhook.json b/captainhook.json index c4c1d8b9d..63d88f065 100644 --- a/captainhook.json +++ b/captainhook.json @@ -11,7 +11,7 @@ "enabled": true, "actions": [ { - "action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}", + "action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential", "conditions": [ { "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", diff --git a/config/artifact.json b/config/artifact.json index de30a1e4c..75ee9cfc1 100644 --- a/config/artifact.json +++ b/config/artifact.json @@ -80,7 +80,11 @@ }, "strawberry-perl": { "binary": { - "windows-x86_64": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip" + "windows-x86_64": { + "type": "url", + "url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip", + "extract": "{pkg_root_path}/strawberry-perl" + } } }, "upx": { diff --git a/config/pkg.target.json b/config/pkg.target.json index b5e4a7c8b..2ae49f400 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -1,7 +1,10 @@ { "vswhere": { "type": "target", - "artifact": "vswhere" + "artifact": "vswhere", + "static-bins@windows": [ + "vswhere.exe" + ] }, "pkg-config": { "type": "target", diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 5e5e8b558..bcf6ca622 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -167,11 +167,22 @@ public function isBinaryExtracted(?string $target_os = null, bool $compare_hash return false; } - // For standalone mode, check directory and hash + // For standalone mode, check directory or file and hash $target_path = $extract_config['path']; - if (!is_dir($target_path)) { - return false; + // Check if target is a file or directory + $is_file_target = !is_dir($target_path) && str_contains($target_path, '.'); + + if ($is_file_target) { + // For single file extraction (e.g., vswhere.exe) + if (!file_exists($target_path)) { + return false; + } + } else { + // For directory extraction + if (!is_dir($target_path)) { + return false; + } } if (!$compare_hash) { @@ -320,7 +331,7 @@ public function getBinaryExtractConfig(array $cache_info = []): array * For merge mode, returns the base path. * For standalone mode, returns the specific directory. */ - public function getBinaryDir(): string + public function getBinaryDir(): ?string { $config = $this->getBinaryExtractConfig(); return $config['path']; diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 778c24f31..93b363823 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -293,7 +293,7 @@ protected function doSelectiveExtract(string $name, array $cache_info, array $fi // Process file mappings foreach ($file_map as $src_pattern => $dst_path) { $dst_path = $this->replacePathVariables($dst_path); - $src_full = "{$temp_path}/{$src_pattern}"; + $src_full = FileSystem::convertPath("{$temp_path}/{$src_pattern}"); // Handle glob patterns if (str_contains($src_pattern, '*')) { @@ -460,40 +460,36 @@ protected function extractArchive(string $filename, string $target): void $target = FileSystem::convertPath($target); $filename = FileSystem::convertPath($filename); - FileSystem::createDir($target); - - if (PHP_OS_FAMILY === 'Windows') { - // Use 7za.exe for Windows - $is_txz = str_ends_with($filename, '.txz') || str_ends_with($filename, '.tar.xz'); - default_shell()->execute7zExtract($filename, $target, $is_txz); - return; - } - - // Unix-like systems: determine compression type - if (str_ends_with($filename, '.tar.gz') || str_ends_with($filename, '.tgz')) { - default_shell()->executeTarExtract($filename, $target, 'gz'); - } elseif (str_ends_with($filename, '.tar.bz2')) { - default_shell()->executeTarExtract($filename, $target, 'bz2'); - } elseif (str_ends_with($filename, '.tar.xz') || str_ends_with($filename, '.txz')) { - default_shell()->executeTarExtract($filename, $target, 'xz'); - } elseif (str_ends_with($filename, '.tar')) { - default_shell()->executeTarExtract($filename, $target, 'none'); - } elseif (str_ends_with($filename, '.zip')) { - // Zip requires special handling for strip-components - $this->unzipWithStrip($filename, $target); - } elseif (str_ends_with($filename, '.exe')) { - // exe just copy to target - $dest_file = FileSystem::convertPath("{$target}/" . basename($filename)); - FileSystem::copy($filename, $dest_file); - } else { - throw new FileSystemException("Unknown archive format: {$filename}"); - } + $extname = FileSystem::extname($filename); + + if ($extname !== 'exe' && !is_dir($target)) { + FileSystem::createDir($target); + } + match (SystemTarget::getTargetOS()) { + 'Windows' => match ($extname) { + 'tar' => default_shell()->executeTarExtract($filename, $target, 'none'), + 'xz', 'txz', 'gz', 'tgz', 'bz2' => default_shell()->execute7zExtract($filename, $target), + 'zip' => $this->unzipWithStrip($filename, $target), + 'exe' => $this->copyFile($filename, $target), + default => throw new FileSystemException("Unknown archive format: {$filename}"), + }, + 'Linux', 'Darwin' => match ($extname) { + 'tar' => default_shell()->executeTarExtract($filename, $target, 'none'), + 'gz', 'tgz' => default_shell()->executeTarExtract($filename, $target, 'gz'), + 'bz2' => default_shell()->executeTarExtract($filename, $target, 'bz2'), + 'xz', 'txz' => default_shell()->executeTarExtract($filename, $target, 'xz'), + 'zip' => $this->unzipWithStrip($filename, $target), + 'exe' => $this->copyFile($filename, $target), + default => throw new FileSystemException("Unknown archive format: {$filename}"), + }, + default => throw new SPCInternalException('Unsupported OS for archive extraction') + }; } /** * Unzip file with stripping top-level directory. */ - protected function unzipWithStrip(string $zip_file, string $extract_path): void + protected function unzipWithStrip(string $zip_file, string $extract_path): bool { $temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); $zip_file = FileSystem::convertPath($zip_file); @@ -572,6 +568,8 @@ protected function unzipWithStrip(string $zip_file, string $extract_path): void // Clean up temp directory FileSystem::removeDir($temp_dir); + + return true; } /** @@ -585,6 +583,7 @@ protected function replacePathVariables(string $path): string '{source_path}' => SOURCE_PATH, '{download_path}' => DOWNLOAD_PATH, '{working_dir}' => WORKING_DIR, + '{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: '', ]; return str_replace(array_keys($replacement), array_values($replacement), $path); } @@ -627,9 +626,9 @@ private static function moveFileOrDir(string $source, string $dest): void } } - private function copyFile(string $source_file, string $target_path): void + private function copyFile(string $source_file, string $target_path): bool { FileSystem::createDir(dirname($target_path)); - FileSystem::copy(FileSystem::convertPath($source_file), $target_path); + return FileSystem::copy(FileSystem::convertPath($source_file), $target_path); } } diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index aefc6716b..6fa40bed7 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -30,7 +30,8 @@ private function __construct( ) { switch ($this->cache_type) { case 'archive': - $this->filename !== null ?: throw new DownloaderException('Archive download result must have a filename.'); + case 'file': + $this->filename !== null ?: throw new DownloaderException('Archive/file download result must have a filename.'); $fn = FileSystem::isRelativePath($this->filename) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->filename) : $this->filename; file_exists($fn) ?: throw new DownloaderException("Downloaded archive file does not exist: {$fn}"); break; @@ -60,7 +61,20 @@ public static function archive( ?string $version = null, array $metadata = [] ): DownloadResult { - return new self('archive', config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + // judge if it is archive or just a pure file + $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + } + + public static function file( + string $filename, + array $config, + bool $verified = false, + ?string $version = null, + array $metadata = [] + ): DownloadResult { + $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; + return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata); } /** @@ -143,4 +157,16 @@ public function withMeta(string $key, mixed $value): self array_merge($this->metadata, [$key => $value]) ); } + + /** + * Check + */ + private static function isArchiveFile(string $filename): bool + { + $archive_extensions = [ + 'zip', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz', 'txz', 'rar', '7z', + ]; + $lower_filename = strtolower($filename); + return array_any($archive_extensions, fn ($ext) => str_ends_with($lower_filename, '.' . $ext)); + } } diff --git a/src/StaticPHP/Command/Dev/EnvCommand.php b/src/StaticPHP/Command/Dev/EnvCommand.php new file mode 100644 index 000000000..2f2dbcf0e --- /dev/null +++ b/src/StaticPHP/Command/Dev/EnvCommand.php @@ -0,0 +1,37 @@ +addArgument('env', InputArgument::REQUIRED, 'The environment variable to show, if not set, all will be shown'); + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + $this->no_motd = true; + parent::initialize($input, $output); + } + + public function handle(): int + { + $env = $this->getArgument('env'); + if (($val = getenv($env)) === false) { + $this->output->writeln("Environment variable '{$env}' is not set."); + return static::FAILURE; + } + $this->output->writeln("{$val}"); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index 30475a5ee..cd90cd94c 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -18,6 +18,7 @@ public function configure(): void public function handle(): int { + f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes'); $fix_policy = match ($this->input->getOption('auto-fix')) { 'never' => FIX_POLICY_DIE, true, null => FIX_POLICY_AUTOFIX, diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index a12227fc0..0e5371ac4 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; @@ -57,6 +58,7 @@ public function __construct() // dev commands new ShellCommand(), new IsInstalledCommand(), + new EnvCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 22ca10f28..d86e42ac2 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -130,6 +130,7 @@ private function emitFix(string $fix_item, array $fix_item_params = []): bool $this->output?->writeln('Fix failed: ' . $e->getMessage() . ''); return false; } catch (\Throwable $e) { + logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString()); $this->output?->writeln('Fix failed with an unexpected error: ' . $e->getMessage() . ''); return false; } finally { diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php new file mode 100644 index 000000000..e6a042d3b --- /dev/null +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -0,0 +1,142 @@ +addInstallPackage('vswhere'); + $is_installed = $installer->isPackageInstalled('vswhere'); + if ($is_installed) { + return CheckResult::ok(); + } + return CheckResult::fail('vswhere is not installed', 'install-vswhere'); + } + + #[CheckItem('if Visual Studio is installed', level: 998)] + public function findVS(): ?CheckResult + { + $a = WindowsUtil::findVisualStudio(); + if ($a !== false) { + return CheckResult::ok("{$a['version']} at {$a['dir']}"); + } + return CheckResult::fail('Visual Studio with C++ tools is not installed. Please install Visual Studio with C++ tools.'); + } + + #[CheckItem('if git associated command exists', level: 997)] + public function checkGitPatch(): ?CheckResult + { + if (WindowsUtil::findCommand('patch.exe') === null) { + return CheckResult::fail('Git patch (minGW command) not found in path. You need to add "C:\Program Files\Git\usr\bin" in Path.'); + } + return CheckResult::ok(); + } + + #[CheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 996)] + public function checkSDK(): ?CheckResult + { + if (!file_exists(getenv('PHP_SDK_PATH') . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) { + return CheckResult::fail('php-sdk-binary-tools not downloaded', 'install-php-sdk'); + } + return CheckResult::ok(getenv('PHP_SDK_PATH')); + } + + #[CheckItem('if nasm installed', level: 995)] + public function checkNasm(): ?CheckResult + { + if (($a = WindowsUtil::findCommand('nasm.exe')) === null) { + return CheckResult::fail('nasm.exe not found in path.', 'install-nasm'); + } + return CheckResult::ok($a); + } + + #[CheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)] + public function checkPerl(): ?CheckResult + { + if (($path = WindowsUtil::findCommand('perl.exe')) === null) { + return CheckResult::fail('perl not found in path.', 'install-perl'); + } + if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) { + return CheckResult::fail($path . ' is not built for msvc.', 'install-perl'); + } + return CheckResult::ok($path); + } + + #[CheckItem('if environment is properly set up', level: 1)] + public function checkenv(): ?CheckResult + { + // manually trigger after init + try { + ToolchainManager::afterInitToolchain(); + } catch (\Exception $e) { + return CheckResult::fail('Environment setup failed: ' . $e->getMessage()); + } + $required_cmd = ['cl.exe', 'link.exe', 'lib.exe', 'dumpbin.exe', 'msbuild.exe', 'nmake.exe']; + foreach ($required_cmd as $cmd) { + if (WindowsUtil::findCommand($cmd) === null) { + return CheckResult::fail("{$cmd} not found in path. Please make sure Visual Studio with C++ tools is properly installed."); + } + } + return CheckResult::ok(); + } + + #[FixItem('install-perl')] + public function installPerl(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('strawberry-perl'); + $installer->run(false); + GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl'); + return true; + } + + #[FixItem('install-php-sdk')] + public function installSDK(): bool + { + FileSystem::removeDir(getenv('PHP_SDK_PATH')); + $installer = new PackageInstaller(); + $installer->addInstallPackage('php-sdk-binary-tools'); + $installer->run(false); + return true; + } + + #[FixItem('install-nasm')] + public function installNasm(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('nasm'); + $installer->run(false); + return true; + } + + #[FixItem('install-vswhere')] + public function installVSWhere(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('vswhere'); + $installer->run(false); + return true; + } +} diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index c87b7f29b..e2373253a 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -11,7 +11,6 @@ use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; -use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\System\LinuxUtil; @@ -27,9 +26,6 @@ public function __construct(protected array $options = []) { ApplicationContext::set(PackageBuilder::class, $this); - // apply build toolchain envs - GlobalEnvManager::afterInit(); - $this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1; } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 530003970..4c44ce920 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -15,6 +15,7 @@ use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\V2CompatLayer; use ZM\Logger\ConsoleColor; @@ -120,6 +121,9 @@ public function printBuildPackageOutputs(): void */ public function run(bool $interactive = true, bool $disable_delay_msg = false): void { + // apply build toolchain envs + GlobalEnvManager::afterInit(); + if (empty($this->packages)) { // resolve input, make dependency graph $this->resolvePackages(); diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index ee6f790c1..88393289b 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -42,7 +42,7 @@ public function executeCurl(string $url, string $method = 'GET', array $headers $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}"; $this->logCommandInfo($cmd); - $result = $this->passthru($cmd, console_output: false, capture_output: true, throw_on_error: false); + $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); $ret = $result['code']; $output = $result['output']; if ($ret !== 0) { @@ -96,15 +96,15 @@ public function executeGitClone(string $url, string $branch, string $path, bool $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); $this->logCommandInfo($cmd); logger()->debug("[GIT CLONE] {$cmd}"); - $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + $this->passthru($cmd, $this->console_putput); if ($submodules !== null) { $depth_flag = $shallow ? '--depth 1' : ''; foreach ($submodules as $submodule) { $submodule = escapeshellarg($submodule); - $submodule_cmd = clean_spaces("cd {$path_arg} && {$git} submodule update --init {$depth_flag} {$submodule}"); + $submodule_cmd = clean_spaces("{$git} submodule update --init {$depth_flag} {$submodule}"); $this->logCommandInfo($submodule_cmd); logger()->debug("[GIT SUBMODULE] {$submodule_cmd}"); - $this->passthru($submodule_cmd, $this->console_putput, capture_output: false, throw_on_error: true); + $this->passthru($submodule_cmd, $this->console_putput, cwd: $path_arg); } } } @@ -117,7 +117,7 @@ public function executeGitClone(string $url, string $branch, string $path, bool * @param string $compression Compression type: 'gz', 'bz2', 'xz', or 'none' * @param int $strip Number of leading components to strip (default: 1) */ - public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): void + public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): bool { $archive_arg = escapeshellarg(FileSystem::convertPath($archive_path)); $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); @@ -135,7 +135,8 @@ public function executeTarExtract(string $archive_path, string $target_path, str $this->logCommandInfo($cmd); logger()->debug("[TAR EXTRACT] {$cmd}"); - $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + $this->passthru($cmd, $this->console_putput); + return true; } /** @@ -154,7 +155,7 @@ public function executeUnzip(string $zip_path, string $target_path): void $this->logCommandInfo($cmd); logger()->debug("[UNZIP] {$cmd}"); - $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + $this->passthru($cmd, $this->console_putput); } /** @@ -162,9 +163,8 @@ public function executeUnzip(string $zip_path, string $target_path): void * * @param string $archive_path Path to the archive file * @param string $target_path Path to extract to - * @param bool $is_txz Whether this is a .txz/.tar.xz file that needs double extraction */ - public function execute7zExtract(string $archive_path, string $target_path, bool $is_txz = false): void + public function execute7zExtract(string $archive_path, string $target_path): bool { $sdk_path = getenv('PHP_SDK_PATH'); if ($sdk_path === false) { @@ -177,15 +177,19 @@ public function execute7zExtract(string $archive_path, string $target_path, bool $mute = $this->console_putput ? '' : ' > NUL'; - if ($is_txz) { - // txz/tar.xz contains a tar file inside, extract twice - $cmd = "{$_7z} x {$archive_arg} -so | {$_7z} x -si -ttar -o{$target_arg} -y{$mute}"; - } else { - $cmd = "{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"; - } + $run = function ($cmd) { + $this->logCommandInfo($cmd); + logger()->debug("[7Z EXTRACT] {$cmd}"); + $this->passthru($cmd, $this->console_putput); + }; - $this->logCommandInfo($cmd); - logger()->debug("[7Z EXTRACT] {$cmd}"); - $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + $extname = FileSystem::extname($archive_path); + match ($extname) { + 'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'), + 'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar tar -f - -x -C {$target_arg} --strip-components 1"), + default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"), + }; + + return true; } } diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index e465f66e8..1368c0178 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -148,8 +148,12 @@ protected function passthru( bool $console_output = false, ?string $original_command = null, bool $capture_output = false, - bool $throw_on_error = true + bool $throw_on_error = true, + ?string $cwd = null ): array { + if ($cwd !== null) { + $cwd = $cwd; + } $file_res = null; if ($this->enable_log_file) { // write executed command to the log file using fwrite @@ -160,10 +164,10 @@ protected function passthru( } $descriptors = [ 0 => ['file', 'php://stdin', 'r'], // stdin - 1 => ['pipe', 'w'], // stdout - 2 => ['pipe', 'w'], // stderr + 1 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stdout + 2 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stderr ]; - $process = proc_open($cmd, $descriptors, $pipes); + $process = proc_open($cmd, $descriptors, $pipes, $cwd); $output_value = ''; try { diff --git a/src/StaticPHP/Runtime/Shell/UnixShell.php b/src/StaticPHP/Runtime/Shell/UnixShell.php index 7690f247a..7d74f65f3 100644 --- a/src/StaticPHP/Runtime/Shell/UnixShell.php +++ b/src/StaticPHP/Runtime/Shell/UnixShell.php @@ -33,7 +33,7 @@ public function exec(string $cmd): static $original_command = $cmd; $this->logCommandInfo($original_command); $this->last_cmd = $cmd = $this->getExecString($cmd); - $this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true); + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); return $this; } @@ -71,7 +71,7 @@ public function execWithResult(string $cmd, bool $with_log = true): array } $cmd = $this->getExecString($cmd); $this->logCommandInfo($cmd); - $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false); + $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd); $out = explode("\n", $result['output']); return [$result['code'], $out]; } @@ -83,9 +83,6 @@ private function getExecString(string $cmd): string if (!empty($env_str)) { $cmd = "{$env_str} {$cmd}"; } - if ($this->cd !== null) { - $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } return $cmd; } } diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index 5a7511a9c..a60c41b23 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -4,7 +4,6 @@ namespace StaticPHP\Runtime\Shell; -use StaticPHP\Exception\ExecutionException; use StaticPHP\Exception\SPCInternalException; use ZM\Logger\ConsoleColor; @@ -28,7 +27,7 @@ public function exec(string $cmd): static $this->last_cmd = $cmd = $this->getExecString($cmd); // echo $cmd . PHP_EOL; - $this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true); + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); return $this; } @@ -46,7 +45,7 @@ public function execWithResult(string $cmd, bool $with_log = true): array logger()->debug('Running command with result: ' . $cmd); } $cmd = $this->getExecString($cmd); - $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false); + $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd); $out = explode("\n", $result['output']); return [$result['code'], $out]; } @@ -68,89 +67,8 @@ public function getLastCommand(): string return $this->last_cmd; } - /** - * Executes a command with console and log file output. - * - * @param string $cmd Full command to execute (including cd and env vars) - * @param bool $console_output If true, output will be printed to console - * @param null|string $original_command Original command string for logging - * @param bool $capture_output If true, capture and return output - * @param bool $throw_on_error If true, throw exception on non-zero exit code - * - * @return array{code: int, output: string} Returns exit code and captured output - */ - protected function passthru( - string $cmd, - bool $console_output = false, - ?string $original_command = null, - bool $capture_output = false, - bool $throw_on_error = true - ): array { - $file_res = null; - if ($this->enable_log_file) { - $file_res = fopen(SPC_SHELL_LOG, 'a'); - } - - $output_value = ''; - try { - $process = popen($cmd . ' 2>&1', 'r'); - if (!$process) { - throw new ExecutionException( - cmd: $original_command ?? $cmd, - message: 'Failed to open process for command, popen() failed.', - code: -1, - cd: $this->cd, - env: $this->env - ); - } - - while (($line = fgets($process)) !== false) { - if (static::$passthru_callback !== null) { - $callback = static::$passthru_callback; - $callback(); - } - if ($console_output) { - echo $line; - } - if ($file_res !== null) { - fwrite($file_res, $line); - } - if ($capture_output) { - $output_value .= $line; - } - } - - $result_code = pclose($process); - - if ($throw_on_error && $result_code !== 0) { - if ($file_res !== null) { - fwrite($file_res, "Command exited with non-zero code: {$result_code}\n"); - } - throw new ExecutionException( - cmd: $original_command ?? $cmd, - message: "Command exited with non-zero code: {$result_code}", - code: $result_code, - cd: $this->cd, - env: $this->env, - ); - } - - return [ - 'code' => $result_code, - 'output' => $output_value, - ]; - } finally { - if ($file_res !== null) { - fclose($file_res); - } - } - } - private function getExecString(string $cmd): string { - if ($this->cd !== null) { - $cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } return $cmd; } } diff --git a/src/StaticPHP/Toolchain/MSVCToolchain.php b/src/StaticPHP/Toolchain/MSVCToolchain.php index 68a2d0a28..1449db70d 100644 --- a/src/StaticPHP/Toolchain/MSVCToolchain.php +++ b/src/StaticPHP/Toolchain/MSVCToolchain.php @@ -4,16 +4,57 @@ namespace StaticPHP\Toolchain; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Toolchain\Interface\ToolchainInterface; +use StaticPHP\Util\GlobalEnvManager; +use StaticPHP\Util\System\WindowsUtil; class MSVCToolchain implements ToolchainInterface { - public function initEnv(): void {} + public function initEnv(): void + { + GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\bin'); + $sdk = getenv('PHP_SDK_PATH'); + if ($sdk !== false) { + GlobalEnvManager::addPathIfNotExists($sdk . '\bin'); + GlobalEnvManager::addPathIfNotExists($sdk . '\msys2\usr\bin'); + } + // strawberry-perl + if (is_dir(PKG_ROOT_PATH . '\strawberry-perl')) { + GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl\perl\bin'); + } + } - public function afterInit(): void {} + public function afterInit(): void + { + $count = count(getenv()); + $vs = WindowsUtil::findVisualStudio(); + if ($vs === false || !file_exists($vcvarsall = "{$vs['dir']}\\VC\\Auxiliary\\Build\\vcvarsall.bat")) { + throw new EnvironmentException( + 'Visual Studio with C++ tools not found', + 'Please install Visual Studio with C++ tools' + ); + } + if (getenv('VCINSTALLDIR') === false) { + if (file_exists(DOWNLOAD_PATH . '/.vcenv-cache') && (time() - filemtime(DOWNLOAD_PATH . '/.vcenv-cache')) < 3600) { + $output = file(DOWNLOAD_PATH . '/.vcenv-cache', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + } else { + exec('call "' . $vcvarsall . '" x64 > NUL && set', $output); + file_put_contents(DOWNLOAD_PATH . '/.vcenv-cache', implode("\n", $output)); + } + array_map(fn ($x) => putenv($x), $output); + } + $after = count(getenv()); + if ($after > $count) { + logger()->debug('Applied ' . ($after - $count) . ' environment variables from Visual Studio setup'); + } + } public function getCompilerInfo(): ?string { + if ($vcver = getenv('VisualStudioVersion')) { + return "Visual Studio {$vcver}"; + } return null; } diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 46d15a1ed..8eb98e402 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -120,7 +120,7 @@ public static function copyDir(string $from, string $to): void $src_path = FileSystem::convertPath($from); switch (PHP_OS_FAMILY) { case 'Windows': - f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i'); + cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i'); break; case 'Linux': case 'Darwin': @@ -137,7 +137,7 @@ public static function copyDir(string $from, string $to): void * @param string $from Source file path * @param string $to Destination file path */ - public static function copy(string $from, string $to): void + public static function copy(string $from, string $to): bool { logger()->debug("Copying file from {$from} to {$to}"); $dst_path = FileSystem::convertPath($to); @@ -145,6 +145,7 @@ public static function copy(string $from, string $to): void if (!copy($src_path, $dst_path)) { throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path); } + return true; } /** @@ -317,7 +318,12 @@ public static function removeDir(string $dir): bool } } elseif (is_link($sub_file) || is_file($sub_file)) { if (!unlink($sub_file)) { - return false; + $cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f'; + f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret); + if ($ret !== 0) { + logger()->warning('Remove file failed: ' . $sub_file); + return false; + } } } } diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php index cc21b480c..86fcc6524 100644 --- a/src/StaticPHP/Util/GlobalEnvManager.php +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -107,6 +107,8 @@ public static function addPathIfNotExists(string $path): void { if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) { self::putenv("PATH={$path}:" . getenv('PATH')); + } elseif (SystemTarget::getTargetOS() === 'Windows' && !str_contains(getenv('PATH'), $path)) { + self::putenv("PATH={$path};" . getenv('PATH')); } } diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index 1d9111617..a6df41564 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -15,13 +15,10 @@ class WindowsUtil * @param array $paths search path (default use env path) * @return null|string null if not found, string is absolute path */ - public static function findCommand(string $name, array $paths = [], bool $include_sdk_bin = false): ?string + public static function findCommand(string $name, array $paths = []): ?string { if (!$paths) { $paths = explode(PATH_SEPARATOR, getenv('Path')); - if ($include_sdk_bin) { - $paths[] = getenv('PHP_SDK_PATH') . '\bin'; - } } foreach ($paths as $path) { if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { @@ -34,29 +31,35 @@ public static function findCommand(string $name, array $paths = [], bool $includ /** * Find Visual Studio installation. * - * @return array|false False if not installed, array contains 'version' and 'dir' + * @return array{ + * version: string, + * major_version: string, + * dir: string + * }|false False if not installed, array contains 'version' and 'dir' */ public static function findVisualStudio(): array|false { - $check_path = [ - 'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs17', - 'C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs17', - 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs17', - 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs16', - 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs16', - 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs16', + // call vswhere (need VS and C++ tools installed), output is json + $vswhere_exec = PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'vswhere.exe'; + $args = [ + '-latest', + '-format', 'json', + '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', ]; - foreach ($check_path as $path => $vs_version) { - if (file_exists($path)) { - $vs_ver = $vs_version; - $d_dir = dirname($path, 4); - return [ - 'version' => $vs_ver, - 'dir' => $d_dir, - ]; - } + $cmd = escapeshellarg($vswhere_exec) . ' ' . implode(' ', $args); + $result = f_exec($cmd, $out, $code); + if ($code !== 0 || !$result) { + return false; } - return false; + $json = json_decode(implode("\n", $out), true); + if (!is_array($json) || count($json) === 0) { + return false; + } + return [ + 'version' => $json[0]['installationVersion'], + 'major_version' => explode('.', $json[0]['installationVersion'])[0], + 'dir' => $json[0]['installationPath'], + ]; } /** From fe0b983f6c49320787fc127960c364bd3bcbf0cd Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 13:46:15 +0800 Subject: [PATCH 04/21] Fix debug mode and verbosity relation --- src/StaticPHP/Command/BaseCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index da01723ac..e416be26d 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -94,7 +94,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Set debug mode in ApplicationContext - $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE; + $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; ApplicationContext::setDebug($isDebug); // show raw argv list for logger()->debug From 4bbe56dd9f06414ea9c501b80f6e43742dbaf11f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 13:47:32 +0800 Subject: [PATCH 05/21] Fix windows extracting with curl typo, ignore traits in package --- src/StaticPHP/Exception/ExceptionHandler.php | 2 +- src/StaticPHP/Runtime/Shell/DefaultShell.php | 2 +- src/StaticPHP/Util/FileSystem.php | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index a77327634..53dc15a85 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -190,7 +190,7 @@ private static function logError($message, int $indent_space = 0, bool $output_l $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { - InteractiveTerm::plain(ConsoleColor::red($line) . ''); + InteractiveTerm::plain(ConsoleColor::red($line) . '', 'error'); } } } diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 88393289b..a6421bdb9 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -186,7 +186,7 @@ public function execute7zExtract(string $archive_path, string $target_path): boo $extname = FileSystem::extname($archive_path); match ($extname) { 'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'), - 'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar tar -f - -x -C {$target_arg} --strip-components 1"), + 'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar -f - -x -C {$target_arg} --strip-components 1"), default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"), }; diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 8eb98e402..2f540d70d 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -267,6 +267,9 @@ public static function getClassesPsr4(string $dir, string $base_namespace, mixed if ($auto_require && !class_exists($class_name, false)) { require_once $file_path; } + if (class_exists($class_name, false) === false) { + continue; + } if (is_string($return_path_value)) { $classes[$class_name] = $return_path_value . '/' . $v; From eb0a36e379fcc552ec870b6875e3bb1ef94dd891 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 13:47:49 +0800 Subject: [PATCH 06/21] Rename --- src/Package/Library/imap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index bacfbe2eb..69e6c8820 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -14,7 +14,7 @@ #[Library('imap')] class imap { - #[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')] + #[AfterStage('php', [php::class, 'patchUnixEmbedScripts'], 'imap')] #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] public function afterPatchScripts(): void { From 48fbeab7e4d3b90cb96bbfcee3fb2af89fc85db2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 13:48:01 +0800 Subject: [PATCH 07/21] Add log for interactive term --- src/StaticPHP/Util/InteractiveTerm.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index ede87a643..1682ed1f6 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -23,6 +23,7 @@ public static function notice(string $message, bool $indent = false): void logger()->notice(strip_ansi_colors($message)); } else { $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message)); + logger()->debug(strip_ansi_colors($message)); } } @@ -34,15 +35,22 @@ public static function success(string $message, bool $indent = false): void logger()->info(strip_ansi_colors($message)); } else { $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); + logger()->debug(strip_ansi_colors($message)); } } - public static function plain(string $message): void + public static function plain(string $message, string $level = 'info'): void { $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); if ($output->isVerbose()) { - logger()->info(strip_ansi_colors($message)); + match ($level) { + 'debug' => logger()->debug(strip_ansi_colors($message)), + 'notice' => logger()->notice(strip_ansi_colors($message)), + 'warning' => logger()->warning(strip_ansi_colors($message)), + 'error' => logger()->error(strip_ansi_colors($message)), + default => logger()->info(strip_ansi_colors($message)), + }; } else { $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } @@ -66,6 +74,7 @@ public static function error(string $message, bool $indent = true): void logger()->error(strip_ansi_colors($message)); } else { $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message))); + logger()->debug(strip_ansi_colors($message)); } } @@ -78,6 +87,7 @@ public static function setMessage(string $message): void { $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); + logger()->debug(strip_ansi_colors($message)); } public static function finish(string $message, bool $status = true): void @@ -117,6 +127,7 @@ public static function indicateProgress(string $message): void self::$indicator->advance(); return; } + logger()->debug(strip_ansi_colors($message)); // if no ansi, use a dot instead of spinner if ($no_ansi) { self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' •', ' •']); From 7c8b40a49a9fc9787f60e3c28da173b06ddffa09 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 13:49:32 +0800 Subject: [PATCH 08/21] Add windows php cli builds, support micro patches --- src/Package/Target/php.php | 441 +---------------------- src/StaticPHP/Package/PackageBuilder.php | 13 +- src/StaticPHP/Runtime/Shell/Shell.php | 3 - src/StaticPHP/Util/SourcePatcher.php | 66 ++++ 4 files changed, 80 insertions(+), 443 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 88e018587..8cd6e9407 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,19 +4,16 @@ namespace Package\Target; +use Package\Target\php\unix; +use Package\Target\php\windows; use StaticPHP\Attribute\Package\BeforeStage; -use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; use StaticPHP\Attribute\Package\ResolveBuild; -use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\Package\Validate; -use StaticPHP\Attribute\PatchDescription; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Exception\EnvironmentException; -use StaticPHP\Exception\SPCException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\Package; use StaticPHP\Package\PackageBuilder; @@ -28,16 +25,11 @@ use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ToolchainManager; -use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; -use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\SourcePatcher; -use StaticPHP\Util\SPCConfigUtil; -use StaticPHP\Util\System\UnixUtil; use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; -use ZM\Logger\ConsoleColor; #[Target('php')] #[Target('php-cli')] @@ -48,6 +40,9 @@ #[Target('frankenphp')] class php extends TargetPackage { + use unix; + use windows; + public static function getPHPVersionID(): int { $artifact = ArtifactLoader::getArtifactInstance('php-src'); @@ -242,343 +237,6 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void FileSystem::removeDir(BUILD_MODULES_PATH); } - #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] - #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] - #[PatchDescription('Let php m4 tools use static pkg-config')] - public function patchBeforeBuildconf(TargetPackage $package): void - { - // patch configure.ac for musl and musl-toolchain - $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; - FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/configure.ac', - 'if command -v ldd >/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1', - 'if ' . ($musl ? 'true' : 'false') - ); - - // let php m4 tools use static pkg-config - FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); - } - - #[Stage] - public function buildconfForUnix(TargetPackage $package): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); - V2CompatLayer::emitPatchPoint('before-php-buildconf'); - shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); - } - - #[Stage] - public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure')); - V2CompatLayer::emitPatchPoint('before-php-configure'); - $cmd = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE'); - - $args = []; - $version_id = self::getPHPVersionID(); - // PHP JSON extension is built-in since PHP 8.0 - if ($version_id < 80000) { - $args[] = '--enable-json'; - } - // zts - if ($package->getBuildOption('enable-zts', false)) { - $args[] = '--enable-zts --disable-zend-signals'; - if ($version_id >= 80100 && SystemTarget::getTargetOS() === 'Linux') { - $args[] = '--enable-zend-max-execution-timers'; - } - } - // config-file-path and config-file-scan-dir - if ($option = $package->getBuildOption('with-config-file-path', false)) { - $args[] = "--with-config-file-path={$option}"; - } - if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) { - $args[] = "--with-config-file-scan-dir={$option}"; - } - // perform enable cli options - $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; - $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm'; - $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { - 'Linux' => '--enable-micro=all-static', - default => '--enable-micro', - } : null; - $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; - $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; - $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; - $args = implode(' ', array_filter($args)); - - $static_extension_str = $this->makeStaticExtensionString($installer); - - // run ./configure with args - $this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([ - 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'CPPFLAGS' => "-I{$package->getIncludeDir()}", - 'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), - ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); - } - - #[Stage] - public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void - { - V2CompatLayer::emitPatchPoint('before-php-make'); - - logger()->info('cleaning up php-src build files'); - shell()->cd($package->getSourceDir())->exec('make clean'); - - if ($installer->isPackageResolved('php-cli')) { - $package->runStage([self::class, 'makeCliForUnix']); - } - if ($installer->isPackageResolved('php-cgi')) { - $package->runStage([self::class, 'makeCgiForUnix']); - } - if ($installer->isPackageResolved('php-fpm')) { - $package->runStage([self::class, 'makeFpmForUnix']); - } - if ($installer->isPackageResolved('php-micro')) { - $package->runStage([self::class, 'makeMicroForUnix']); - } - if ($installer->isPackageResolved('php-embed')) { - $package->runStage([self::class, 'makeEmbedForUnix']); - } - } - - #[Stage] - public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); - $concurrency = $builder->concurrency; - shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("make -j{$concurrency} cli"); - - $builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php'); - $package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php'); - } - - #[Stage] - public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); - $concurrency = $builder->concurrency; - shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("make -j{$concurrency} cgi"); - - $builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi'); - $package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi'); - } - - #[Stage] - public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); - $concurrency = $builder->concurrency; - shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("make -j{$concurrency} fpm"); - - $builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm'); - $package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm'); - } - - #[Stage] - #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] - public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void - { - $phar_patched = false; - try { - if ($installer->isPackageResolved('ext-phar')) { - $phar_patched = true; - SourcePatcher::patchMicroPhar(self::getPHPVersionID()); - } - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); - // apply --with-micro-fake-cli option - $vars = $this->makeVars($installer); - $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} micro"); - - $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx'); - $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); - } finally { - if ($phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - } - - #[Stage] - public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); - $shared_exts = array_filter( - $installer->getResolvedPackages(), - static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp() - ); - $install_modules = $shared_exts ? 'install-modules' : ''; - - // detect changes in module path - $diff = new DirDiff(BUILD_MODULES_PATH, true); - - $root = BUILD_ROOT_PATH; - $sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i'; - - shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); - - // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared ------------- - - // process libphp.so for shared embed - $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; - $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; - if (file_exists($libphp_so)) { - // rename libphp.so if -release is set - if (SystemTarget::getTargetOS() === 'Linux') { - $this->processLibphpSoFile($libphp_so, $installer); - } - // deploy - $builder->deployBinary($libphp_so, $libphp_so, false); - $package->setOutput('Library path for embed SAPI', $libphp_so); - } - - // process shared extensions that built-with-php - $increment_files = $diff->getChangedFiles(); - $files = []; - foreach ($increment_files as $increment_file) { - $builder->deployBinary($increment_file, $increment_file, false); - $files[] = basename($increment_file); - } - if (!empty($files)) { - $package->setOutput('Built shared extensions', implode(', ', $files)); - } - - // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- - - // process libphp.a for static embed - if (!file_exists("{$package->getLibDir()}/libphp.a")) { - return; - } - $ar = getenv('AR') ?: 'ar'; - $libphp_a = "{$package->getLibDir()}/libphp.a"; - shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}"); - UnixUtil::exportDynamicSymbols($libphp_a); - - // deploy embed php scripts - $package->runStage([$this, 'patchEmbedScripts']); - } - - #[Stage] - public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void - { - // collect shared extensions - /** @var PhpExtensionPackage[] $shared_extensions */ - $shared_extensions = array_filter( - $installer->getResolvedPackages(PhpExtensionPackage::class), - fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp() - ); - if (!empty($shared_extensions)) { - if ($toolchain->isStatic()) { - throw new WrongUsageException( - "You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" . - 'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" . - 'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.' - ); - } - FileSystem::createDir(BUILD_MODULES_PATH); - - // backup - FileSystem::backupFile(BUILD_BIN_PATH . '/php-config'); - FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4'); - - FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"'); - FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes'); - } - - try { - logger()->debug('Building shared extensions...'); - foreach ($shared_extensions as $extension) { - InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); - $extension->buildShared(); - } - } finally { - // restore php-config - if (!empty($shared_extensions)) { - FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config'); - FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4'); - } - } - } - - #[BuildFor('Darwin')] - #[BuildFor('Linux')] - public function build(TargetPackage $package): void - { - // virtual target, do nothing - if ($package->getName() !== 'php') { - return; - } - - $package->runStage([$this, 'buildconfForUnix']); - $package->runStage([$this, 'configureForUnix']); - $package->runStage([$this, 'makeForUnix']); - - $package->runStage([$this, 'unixBuildSharedExt']); - } - - #[BuildFor('Windows')] - public function buildWin(TargetPackage $package): void - { - throw new EnvironmentException('Not implemented'); - } - - /** - * Patch phpize and php-config if needed - */ - #[Stage] - public function patchEmbedScripts(): void - { - // patch phpize - if (file_exists(BUILD_BIN_PATH . '/phpize')) { - logger()->debug('Patching phpize prefix'); - FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'"); - FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#'); - $this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize'); - } - // patch php-config - if (file_exists(BUILD_BIN_PATH . '/php-config')) { - logger()->debug('Patching php-config prefix and libs order'); - $php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config'); - $php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str); - // move mimalloc to the beginning of libs - $php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str); - // move lstdc++ to the end of libs - $php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str); - FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str); - $this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config'); - } - } - - /** - * Seek php-src/config.log when building PHP, add it to exception. - */ - protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void - { - try { - $callback(); - } catch (SPCException $e) { - if (file_exists("{$source_dir}/config.log")) { - $e->addExtraLogFile('php-src config.log', 'php-src.config.log'); - copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log'); - } - throw $e; - } - } - private function makeStaticExtensionString(PackageInstaller $installer): string { $arg = []; @@ -599,93 +257,4 @@ private function makeStaticExtensionString(PackageInstaller $installer): string logger()->debug("Static extension configure args: {$str}"); return $str; } - - /** - * Make environment variables for php make. - * This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking. - */ - private function makeVars(PackageInstaller $installer): array - { - $config = (new SPCConfigUtil(['libs_only_deps' => true]))->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); - $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; - $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; - - return array_filter([ - 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}", - 'EXTRA_LDFLAGS' => $config['ldflags'], - 'EXTRA_LIBS' => $config['libs'], - ]); - } - - /** - * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. - */ - private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void - { - $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; - $libDir = BUILD_LIB_PATH; - $modulesDir = BUILD_MODULES_PATH; - $realLibName = 'libphp.so'; - $cwd = getcwd(); - - if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) { - $release = $matches[1]; - $realLibName = "libphp-{$release}.so"; - $libphpRelease = "{$libDir}/{$realLibName}"; - if (!file_exists($libphpRelease) && file_exists($libphpSo)) { - rename($libphpSo, $libphpRelease); - } - if (file_exists($libphpRelease)) { - chdir($libDir); - if (file_exists($libphpSo)) { - unlink($libphpSo); - } - symlink($realLibName, 'libphp.so'); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($realLibName), - escapeshellarg($libphpRelease) - )); - } - if (is_dir($modulesDir)) { - chdir($modulesDir); - foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { - if (!$ext->isBuildShared()) { - continue; - } - $name = $ext->getName(); - $versioned = "{$name}-{$release}.so"; - $unversioned = "{$name}.so"; - $src = "{$modulesDir}/{$versioned}"; - $dst = "{$modulesDir}/{$unversioned}"; - if (is_file($src)) { - rename($src, $dst); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($unversioned), - escapeshellarg($dst) - )); - } - } - } - chdir($cwd); - } - - $target = "{$libDir}/{$realLibName}"; - if (file_exists($target)) { - [, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target)); - $output = implode("\n", $output); - if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) { - $currentSoname = $sonameMatch[1]; - if ($currentSoname !== basename($target)) { - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg(basename($target)), - escapeshellarg($target) - )); - } - } - } - } } diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index e2373253a..94c55a66e 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -99,7 +99,7 @@ public function deployBinary(string $src, string $dst, bool $executable = true): // ignore copy to self if (realpath($src) !== realpath($dst)) { - shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); + FileSystem::copy($src, $dst); } // file exist @@ -111,7 +111,7 @@ public function deployBinary(string $src, string $dst, bool $executable = true): $this->extractDebugInfo($dst); // strip - if (!$this->getOption('no-strip')) { + if (!$this->getOption('no-strip') && SystemTarget::isUnix()) { $this->stripBinary($dst); } @@ -123,6 +123,9 @@ public function deployBinary(string $src, string $dst, bool $executable = true): } logger()->info("Compressing {$dst} with UPX"); shell()->exec(getenv('UPX_EXEC') . " --best {$dst}"); + } elseif ($upx_option && SystemTarget::getTargetOS() === 'Windows' && $executable) { + logger()->info("Compressing {$dst} with UPX"); + shell()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst)); } return $dst; @@ -136,12 +139,13 @@ public function deployBinary(string $src, string $dst, bool $executable = true): public function extractDebugInfo(string $binary_path): string { $target_dir = BUILD_ROOT_PATH . '/debug'; - FileSystem::createDir($target_dir); $basename = basename($binary_path); $debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug'); if (SystemTarget::getTargetOS() === 'Darwin') { + FileSystem::createDir($target_dir); shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); } elseif (SystemTarget::getTargetOS() === 'Linux') { + FileSystem::createDir($target_dir); if ($eu_strip = LinuxUtil::findCommand('eu-strip')) { shell() ->exec("{$eu_strip} -f {$debug_file} {$binary_path}") @@ -152,7 +156,8 @@ public function extractDebugInfo(string $binary_path): string ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); } } else { - throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS'); + logger()->debug('extractDebugInfo is only supported on Linux and macOS'); + return ''; } return $debug_file; } diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index 1368c0178..bf30d9d82 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -151,9 +151,6 @@ protected function passthru( bool $throw_on_error = true, ?string $cwd = null ): array { - if ($cwd !== null) { - $cwd = $cwd; - } $file_res = null; if ($this->enable_log_file) { // write executed command to the log file using fwrite diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 42ea8c369..6a16f041f 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -6,6 +6,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\Exception\PatchException; +use StaticPHP\Registry\PackageLoader; /** * SourcePatcher provides static utility methods for patching source files. @@ -194,4 +195,69 @@ public static function unpatchMicroPhar(): void { FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); } + + public static function patchPhpSrc(?array $items = null): bool + { + $patch_dir = ROOT_DIR . '/src/globals/patch/php-src-patches'; + // in phar mode, we need to extract all the patch files + if (str_starts_with($patch_dir, 'phar://')) { + $tmp_dir = sys_get_temp_dir() . '/php-src-patches'; + FileSystem::createDir($tmp_dir); + foreach (FileSystem::scanDirFiles($patch_dir) as $file) { + FileSystem::writeFile("{$tmp_dir}/" . basename($file), file_get_contents($file)); + } + $patch_dir = $tmp_dir; + } + $php_package = PackageLoader::getTargetPackage('php'); + if (!file_exists("{$php_package->getSourceDir()}/sapi/micro/php_micro.c")) { + return false; + } + $ver_file = "{$php_package->getSourceDir()}/main/php_version.h"; + if (!file_exists($ver_file)) { + throw new PatchException('php-src patcher (original micro patches)', 'Patch failed, cannot find php source files'); + } + $version_h = FileSystem::readFile("{$php_package->getSourceDir()}/main/php_version.h"); + preg_match('/#\s*define\s+PHP_MAJOR_VERSION\s+(\d+)\s+#\s*define\s+PHP_MINOR_VERSION\s+(\d+)\s+/m', $version_h, $match); + // $ver = "{$match[1]}.{$match[2]}"; + + $major_ver = $match[1] . $match[2]; + if ($major_ver === '74') { + return false; + } + // $check = !defined('DEBUG_MODE') ? ' -q' : ''; + // f_passthru('cd ' . SOURCE_PATH . '/php-src && git checkout' . $check . ' HEAD'); + + if ($items !== null) { + $spc_micro_patches = $items; + } else { + $spc_micro_patches = getenv('SPC_MICRO_PATCHES'); + $spc_micro_patches = $spc_micro_patches === false ? [] : explode(',', $spc_micro_patches); + } + $spc_micro_patches = array_filter($spc_micro_patches, fn ($item) => trim((string) $item) !== ''); + $patch_list = $spc_micro_patches; + $patches = []; + $serial = ['80', '81', '82', '83', '84', '85']; + foreach ($patch_list as $patchName) { + if (file_exists("{$patch_dir}/{$patchName}.patch")) { + $patches[] = "{$patch_dir}/{$patchName}.patch"; + continue; + } + for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) { + $tryMajMin = $serial[$i]; + if (!file_exists("{$patch_dir}/{$patchName}_{$tryMajMin}.patch")) { + continue; + } + $patches[] = "{$patch_dir}/{$patchName}_{$tryMajMin}.patch"; + continue 2; + } + throw new PatchException('phpmicro patches', "Failed finding patch file or versioned file {$patchName} !"); + } + + foreach ($patches as $patch) { + logger()->info("Patching micro with {$patch}"); + self::patchFile($patch, $php_package->getSourceDir()); + } + + return true; + } } From f6b47ad810b47d7f99f6068325b0791d52b9b8bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 13:50:36 +0800 Subject: [PATCH 09/21] Separate unix and windows build for php --- src/Package/Target/php/unix.php | 450 +++++++++++++++++++++++++++++ src/Package/Target/php/windows.php | 228 +++++++++++++++ 2 files changed, 678 insertions(+) create mode 100644 src/Package/Target/php/unix.php create mode 100644 src/Package/Target/php/windows.php diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php new file mode 100644 index 000000000..23d465dfb --- /dev/null +++ b/src/Package/Target/php/unix.php @@ -0,0 +1,450 @@ +/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1', + 'if ' . ($musl ? 'true' : 'false') + ); + + // let php m4 tools use static pkg-config + FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + } + + #[Stage] + public function buildconfForUnix(TargetPackage $package): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); + V2CompatLayer::emitPatchPoint('before-php-buildconf'); + shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); + } + + #[Stage] + public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure')); + V2CompatLayer::emitPatchPoint('before-php-configure'); + $cmd = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE'); + + $args = []; + $version_id = self::getPHPVersionID(); + // PHP JSON extension is built-in since PHP 8.0 + if ($version_id < 80000) { + $args[] = '--enable-json'; + } + // zts + if ($package->getBuildOption('enable-zts', false)) { + $args[] = '--enable-zts --disable-zend-signals'; + if ($version_id >= 80100 && SystemTarget::getTargetOS() === 'Linux') { + $args[] = '--enable-zend-max-execution-timers'; + } + } + // config-file-path and config-file-scan-dir + if ($option = $package->getBuildOption('with-config-file-path', false)) { + $args[] = "--with-config-file-path={$option}"; + } + if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) { + $args[] = "--with-config-file-scan-dir={$option}"; + } + // perform enable cli options + $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; + $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm'; + $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { + 'Linux' => '--enable-micro=all-static', + default => '--enable-micro', + } : null; + $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; + $args = implode(' ', array_filter($args)); + + $static_extension_str = $this->makeStaticExtensionString($installer); + + // run ./configure with args + $this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([ + 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'CPPFLAGS' => "-I{$package->getIncludeDir()}", + 'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), + ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); + } + + #[Stage] + public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void + { + V2CompatLayer::emitPatchPoint('before-php-make'); + + logger()->info('cleaning up php-src build files'); + shell()->cd($package->getSourceDir())->exec('make clean'); + + if ($installer->isPackageResolved('php-cli')) { + $package->runStage([self::class, 'makeCliForUnix']); + } + if ($installer->isPackageResolved('php-cgi')) { + $package->runStage([self::class, 'makeCgiForUnix']); + } + if ($installer->isPackageResolved('php-fpm')) { + $package->runStage([self::class, 'makeFpmForUnix']); + } + if ($installer->isPackageResolved('php-micro')) { + $package->runStage([self::class, 'makeMicroForUnix']); + } + if ($installer->isPackageResolved('php-embed')) { + $package->runStage([self::class, 'makeEmbedForUnix']); + } + } + + #[Stage] + public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); + $concurrency = $builder->concurrency; + shell()->cd($package->getSourceDir()) + ->setEnv($this->makeVars($installer)) + ->exec("make -j{$concurrency} cli"); + + $builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php'); + $package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php'); + } + + #[Stage] + public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); + $concurrency = $builder->concurrency; + shell()->cd($package->getSourceDir()) + ->setEnv($this->makeVars($installer)) + ->exec("make -j{$concurrency} cgi"); + + $builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi'); + $package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi'); + } + + #[Stage] + public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); + $concurrency = $builder->concurrency; + shell()->cd($package->getSourceDir()) + ->setEnv($this->makeVars($installer)) + ->exec("make -j{$concurrency} fpm"); + + $builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm'); + $package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm'); + } + + #[Stage] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] + public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + $phar_patched = false; + try { + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); + // apply --with-micro-fake-cli option + $vars = $this->makeVars($installer); + $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + // build + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} micro"); + + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx'); + $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } + } + + #[Stage] + public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); + $shared_exts = array_filter( + $installer->getResolvedPackages(), + static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp() + ); + $install_modules = $shared_exts ? 'install-modules' : ''; + + // detect changes in module path + $diff = new DirDiff(BUILD_MODULES_PATH, true); + + $root = BUILD_ROOT_PATH; + $sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i'; + + shell()->cd($package->getSourceDir()) + ->setEnv($this->makeVars($installer)) + ->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile') + ->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); + + // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared ------------- + + // process libphp.so for shared embed + $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; + $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; + if (file_exists($libphp_so)) { + // rename libphp.so if -release is set + if (SystemTarget::getTargetOS() === 'Linux') { + $this->processLibphpSoFile($libphp_so, $installer); + } + // deploy + $builder->deployBinary($libphp_so, $libphp_so, false); + $package->setOutput('Library path for embed SAPI', $libphp_so); + } + + // process shared extensions that built-with-php + $increment_files = $diff->getChangedFiles(); + $files = []; + foreach ($increment_files as $increment_file) { + $builder->deployBinary($increment_file, $increment_file, false); + $files[] = basename($increment_file); + } + if (!empty($files)) { + $package->setOutput('Built shared extensions', implode(', ', $files)); + } + + // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- + + // process libphp.a for static embed + if (!file_exists("{$package->getLibDir()}/libphp.a")) { + return; + } + $ar = getenv('AR') ?: 'ar'; + $libphp_a = "{$package->getLibDir()}/libphp.a"; + shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}"); + UnixUtil::exportDynamicSymbols($libphp_a); + + // deploy embed php scripts + $package->runStage([$this, 'patchEmbedScripts']); + } + + #[Stage] + public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void + { + // collect shared extensions + /** @var PhpExtensionPackage[] $shared_extensions */ + $shared_extensions = array_filter( + $installer->getResolvedPackages(PhpExtensionPackage::class), + fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp() + ); + if (!empty($shared_extensions)) { + if ($toolchain->isStatic()) { + throw new WrongUsageException( + "You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" . + 'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" . + 'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.' + ); + } + FileSystem::createDir(BUILD_MODULES_PATH); + + // backup + FileSystem::backupFile(BUILD_BIN_PATH . '/php-config'); + FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4'); + + FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"'); + FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes'); + } + + try { + logger()->debug('Building shared extensions...'); + foreach ($shared_extensions as $extension) { + InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); + $extension->buildShared(); + } + } finally { + // restore php-config + if (!empty($shared_extensions)) { + FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config'); + FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4'); + } + } + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(TargetPackage $package): void + { + // virtual target, do nothing + if ($package->getName() !== 'php') { + return; + } + + $package->runStage([$this, 'buildconfForUnix']); + $package->runStage([$this, 'configureForUnix']); + $package->runStage([$this, 'makeForUnix']); + + $package->runStage([$this, 'unixBuildSharedExt']); + } + + /** + * Patch phpize and php-config if needed + */ + #[Stage] + public function patchUnixEmbedScripts(): void + { + // patch phpize + if (file_exists(BUILD_BIN_PATH . '/phpize')) { + logger()->debug('Patching phpize prefix'); + FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'"); + FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#'); + $this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize'); + } + // patch php-config + if (file_exists(BUILD_BIN_PATH . '/php-config')) { + logger()->debug('Patching php-config prefix and libs order'); + $php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config'); + $php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str); + // move mimalloc to the beginning of libs + $php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str); + // move lstdc++ to the end of libs + $php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str); + FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str); + $this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config'); + } + } + + /** + * Seek php-src/config.log when building PHP, add it to exception. + */ + protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void + { + try { + $callback(); + } catch (SPCException $e) { + if (file_exists("{$source_dir}/config.log")) { + $e->addExtraLogFile('php-src config.log', 'php-src.config.log'); + copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log'); + } + throw $e; + } + } + + /** + * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. + */ + private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void + { + $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; + $libDir = BUILD_LIB_PATH; + $modulesDir = BUILD_MODULES_PATH; + $realLibName = 'libphp.so'; + $cwd = getcwd(); + + if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) { + $release = $matches[1]; + $realLibName = "libphp-{$release}.so"; + $libphpRelease = "{$libDir}/{$realLibName}"; + if (!file_exists($libphpRelease) && file_exists($libphpSo)) { + rename($libphpSo, $libphpRelease); + } + if (file_exists($libphpRelease)) { + chdir($libDir); + if (file_exists($libphpSo)) { + unlink($libphpSo); + } + symlink($realLibName, 'libphp.so'); + shell()->exec(sprintf( + 'patchelf --set-soname %s %s', + escapeshellarg($realLibName), + escapeshellarg($libphpRelease) + )); + } + if (is_dir($modulesDir)) { + chdir($modulesDir); + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if (!$ext->isBuildShared()) { + continue; + } + $name = $ext->getName(); + $versioned = "{$name}-{$release}.so"; + $unversioned = "{$name}.so"; + $src = "{$modulesDir}/{$versioned}"; + $dst = "{$modulesDir}/{$unversioned}"; + if (is_file($src)) { + rename($src, $dst); + shell()->exec(sprintf( + 'patchelf --set-soname %s %s', + escapeshellarg($unversioned), + escapeshellarg($dst) + )); + } + } + } + chdir($cwd); + } + + $target = "{$libDir}/{$realLibName}"; + if (file_exists($target)) { + [, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target)); + $output = implode("\n", $output); + if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) { + $currentSoname = $sonameMatch[1]; + if ($currentSoname !== basename($target)) { + shell()->exec(sprintf( + 'patchelf --set-soname %s %s', + escapeshellarg(basename($target)), + escapeshellarg($target) + )); + } + } + } + } + + /** + * Make environment variables for php make. + * This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking. + */ + private function makeVars(PackageInstaller $installer): array + { + $config = (new SPCConfigUtil(['libs_only_deps' => true]))->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; + $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; + + return array_filter([ + 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}", + 'EXTRA_LDFLAGS' => $config['ldflags'], + 'EXTRA_LIBS' => $config['libs'], + ]); + } +} diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php new file mode 100644 index 000000000..289526379 --- /dev/null +++ b/src/Package/Target/php/windows.php @@ -0,0 +1,228 @@ +cd($package->getSourceDir())->exec('.\buildconf.bat'); + } + + #[Stage] + public function configureForWindows(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure.bat')); + V2CompatLayer::emitPatchPoint('before-php-configure'); + $args = [ + '--disable-all', + "--with-php-build={$package->getBuildRootPath()}", + "--with-extra-includes={$package->getIncludeDir()}", + "--with-extra-libs={$package->getLibDir()}", + ]; + // sapis + $cli = $installer->isPackageResolved('php-cli'); + $cgi = $installer->isPackageResolved('php-cgi'); + $micro = $installer->isPackageResolved('php-micro'); + $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; + $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; + $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; + + // zts + $args[] = $package->getBuildOption('enable-zts', false) ? '--enable-zts=yes' : '--enable-zts=no'; + // opcache-jit + $args[] = !$package->getBuildOption('disable-opcache-jit', false) ? '--enable-opcache-jit=yes' : '--enable-opcache-jit=no'; + // micro win32 + if ($micro && $package->getBuildOption('enable-micro-win32', false)) { + $args[] = '--enable-micro-win32=yes'; + } + // config-file-scan-dir + if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) { + $args[] = "--with-config-file-scan-dir={$option}"; + } + // micro logo + if ($micro && ($logo = $this->getBuildOption('with-micro-logo')) !== null) { + $args[] = "--enable-micro-logo={$logo}"; + copy($logo, SOURCE_PATH . '\php-src\\' . $logo); + } + $args = implode(' ', $args); + $static_extension_str = $this->makeStaticExtensionString($installer); + cmd()->cd($package->getSourceDir())->exec(".\\configure.bat {$args} {$static_extension_str}"); + } + + #[BeforeStage('php', [self::class, 'makeCliForWindows'])] + #[PatchDescription('Patch Windows Makefile for CLI target')] + public function patchCLITarget(TargetPackage $package): void + { + // search Makefile code line contains "$(BUILD_DIR)\php.exe:" + $content = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); + $lines = explode("\r\n", $content); + $line_num = 0; + $found = false; + foreach ($lines as $v) { + if (str_contains($v, '$(BUILD_DIR)\php.exe:')) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new PatchException('Windows Makefile patching for php.exe target', 'Cannot patch windows CLI Makefile, Makefile does not contain "$(BUILD_DIR)\php.exe:" line'); + } + $lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest'; + $lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); + } + + #[Stage] + public function makeCliForWindows(TargetPackage $package, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('nmake php-cli')); + + // extra lib + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + + // Add debug symbols for release build if --no-strip is specified + // We need to modify CFLAGS to replace /Ox with /Zi and add /DEBUG to LDFLAGS + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + // Read current CFLAGS from Makefile and replace optimization flags + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + // Replace /Ox (full optimization) with /Zi (debug info) and /Od (disable optimization) + // Keep optimization for speed: /O2 /Zi instead of /Od /Zi + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CLI=/DEBUG" '; + } + } + + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php.exe"); + + $this->deployWindowsBinary($builder, $package, 'php-cli'); + } + + #[Stage] + public function makeForWindows(TargetPackage $package, PackageInstaller $installer): void + { + V2CompatLayer::emitPatchPoint('before-php-make'); + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('nmake clean')); + cmd()->cd($package->getSourceDir())->exec('nmake clean'); + + if ($installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'makeCliForWindows']); + } + if ($installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'makeCgiForWindows']); + } + if ($installer->isPackageResolved('php-micro')) { + $package->runStage([$this, 'makeMicroForWindows']); + } + } + + #[BuildFor('Windows')] + public function buildWin(TargetPackage $package): void + { + if ($package->getName() !== 'php') { + return; + } + + $package->runStage([$this, 'buildconfForWindows']); + $package->runStage([$this, 'configureForWindows']); + $package->runStage([$this, 'makeForWindows']); + } + + #[BeforeStage('php', [self::class, 'buildconfForWindows'])] + #[PatchDescription('Patch SPC_MICRO_PATCHES defined patches')] + #[PatchDescription('Fix PHP 8.1 static build bug on Windows')] + #[PatchDescription('Fix PHP Visual Studio version detection')] + public function patchBeforeBuildconfForWindows(TargetPackage $package): void + { + // php-src patches from micro + SourcePatcher::patchPhpSrc(); + + // php 8.1 bug + if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) { + logger()->info('Patching PHP 8.1 windows Fiber bug'); + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\win32\\build\\config.w32", + "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", + "ADD_FLAG('ASM_OBJS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj $(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');" + ); + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\win32\\build\\config.w32", + "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", + '' + ); + } + + // Fix PHP VS version + // get vs version + $vc = WindowsUtil::findVisualStudio(); + $vc_matches = match ($vc['major_version']) { + '17' => ['VS17', 'Visual C++ 2022'], + '16' => ['VS16', 'Visual C++ 2019'], + default => ['unknown', 'unknown'], + }; + // patch php-src/win32/build/confutils.js + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\win32\\build\\confutils.js", + 'var name = "unknown";', + "var name = short ? \"{$vc_matches[0]}\" : \"{$vc_matches[1]}\";return name;" + ); + + // patch micro win32 + if ($package->getBuildOption('enable-micro-win32') && !file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) { + copy("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", "{$package->getSourceDir()}\\php-src\\sapi\\micro\\php_micro.c.win32bak"); + FileSystem::replaceFileStr("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1"); + } else { + if (file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) { + rename("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c"); + } + } + } + + protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $package, string $sapi): void + { + $rel_type = 'Release'; // TODO: Debug build support + $ts = $builder->getOption('enable-zts') ? '_TS' : ''; + $debug_dir = BUILD_ROOT_PATH . '\debug'; + $src = match ($sapi) { + 'php-cli' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'], + 'php-micro' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'], + 'php-cgi' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'], + default => throw new SPCInternalException("Deployment does not accept type {$sapi}"), + }; + $src = "{$src[0]}\\{$src[1]}"; + $dst = BUILD_BIN_PATH . '\\' . basename($src); + + $builder->deployBinary($src, $dst); + + // make debug info file path + if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { + cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg($debug_dir)); + } + } +} From 6d292b4c544867b567b856325f226063cefba579 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 14:24:59 +0800 Subject: [PATCH 10/21] Add WindowsCMakeExecutor --- src/Package/Library/onig.php | 24 ++ .../Runtime/Executor/WindowsCMakeExecutor.php | 224 ++++++++++++++++++ src/StaticPHP/Runtime/Shell/Shell.php | 11 +- src/StaticPHP/Runtime/Shell/WindowsCmd.php | 14 +- 4 files changed, 258 insertions(+), 15 deletions(-) create mode 100644 src/Package/Library/onig.php create mode 100644 src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php diff --git a/src/Package/Library/onig.php b/src/Package/Library/onig.php new file mode 100644 index 000000000..2cea572bf --- /dev/null +++ b/src/Package/Library/onig.php @@ -0,0 +1,24 @@ +addConfigureArgs('-DMSVC_STATIC_RUNTIME=ON') + ->build(); + FileSystem::copy("{$package->getLibDir()}\\onig.lib", "{$package->getLibDir()}\\onig_a.lib"); + } +} diff --git a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php new file mode 100644 index 000000000..1fb9c72be --- /dev/null +++ b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php @@ -0,0 +1,224 @@ +package); + if ($builder !== null) { + $this->builder = $builder; + } elseif (ApplicationContext::has(PackageBuilder::class)) { + $this->builder = ApplicationContext::get(PackageBuilder::class); + } else { + throw new SPCInternalException('PackageBuilder not found in ApplicationContext.'); + } + $this->installer = ApplicationContext::get(PackageInstaller::class); + $this->initCmd(); + + // judge that this package has artifact.source and defined build stage + if (!$this->package->hasStage('build')) { + throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); + } + } + + public function build(): static + { + $this->initBuildDir(); + + if ($this->reset) { + FileSystem::resetDir($this->build_dir); + } + + // configure + if ($this->steps >= 1) { + $args = array_merge($this->configure_args, $this->getDefaultCMakeArgs()); + $args = array_diff($args, $this->ignore_args); + $configure_args = implode(' ', $args); + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake configure)')); + $this->cmd->exec("cmake {$configure_args}"); + } + + // make + if ($this->steps >= 2) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake build)')); + $this->cmd->cd($this->build_dir)->exec("cmake --build {$this->build_dir} --config Release --target install -j{$this->builder->concurrency}"); + } + + return $this; + } + + /** + * Add optional package configuration. + * This method checks if a package is available and adds the corresponding arguments to the CMake configuration. + * + * @param string $name package name to check + * @param \Closure|string $true_args arguments to use if the package is available (allow closure, returns string) + * @param string $false_args arguments to use if the package is not available + * @return $this + */ + public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static + { + if ($get = $this->installer->getResolvedPackages()[$name] ?? null) { + logger()->info("Building package [{$this->package->getName()}] with {$name} support"); + $args = $true_args instanceof \Closure ? $true_args($get) : $true_args; + } else { + logger()->info("Building package [{$this->package->getName()}] without {$name} support"); + $args = $false_args; + } + $this->addConfigureArgs($args); + return $this; + } + + /** + * Add configure args. + */ + public function addConfigureArgs(...$args): static + { + $this->configure_args = [...$this->configure_args, ...$args]; + return $this; + } + + /** + * Remove some configure args, to bypass the configure option checking for some libs. + */ + public function removeConfigureArgs(...$args): static + { + $this->ignore_args = [...$this->ignore_args, ...$args]; + return $this; + } + + public function setEnv(array $env): static + { + $this->cmd->setEnv($env); + return $this; + } + + public function appendEnv(array $env): static + { + $this->cmd->appendEnv($env); + return $this; + } + + /** + * To build steps. + * + * @param int $step Step number, accept 1-3 + * @return $this + */ + public function toStep(int $step): static + { + $this->steps = $step; + return $this; + } + + /** + * Set custom CMake build directory. + * + * @param string $dir custom CMake build directory + */ + public function setBuildDir(string $dir): static + { + $this->build_dir = $dir; + return $this; + } + + /** + * Set the custom default args. + */ + public function setCustomDefaultArgs(...$args): static + { + $this->custom_default_args = $args; + return $this; + } + + /** + * Set the reset status. + * If we set it to false, it will not clean and create the specified cmake working directory. + */ + public function setReset(bool $reset): static + { + $this->reset = $reset; + return $this; + } + + /** + * Get configure argument string. + */ + public function getConfigureArgsString(): string + { + return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs())); + } + + /** + * Returns the default CMake args. + */ + private function getDefaultCMakeArgs(): array + { + return $this->custom_default_args ?? [ + '-A x64', + '-DCMAKE_BUILD_TYPE=Release', + '-DBUILD_SHARED_LIBS=OFF', + '-DBUILD_STATIC_LIBS=ON', + "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", + '-DCMAKE_INSTALL_PREFIX=' . escapeshellarg($this->package->getBuildRootPath()), + '-B ' . escapeshellarg(FileSystem::convertPath($this->build_dir)), + ]; + } + + private function makeCmakeToolchainFile(): string + { + if (file_exists(SOURCE_PATH . '\toolchain.cmake')) { + return SOURCE_PATH . '\toolchain.cmake'; + } + return WindowsUtil::makeCmakeToolchainFile(); + } + + /** + * Initialize the CMake build directory. + * If the directory is not set, it defaults to the package's source directory with '/build' appended. + */ + private function initBuildDir(): void + { + if ($this->build_dir === null) { + $this->build_dir = "{$this->package->getSourceDir()}\\build"; + } + } + + private function initCmd(): void + { + $this->cmd = cmd()->cd($this->package->getSourceDir()); + } +} diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index bf30d9d82..7300b8e18 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -149,7 +149,8 @@ protected function passthru( ?string $original_command = null, bool $capture_output = false, bool $throw_on_error = true, - ?string $cwd = null + ?string $cwd = null, + ?array $env = null, ): array { $file_res = null; if ($this->enable_log_file) { @@ -164,7 +165,13 @@ protected function passthru( 1 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stdout 2 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stderr ]; - $process = proc_open($cmd, $descriptors, $pipes, $cwd); + if ($env !== null && $env !== []) { + // merge current PHP envs + $env = array_merge(getenv(), $env); + } else { + $env = null; + } + $process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env); $output_value = ''; try { diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index a60c41b23..e9d7a6c0d 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -45,23 +45,11 @@ public function execWithResult(string $cmd, bool $with_log = true): array logger()->debug('Running command with result: ' . $cmd); } $cmd = $this->getExecString($cmd); - $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd); + $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env); $out = explode("\n", $result['output']); return [$result['code'], $out]; } - public function setEnv(array $env): static - { - // windows currently does not support setting environment variables - throw new SPCInternalException('Windows does not support setting environment variables in shell commands.'); - } - - public function appendEnv(array $env): static - { - // windows currently does not support appending environment variables - throw new SPCInternalException('Windows does not support appending environment variables in shell commands.'); - } - public function getLastCommand(): string { return $this->last_cmd; From e3f9894331fabed3e1823234e13db28c79b4a60b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 14:43:42 +0800 Subject: [PATCH 11/21] Apply copilot's suggestion --- src/StaticPHP/Command/Dev/EnvCommand.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/Dev/EnvCommand.php b/src/StaticPHP/Command/Dev/EnvCommand.php index 2f2dbcf0e..160504ed9 100644 --- a/src/StaticPHP/Command/Dev/EnvCommand.php +++ b/src/StaticPHP/Command/Dev/EnvCommand.php @@ -15,7 +15,7 @@ class EnvCommand extends BaseCommand { public function configure(): void { - $this->addArgument('env', InputArgument::REQUIRED, 'The environment variable to show, if not set, all will be shown'); + $this->addArgument('env', InputArgument::OPTIONAL, 'The environment variable to show, if not set, all will be shown'); } public function initialize(InputInterface $input, OutputInterface $output): void @@ -31,6 +31,12 @@ public function handle(): int $this->output->writeln("Environment variable '{$env}' is not set."); return static::FAILURE; } + if (is_array($val)) { + foreach ($val as $k => $v) { + $this->output->writeln("{$k}={$v}"); + } + return static::SUCCESS; + } $this->output->writeln("{$val}"); return static::SUCCESS; } From c4cec15c188d05e8f3e826345f598406b0d0f2bc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 14:45:35 +0800 Subject: [PATCH 12/21] Use container instead of passing --- .../Runtime/Executor/WindowsCMakeExecutor.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php index 1fb9c72be..9e0978196 100644 --- a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php @@ -35,16 +35,10 @@ class WindowsCMakeExecutor extends Executor protected PackageInstaller $installer; - public function __construct(protected LibraryPackage $package, ?PackageBuilder $builder = null) + public function __construct(protected LibraryPackage $package) { parent::__construct($this->package); - if ($builder !== null) { - $this->builder = $builder; - } elseif (ApplicationContext::has(PackageBuilder::class)) { - $this->builder = ApplicationContext::get(PackageBuilder::class); - } else { - throw new SPCInternalException('PackageBuilder not found in ApplicationContext.'); - } + $this->builder = ApplicationContext::get(PackageBuilder::class); $this->installer = ApplicationContext::get(PackageInstaller::class); $this->initCmd(); From da8b7c2bc453f36c49b097f938fdd047fe69f8ac Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 14:45:56 +0800 Subject: [PATCH 13/21] Use the real build target to display --- src/Package/Target/php/windows.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 289526379..c2ce2500a 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -98,7 +98,7 @@ public function patchCLITarget(TargetPackage $package): void #[Stage] public function makeCliForWindows(TargetPackage $package, PackageBuilder $builder): void { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('nmake php-cli')); + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); // extra lib $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; @@ -215,14 +215,14 @@ protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $p 'php-cgi' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'], default => throw new SPCInternalException("Deployment does not accept type {$sapi}"), }; - $src = "{$src[0]}\\{$src[1]}"; - $dst = BUILD_BIN_PATH . '\\' . basename($src); + $src_file = "{$src[0]}\\{$src[1]}"; + $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); - $builder->deployBinary($src, $dst); + $builder->deployBinary($src_file, $dst_file); // make debug info file path if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { - cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg($debug_dir)); + FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); } } } From 4e841cfc67888a6bf08158d891b45b1f57971a3f Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Thu, 11 Dec 2025 14:47:14 +0800 Subject: [PATCH 14/21] Update src/Package/Target/php/windows.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Target/php/windows.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 289526379..7e22542d8 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -181,11 +181,15 @@ public function patchBeforeBuildconfForWindows(TargetPackage $package): void // Fix PHP VS version // get vs version $vc = WindowsUtil::findVisualStudio(); - $vc_matches = match ($vc['major_version']) { - '17' => ['VS17', 'Visual C++ 2022'], - '16' => ['VS16', 'Visual C++ 2019'], - default => ['unknown', 'unknown'], - }; + if ($vc === false) { + $vc_matches = ['unknown', 'unknown']; + } else { + $vc_matches = match ($vc['major_version']) { + '17' => ['VS17', 'Visual C++ 2022'], + '16' => ['VS16', 'Visual C++ 2019'], + default => ['unknown', 'unknown'], + }; + } // patch php-src/win32/build/confutils.js FileSystem::replaceFileStr( "{$package->getSourceDir()}\\win32\\build\\confutils.js", From 9a91aecb2843199a60911bfaf83dd38ca16b0670 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Thu, 11 Dec 2025 14:47:44 +0800 Subject: [PATCH 15/21] Update src/Package/Target/php/windows.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Target/php/windows.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 7e22542d8..765f6ebd2 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -199,7 +199,7 @@ public function patchBeforeBuildconfForWindows(TargetPackage $package): void // patch micro win32 if ($package->getBuildOption('enable-micro-win32') && !file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) { - copy("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", "{$package->getSourceDir()}\\php-src\\sapi\\micro\\php_micro.c.win32bak"); + copy("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak"); FileSystem::replaceFileStr("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1"); } else { if (file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) { From a4fd618a1008313aa55e933d878f145e53dceafe Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Thu, 11 Dec 2025 14:49:16 +0800 Subject: [PATCH 16/21] Update src/StaticPHP/Artifact/Artifact.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Artifact.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index bcf6ca622..b5cf74c00 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -171,7 +171,7 @@ public function isBinaryExtracted(?string $target_os = null, bool $compare_hash $target_path = $extract_config['path']; // Check if target is a file or directory - $is_file_target = !is_dir($target_path) && str_contains($target_path, '.'); + $is_file_target = !is_dir($target_path) && (pathinfo($target_path, PATHINFO_EXTENSION) !== ''); if ($is_file_target) { // For single file extraction (e.g., vswhere.exe) From 63c7aa8d38e2eb875a669548706550d0cb67670f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 14:53:16 +0800 Subject: [PATCH 17/21] Update captainhook.json to cross-platform friendly --- captainhook.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captainhook.json b/captainhook.json index 63d88f065..6a7b2f4f7 100644 --- a/captainhook.json +++ b/captainhook.json @@ -3,7 +3,7 @@ "enabled": true, "actions": [ { - "action": ".\\vendor\\bin\\phpstan analyse --memory-limit 300M" + "action": "vendor/bin/phpstan analyse --memory-limit 300M" } ] }, @@ -11,7 +11,7 @@ "enabled": true, "actions": [ { - "action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential", + "action": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential", "conditions": [ { "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", From f8952da2a34fd6b36045bd910b6778846f23843f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 14:54:30 +0800 Subject: [PATCH 18/21] Update captainhook.json to cross-platform friendly --- captainhook.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captainhook.json b/captainhook.json index 6a7b2f4f7..233e387eb 100644 --- a/captainhook.json +++ b/captainhook.json @@ -3,7 +3,7 @@ "enabled": true, "actions": [ { - "action": "vendor/bin/phpstan analyse --memory-limit 300M" + "action": "php vendor/bin/phpstan analyse --memory-limit 300M" } ] }, @@ -11,7 +11,7 @@ "enabled": true, "actions": [ { - "action": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential", + "action": "php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential", "conditions": [ { "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", From 88d135a4e5d23ed51a91e59991199b8ae19b386e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 15:50:39 +0800 Subject: [PATCH 19/21] Allow interrupt on Windows --- src/StaticPHP/Runtime/Shell/Shell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index 7300b8e18..2d0d90b8c 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -171,7 +171,7 @@ protected function passthru( } else { $env = null; } - $process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env); + $process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env, options: PHP_OS_FAMILY === 'Windows' ? ['create_process_group' => true] : null); $output_value = ''; try { From fefcbf4029d89c5628c190565ed155894565c024 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 15:51:32 +0800 Subject: [PATCH 20/21] Allow automatically get latest gRPC source (#909) --- config/artifact.json | 2 +- .../Artifact/Downloader/Type/Git.php | 55 ++++++++++++++++++- src/StaticPHP/Config/ConfigValidator.php | 2 +- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/config/artifact.json b/config/artifact.json index 75ee9cfc1..c8fd66215 100644 --- a/config/artifact.json +++ b/config/artifact.json @@ -399,7 +399,7 @@ "binary": "hosted", "source": { "type": "git", - "rev": "v1.75.x", + "regex": "v(?1.\\d+).x", "url": "https://github.com/grpc/grpc.git" } }, diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 8b1f20d34..83c236eb4 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -6,6 +6,8 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Exception\DownloaderException; +use StaticPHP\Util\FileSystem; /** git */ class Git implements DownloadTypeInterface @@ -15,8 +17,55 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $path = DOWNLOAD_PATH . "/{$name}"; logger()->debug("Cloning git repository for {$name} from {$config['url']}"); $shallow = !$downloader->getOption('no-shallow-clone', false); - default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $version = "dev-{$config['rev']}"; - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + + // direct branch clone + if (isset($config['rev'])) { + default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); + $version = "dev-{$config['rev']}"; + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + } + if (!isset($config['regex'])) { + throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); + } + + // regex matches branch first, we need to fetch all refs in emptyfirst + $gitdir = sys_get_temp_dir() . '/' . $name; + FileSystem::resetDir($gitdir); + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->cd($gitdir) + ->exec(SPC_GIT_EXEC . ' init') + ->exec(SPC_GIT_EXEC . ' remote add origin ' . escapeshellarg($config['url'])) + ->execWithResult(SPC_GIT_EXEC . ' ls-remote origin'); + if ($result[0] !== 0) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $refs = $result[1]; + $matched_version_branch = []; + $matched_count = 0; + + $regex = '/^' . $config['regex'] . '$/'; + foreach ($refs as $ref) { + $matches = null; + if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) { + ++$matched_count; + $branch = $matches[1]; + if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) { + $matched_version_branch[$vermatch['version']] = $vermatch[0]; + } + } + } + // sort versions + uksort($matched_version_branch, function ($a, $b) { + return version_compare($b, $a); + }); + if (!empty($matched_version_branch)) { + // use the highest version + $version = array_key_first($matched_version_branch); + $branch = $matched_version_branch[$version]; + logger()->info("Matched version {$version} from branch {$branch} for {$name}"); + default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + } + throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); } } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 4de0529f0..b32f41063 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -79,7 +79,7 @@ class ConfigValidator public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] 'filelist' => [['url', 'regex'], ['extract']], - 'git' => [['url', 'rev'], ['extract', 'submodules']], + 'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']], 'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']], 'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']], 'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']], From 910f10a1ddd6b47392e90bbb531b3a1a9317c598 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 16:04:29 +0800 Subject: [PATCH 21/21] Typo --- src/StaticPHP/Doctor/Item/OSCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Doctor/Item/OSCheck.php b/src/StaticPHP/Doctor/Item/OSCheck.php index 9e1c1809c..7bd19df81 100644 --- a/src/StaticPHP/Doctor/Item/OSCheck.php +++ b/src/StaticPHP/Doctor/Item/OSCheck.php @@ -10,7 +10,7 @@ class OSCheck { - #[CheckItem('if current OS are supported', level: 1000)] + #[CheckItem('if current OS is supported', level: 1000)] public function checkOS(): ?CheckResult { if (!in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'Windows'])) {