From e316971764eef0de65b650e1aac052f0c9651842 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 29 Nov 2025 14:08:34 +0800 Subject: [PATCH 001/682] Fix cross-device warning in rename function --- src/SPC/store/FileSystem.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 86daefc43..3b88a2bce 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -660,11 +660,19 @@ private static function moveFileOrDir(string $source, string $dest): void $source = self::convertPath($source); $dest = self::convertPath($dest); - // Try rename first (fast, atomic) - if (@rename($source, $dest)) { - return; + // Check if source and dest are on the same device to avoid cross-device rename errors + $source_stat = @stat($source); + $dest_parent = dirname($dest); + $dest_stat = @stat($dest_parent); + + // Only use rename if on same device + if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) { + if (@rename($source, $dest)) { + return; + } } + // Fall back to copy + delete for cross-device moves or if rename failed if (is_dir($source)) { self::copyDir($source, $dest); self::removeDir($source); From e6591ffe9c690eff2ba73ee464f54eb7381c7c0c Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 29 Nov 2025 12:43:19 +0100 Subject: [PATCH 002/682] add pcov extension (shared only, like xdebug) --- config/ext.json | 7 +++++++ config/source.json | 9 +++++++++ src/globals/test-extensions.php | 20 ++++++++++---------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/config/ext.json b/config/ext.json index e8f69339e..d3fd2aa27 100644 --- a/config/ext.json +++ b/config/ext.json @@ -567,6 +567,13 @@ "type": "builtin", "unix-only": true }, + "pcov": { + "type": "external", + "source": "pcov", + "target": [ + "shared" + ] + }, "pdo": { "type": "builtin" }, diff --git a/config/source.json b/config/source.json index 06aa5c6f5..9a80cd059 100644 --- a/config/source.json +++ b/config/source.json @@ -963,6 +963,15 @@ "path": "LICENSE" } }, + "pcov": { + "type": "url", + "url": "https://pecl.php.net/get/pcov", + "filename": "pcov.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "pdo_sqlsrv": { "type": "url", "url": "https://pecl.php.net/get/pdo_sqlsrv", diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 1d31d5202..f44914ece 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,9 +13,9 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', - '8.2', - '8.3', + // '8.1', + // '8.2', + // '8.3', '8.4', '8.5', // 'git', @@ -25,17 +25,17 @@ $test_os = [ 'macos-15-intel', // bin/spc for x86_64 'macos-15', // bin/spc for arm64 - 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 + // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - 'ubuntu-24.04', // bin/spc for x86_64 - 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + // 'ubuntu-24.04', // bin/spc for x86_64 + // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 // 'windows-2025', ]; // whether enable thread safe -$zts = false; +$zts = true; $no_strip = false; @@ -50,14 +50,14 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'maxminddb', + 'Linux', 'Darwin' => 'bcmath', 'Windows' => 'bcmath', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => '', - 'Darwin' => '', + 'Linux' => 'pcov', + 'Darwin' => 'pcov', 'Windows' => '', }; From 14bfb4198a88347a1d271c78da60467880a3d604 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 30 Nov 2025 15:35:04 +0800 Subject: [PATCH 003/682] v3 base --- .github/workflows/tests.yml | 2 +- .gitignore | 8 +- bin/spc | 23 +- bin/spc-debug | 4 + composer.json | 11 +- composer.lock | 696 ++++---- config/artifact.json | 1046 +++++++++++ config/env.ini | 27 +- config/pkg.ext.json | 1541 +++++++++++++++++ config/pkg.json | 105 -- config/pkg.lib.json | 989 +++++++++++ config/pkg.target.json | 95 + phpstan.neon | 1 + spc.registry.json | 32 + src/Package/Artifact/go_xcaddy.php | 82 + .../Command/SwitchPhpVersionCommand.php | 123 ++ src/Package/Library/libiconv.php | 27 + src/Package/Library/libxml2.php | 54 + src/Package/Library/postgresql.php | 22 + src/Package/README.md | 3 + src/Package/Target/go_xcaddy.php | 24 + src/Package/Target/php.php | 397 +++++ src/Package/Target/pkgconfig.php | 45 + src/StaticPHP/Artifact/Artifact.php | 544 ++++++ src/StaticPHP/Artifact/ArtifactCache.php | 305 ++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 665 +++++++ src/StaticPHP/Artifact/ArtifactExtractor.php | 619 +++++++ src/StaticPHP/Artifact/ArtifactLoader.php | 190 ++ .../Artifact/Downloader/DownloadResult.php | 146 ++ .../Artifact/Downloader/Type/BitBucketTag.php | 41 + .../Downloader/Type/DownloadTypeInterface.php | 18 + .../Artifact/Downloader/Type/FileList.php | 46 + .../Artifact/Downloader/Type/Git.php | 22 + .../Downloader/Type/GitHubRelease.php | 98 ++ .../Downloader/Type/GitHubTarball.php | 78 + .../Downloader/Type/GitHubTokenSetupTrait.php | 22 + .../Artifact/Downloader/Type/LocalDir.php | 18 + .../Artifact/Downloader/Type/PIE.php | 47 + .../Artifact/Downloader/Type/PhpRelease.php | 76 + .../Artifact/Downloader/Type/Url.php | 23 + .../Downloader/Type/ValidatorInterface.php | 13 + src/StaticPHP/Artifact/DownloaderOptions.php | 103 ++ .../Attribute/Artifact/AfterBinaryExtract.php | 63 + .../Attribute/Artifact/AfterSourceExtract.php | 50 + .../Attribute/Artifact/BinaryExtract.php | 45 + .../Attribute/Artifact/CustomBinary.php | 11 + .../Attribute/Artifact/CustomSource.php | 11 + .../Attribute/Artifact/SourceExtract.php | 39 + src/StaticPHP/Attribute/Doctor/CheckItem.php | 18 + src/StaticPHP/Attribute/Doctor/FixItem.php | 14 + .../Attribute/Doctor/OptionalCheck.php | 11 + .../Attribute/Package/AfterStage.php | 14 + .../Attribute/Package/BeforeStage.php | 14 + src/StaticPHP/Attribute/Package/BuildFor.php | 17 + .../Package/CustomPhpConfigureArg.php | 14 + src/StaticPHP/Attribute/Package/Extension.php | 14 + src/StaticPHP/Attribute/Package/Info.php | 8 + .../Attribute/Package/InitPackage.php | 8 + src/StaticPHP/Attribute/Package/Library.php | 14 + .../Attribute/Package/PatchBeforeBuild.php | 11 + .../Attribute/Package/ResolveBuild.php | 11 + src/StaticPHP/Attribute/Package/Stage.php | 14 + src/StaticPHP/Attribute/Package/Target.php | 14 + src/StaticPHP/Attribute/Package/Validate.php | 8 + src/StaticPHP/Attribute/PatchDescription.php | 11 + src/StaticPHP/Command/BaseCommand.php | 180 ++ src/StaticPHP/Command/BuildLibsCommand.php | 29 + src/StaticPHP/Command/BuildTargetCommand.php | 56 + src/StaticPHP/Command/DoctorCommand.php | 34 + src/StaticPHP/Command/DownloadCommand.php | 116 ++ src/StaticPHP/Command/ExtractCommand.php | 108 ++ .../Command/InstallPackageCommand.php | 27 + src/StaticPHP/Command/SPCConfigCommand.php | 56 + src/StaticPHP/Config/ArtifactConfig.php | 68 + src/StaticPHP/Config/ConfigType.php | 52 + src/StaticPHP/Config/ConfigValidator.php | 370 ++++ src/StaticPHP/Config/PackageConfig.php | 102 ++ src/StaticPHP/ConsoleApplication.php | 64 + src/StaticPHP/DI/ApplicationContext.php | 195 +++ src/StaticPHP/DI/CallbackInvoker.php | 98 ++ src/StaticPHP/Doctor/CheckResult.php | 46 + src/StaticPHP/Doctor/Doctor.php | 161 ++ src/StaticPHP/Doctor/DoctorLoader.php | 123 ++ src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 73 + src/StaticPHP/Doctor/Item/MacOSToolCheck.php | 106 ++ src/StaticPHP/Doctor/Item/OSCheck.php | 23 + src/StaticPHP/Doctor/Item/PkgConfigCheck.php | 53 + .../Doctor/Item/Re2cVersionCheck.php | 35 + .../Exception/BuildFailureException.php | 13 + .../Exception/DownloaderException.php | 13 + .../Exception/EnvironmentException.php | 25 + src/StaticPHP/Exception/ExceptionHandler.php | 228 +++ .../Exception/ExecutionException.php | 58 + .../Exception/FileSystemException.php | 7 + .../Exception/InterruptException.php | 10 + src/StaticPHP/Exception/PatchException.php | 25 + src/StaticPHP/Exception/SPCException.php | 147 ++ .../Exception/SPCInternalException.php | 12 + .../Exception/ValidationException.php | 61 + .../Exception/WrongUsageException.php | 13 + src/StaticPHP/Package/LibraryPackage.php | 188 ++ src/StaticPHP/Package/Package.php | 162 ++ src/StaticPHP/Package/PackageBuilder.php | 104 ++ .../Package/PackageCallbacksTrait.php | 89 + src/StaticPHP/Package/PackageInstaller.php | 534 ++++++ src/StaticPHP/Package/PackageLoader.php | 280 +++ src/StaticPHP/Package/PhpExtensionPackage.php | 110 ++ src/StaticPHP/Package/TargetPackage.php | 142 ++ src/StaticPHP/Registry/Registry.php | 265 +++ src/StaticPHP/Runtime/Executor/Executor.php | 17 + .../Runtime/Executor/UnixAutoconfExecutor.php | 198 +++ .../Runtime/Executor/UnixCMakeExecutor.php | 333 ++++ src/StaticPHP/Runtime/Shell/DefaultShell.php | 191 ++ src/StaticPHP/Runtime/Shell/Shell.php | 259 +++ src/StaticPHP/Runtime/Shell/UnixShell.php | 91 + src/StaticPHP/Runtime/Shell/WindowsCmd.php | 156 ++ src/StaticPHP/Runtime/SystemTarget.php | 132 ++ .../Toolchain/ClangNativeToolchain.php | 57 + .../Toolchain/GccNativeToolchain.php | 54 + .../Interface/ToolchainInterface.php | 43 + .../Interface/UnixToolchainInterface.php | 7 + src/StaticPHP/Toolchain/MSVCToolchain.php | 24 + src/StaticPHP/Toolchain/MuslToolchain.php | 56 + src/StaticPHP/Toolchain/ToolchainManager.php | 92 + src/StaticPHP/Toolchain/ZigToolchain.php | 108 ++ src/StaticPHP/Util/DependencyResolver.php | 160 ++ src/StaticPHP/Util/FileSystem.php | 495 ++++++ src/StaticPHP/Util/GlobalEnvManager.php | 178 ++ src/StaticPHP/Util/InteractiveTerm.php | 112 ++ src/StaticPHP/Util/PkgConfigUtil.php | 124 ++ src/StaticPHP/Util/SPCConfigUtil.php | 302 ++++ src/StaticPHP/Util/SourcePatcher.php | 162 ++ src/StaticPHP/Util/System/LinuxUtil.php | 148 ++ src/StaticPHP/Util/System/MacOSUtil.php | 42 + src/StaticPHP/Util/System/UnixUtil.php | 128 ++ src/StaticPHP/Util/System/WindowsUtil.php | 106 ++ src/StaticPHP/Util/V2CompatLayer.php | 140 ++ src/bootstrap.php | 64 + src/globals/defines.php | 38 +- src/globals/functions.php | 131 +- src/globals/internal-env.php | 66 +- src/globals/patch/php-src-patches/Readme.md | 95 + .../patch/php-src-patches/cli_checks_80.patch | 174 ++ .../patch/php-src-patches/cli_checks_81.patch | 183 ++ .../patch/php-src-patches/cli_checks_83.patch | 192 ++ .../patch/php-src-patches/cli_checks_84.patch | 191 ++ .../patch/php-src-patches/cli_checks_85.patch | 178 ++ .../patch/php-src-patches/cli_static_80.patch | 24 + .../patch/php-src-patches/cli_static_84.patch | 23 + .../patch/php-src-patches/cli_static_85.patch | 13 + .../patch/php-src-patches/comctl32.patch | 21 + .../disable_huge_page_80.patch | 11 + .../disable_huge_page_84.patch | 13 + .../php-src-patches/macos_iconv_80.patch | 23 + .../php-src-patches/macos_iconv_81.patch | 23 + .../php-src-patches/macos_iconv_82.patch | 23 + .../php-src-patches/macos_iconv_83.patch | 23 + .../php-src-patches/macos_iconv_84.patch | 21 + .../patch/php-src-patches/phar_80.patch | 22 + .../patch/php-src-patches/phar_81.patch | 22 + .../static_extensions_win32_80.patch | 31 + .../static_extensions_win32_83.patch | 31 + .../static_extensions_win32_84.patch | 34 + .../static_extensions_win32_85.patch | 34 + .../php-src-patches/static_opcache_80.patch | 129 ++ .../php-src-patches/static_opcache_81.patch | 129 ++ .../php-src-patches/static_opcache_82.patch | 129 ++ .../php-src-patches/static_opcache_83.patch | 130 ++ .../php-src-patches/static_opcache_84.patch | 146 ++ .../php-src-patches/static_opcache_85.patch | 0 .../php-src-patches/vcruntime140_74.patch | 12 + .../php-src-patches/vcruntime140_80.patch | 11 + .../patch/php-src-patches/win32_74.patch | 21 + .../patch/php-src-patches/win32_80.patch | 21 + .../patch/php-src-patches/win32_82.patch | 22 + .../patch/php-src-patches/win32_85.patch | 22 + .../patch/php-src-patches/win32_api_80.patch | 88 + .../patch/php-src-patches/win32_api_84.patch | 0 .../patch/php-src-patches/zend_stream.patch | 12 + 179 files changed, 19432 insertions(+), 585 deletions(-) create mode 100755 bin/spc-debug create mode 100644 config/artifact.json create mode 100644 config/pkg.ext.json delete mode 100644 config/pkg.json create mode 100644 config/pkg.lib.json create mode 100644 config/pkg.target.json create mode 100644 spc.registry.json create mode 100644 src/Package/Artifact/go_xcaddy.php create mode 100644 src/Package/Command/SwitchPhpVersionCommand.php create mode 100644 src/Package/Library/libiconv.php create mode 100644 src/Package/Library/libxml2.php create mode 100644 src/Package/Library/postgresql.php create mode 100644 src/Package/README.md create mode 100644 src/Package/Target/go_xcaddy.php create mode 100644 src/Package/Target/php.php create mode 100644 src/Package/Target/pkgconfig.php create mode 100644 src/StaticPHP/Artifact/Artifact.php create mode 100644 src/StaticPHP/Artifact/ArtifactCache.php create mode 100644 src/StaticPHP/Artifact/ArtifactDownloader.php create mode 100644 src/StaticPHP/Artifact/ArtifactExtractor.php create mode 100644 src/StaticPHP/Artifact/ArtifactLoader.php create mode 100644 src/StaticPHP/Artifact/Downloader/DownloadResult.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/FileList.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/Git.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/LocalDir.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/PIE.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/Url.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php create mode 100644 src/StaticPHP/Artifact/DownloaderOptions.php create mode 100644 src/StaticPHP/Attribute/Artifact/AfterBinaryExtract.php create mode 100644 src/StaticPHP/Attribute/Artifact/AfterSourceExtract.php create mode 100644 src/StaticPHP/Attribute/Artifact/BinaryExtract.php create mode 100644 src/StaticPHP/Attribute/Artifact/CustomBinary.php create mode 100644 src/StaticPHP/Attribute/Artifact/CustomSource.php create mode 100644 src/StaticPHP/Attribute/Artifact/SourceExtract.php create mode 100644 src/StaticPHP/Attribute/Doctor/CheckItem.php create mode 100644 src/StaticPHP/Attribute/Doctor/FixItem.php create mode 100644 src/StaticPHP/Attribute/Doctor/OptionalCheck.php create mode 100644 src/StaticPHP/Attribute/Package/AfterStage.php create mode 100644 src/StaticPHP/Attribute/Package/BeforeStage.php create mode 100644 src/StaticPHP/Attribute/Package/BuildFor.php create mode 100644 src/StaticPHP/Attribute/Package/CustomPhpConfigureArg.php create mode 100644 src/StaticPHP/Attribute/Package/Extension.php create mode 100644 src/StaticPHP/Attribute/Package/Info.php create mode 100644 src/StaticPHP/Attribute/Package/InitPackage.php create mode 100644 src/StaticPHP/Attribute/Package/Library.php create mode 100644 src/StaticPHP/Attribute/Package/PatchBeforeBuild.php create mode 100644 src/StaticPHP/Attribute/Package/ResolveBuild.php create mode 100644 src/StaticPHP/Attribute/Package/Stage.php create mode 100644 src/StaticPHP/Attribute/Package/Target.php create mode 100644 src/StaticPHP/Attribute/Package/Validate.php create mode 100644 src/StaticPHP/Attribute/PatchDescription.php create mode 100644 src/StaticPHP/Command/BaseCommand.php create mode 100644 src/StaticPHP/Command/BuildLibsCommand.php create mode 100644 src/StaticPHP/Command/BuildTargetCommand.php create mode 100644 src/StaticPHP/Command/DoctorCommand.php create mode 100644 src/StaticPHP/Command/DownloadCommand.php create mode 100644 src/StaticPHP/Command/ExtractCommand.php create mode 100644 src/StaticPHP/Command/InstallPackageCommand.php create mode 100644 src/StaticPHP/Command/SPCConfigCommand.php create mode 100644 src/StaticPHP/Config/ArtifactConfig.php create mode 100644 src/StaticPHP/Config/ConfigType.php create mode 100644 src/StaticPHP/Config/ConfigValidator.php create mode 100644 src/StaticPHP/Config/PackageConfig.php create mode 100644 src/StaticPHP/ConsoleApplication.php create mode 100644 src/StaticPHP/DI/ApplicationContext.php create mode 100644 src/StaticPHP/DI/CallbackInvoker.php create mode 100644 src/StaticPHP/Doctor/CheckResult.php create mode 100644 src/StaticPHP/Doctor/Doctor.php create mode 100644 src/StaticPHP/Doctor/DoctorLoader.php create mode 100644 src/StaticPHP/Doctor/Item/LinuxMuslCheck.php create mode 100644 src/StaticPHP/Doctor/Item/MacOSToolCheck.php create mode 100644 src/StaticPHP/Doctor/Item/OSCheck.php create mode 100644 src/StaticPHP/Doctor/Item/PkgConfigCheck.php create mode 100644 src/StaticPHP/Doctor/Item/Re2cVersionCheck.php create mode 100644 src/StaticPHP/Exception/BuildFailureException.php create mode 100644 src/StaticPHP/Exception/DownloaderException.php create mode 100644 src/StaticPHP/Exception/EnvironmentException.php create mode 100644 src/StaticPHP/Exception/ExceptionHandler.php create mode 100644 src/StaticPHP/Exception/ExecutionException.php create mode 100644 src/StaticPHP/Exception/FileSystemException.php create mode 100644 src/StaticPHP/Exception/InterruptException.php create mode 100644 src/StaticPHP/Exception/PatchException.php create mode 100644 src/StaticPHP/Exception/SPCException.php create mode 100644 src/StaticPHP/Exception/SPCInternalException.php create mode 100644 src/StaticPHP/Exception/ValidationException.php create mode 100644 src/StaticPHP/Exception/WrongUsageException.php create mode 100644 src/StaticPHP/Package/LibraryPackage.php create mode 100644 src/StaticPHP/Package/Package.php create mode 100644 src/StaticPHP/Package/PackageBuilder.php create mode 100644 src/StaticPHP/Package/PackageCallbacksTrait.php create mode 100644 src/StaticPHP/Package/PackageInstaller.php create mode 100644 src/StaticPHP/Package/PackageLoader.php create mode 100644 src/StaticPHP/Package/PhpExtensionPackage.php create mode 100644 src/StaticPHP/Package/TargetPackage.php create mode 100644 src/StaticPHP/Registry/Registry.php create mode 100644 src/StaticPHP/Runtime/Executor/Executor.php create mode 100644 src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php create mode 100644 src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php create mode 100644 src/StaticPHP/Runtime/Shell/DefaultShell.php create mode 100644 src/StaticPHP/Runtime/Shell/Shell.php create mode 100644 src/StaticPHP/Runtime/Shell/UnixShell.php create mode 100644 src/StaticPHP/Runtime/Shell/WindowsCmd.php create mode 100644 src/StaticPHP/Runtime/SystemTarget.php create mode 100644 src/StaticPHP/Toolchain/ClangNativeToolchain.php create mode 100644 src/StaticPHP/Toolchain/GccNativeToolchain.php create mode 100644 src/StaticPHP/Toolchain/Interface/ToolchainInterface.php create mode 100644 src/StaticPHP/Toolchain/Interface/UnixToolchainInterface.php create mode 100644 src/StaticPHP/Toolchain/MSVCToolchain.php create mode 100644 src/StaticPHP/Toolchain/MuslToolchain.php create mode 100644 src/StaticPHP/Toolchain/ToolchainManager.php create mode 100644 src/StaticPHP/Toolchain/ZigToolchain.php create mode 100644 src/StaticPHP/Util/DependencyResolver.php create mode 100644 src/StaticPHP/Util/FileSystem.php create mode 100644 src/StaticPHP/Util/GlobalEnvManager.php create mode 100644 src/StaticPHP/Util/InteractiveTerm.php create mode 100644 src/StaticPHP/Util/PkgConfigUtil.php create mode 100644 src/StaticPHP/Util/SPCConfigUtil.php create mode 100644 src/StaticPHP/Util/SourcePatcher.php create mode 100644 src/StaticPHP/Util/System/LinuxUtil.php create mode 100644 src/StaticPHP/Util/System/MacOSUtil.php create mode 100644 src/StaticPHP/Util/System/UnixUtil.php create mode 100644 src/StaticPHP/Util/System/WindowsUtil.php create mode 100644 src/StaticPHP/Util/V2CompatLayer.php create mode 100644 src/bootstrap.php create mode 100644 src/globals/patch/php-src-patches/Readme.md create mode 100644 src/globals/patch/php-src-patches/cli_checks_80.patch create mode 100644 src/globals/patch/php-src-patches/cli_checks_81.patch create mode 100644 src/globals/patch/php-src-patches/cli_checks_83.patch create mode 100644 src/globals/patch/php-src-patches/cli_checks_84.patch create mode 100644 src/globals/patch/php-src-patches/cli_checks_85.patch create mode 100644 src/globals/patch/php-src-patches/cli_static_80.patch create mode 100644 src/globals/patch/php-src-patches/cli_static_84.patch create mode 100644 src/globals/patch/php-src-patches/cli_static_85.patch create mode 100644 src/globals/patch/php-src-patches/comctl32.patch create mode 100644 src/globals/patch/php-src-patches/disable_huge_page_80.patch create mode 100644 src/globals/patch/php-src-patches/disable_huge_page_84.patch create mode 100644 src/globals/patch/php-src-patches/macos_iconv_80.patch create mode 100644 src/globals/patch/php-src-patches/macos_iconv_81.patch create mode 100644 src/globals/patch/php-src-patches/macos_iconv_82.patch create mode 100644 src/globals/patch/php-src-patches/macos_iconv_83.patch create mode 100644 src/globals/patch/php-src-patches/macos_iconv_84.patch create mode 100644 src/globals/patch/php-src-patches/phar_80.patch create mode 100644 src/globals/patch/php-src-patches/phar_81.patch create mode 100644 src/globals/patch/php-src-patches/static_extensions_win32_80.patch create mode 100644 src/globals/patch/php-src-patches/static_extensions_win32_83.patch create mode 100644 src/globals/patch/php-src-patches/static_extensions_win32_84.patch create mode 100644 src/globals/patch/php-src-patches/static_extensions_win32_85.patch create mode 100644 src/globals/patch/php-src-patches/static_opcache_80.patch create mode 100644 src/globals/patch/php-src-patches/static_opcache_81.patch create mode 100644 src/globals/patch/php-src-patches/static_opcache_82.patch create mode 100644 src/globals/patch/php-src-patches/static_opcache_83.patch create mode 100644 src/globals/patch/php-src-patches/static_opcache_84.patch create mode 100644 src/globals/patch/php-src-patches/static_opcache_85.patch create mode 100644 src/globals/patch/php-src-patches/vcruntime140_74.patch create mode 100644 src/globals/patch/php-src-patches/vcruntime140_80.patch create mode 100644 src/globals/patch/php-src-patches/win32_74.patch create mode 100644 src/globals/patch/php-src-patches/win32_80.patch create mode 100644 src/globals/patch/php-src-patches/win32_82.patch create mode 100644 src/globals/patch/php-src-patches/win32_85.patch create mode 100644 src/globals/patch/php-src-patches/win32_api_80.patch create mode 100644 src/globals/patch/php-src-patches/win32_api_84.patch create mode 100644 src/globals/patch/php-src-patches/zend_stream.patch diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 132b33f00..6c2730fdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Tests on: pull_request: - branches: [ "main" ] + branches: [ "main", "v3" ] types: [ opened, synchronize, reopened ] paths: - 'src/**' diff --git a/.gitignore b/.gitignore index 0d2bd5540..d33eae535 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .idea -runtime/ -docker/libraries/ -docker/extensions/ -docker/source/ +/runtime/ +/docker/libraries/ +/docker/extensions/ +/docker/source/ # Vendor files /vendor/** diff --git a/bin/spc b/bin/spc index 18dd3a383..9bac188ae 100755 --- a/bin/spc +++ b/bin/spc @@ -1,13 +1,9 @@ #!/usr/bin/env php run(); -} catch (Exception $e) { - ExceptionHandler::getInstance()->handle($e); +} catch (SPCException $e) { + ExceptionHandler::handleSPCException($e); + exit(1); +} catch (\Throwable $e) { + ExceptionHandler::handleDefaultException($e); exit(1); } + diff --git a/bin/spc-debug b/bin/spc-debug new file mode 100755 index 000000000..d5a18c837 --- /dev/null +++ b/bin/spc-debug @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# This script runs the 'spc' command with Xdebug enabled for debugging purposes. +php -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.client_port=9003 -d xdebug.start_with_request=yes "$(dirname "$0")/../bin/spc" "$@" diff --git a/composer.json b/composer.json index 1a1005c73..360cdbdbe 100644 --- a/composer.json +++ b/composer.json @@ -9,14 +9,15 @@ } ], "require": { - "php": ">= 8.3", + "php": ">=8.4", "ext-mbstring": "*", "ext-zlib": "*", - "laravel/prompts": "^0.1.12", + "laravel/prompts": "~0.1", + "php-di/php-di": "^7.1", "symfony/console": "^5.4 || ^6 || ^7", "symfony/process": "^7.2", "symfony/yaml": "^7.2", - "zhamao/logger": "^1.1.3" + "zhamao/logger": "^1.1.4" }, "require-dev": { "captainhook/captainhook-phar": "^5.23", @@ -28,7 +29,9 @@ }, "autoload": { "psr-4": { - "SPC\\": "src/SPC" + "SPC\\": "src/SPC", + "StaticPHP\\": "src/StaticPHP", + "Package\\": "src/Package" }, "files": [ "src/globals/defines.php", diff --git a/composer.lock b/composer.lock index e8f320c7c..6afc39dfa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,90 +4,100 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f81132977eb1310f5ccb27c8de76c8d2", + "content-hash": "14b3ad42c138807fa9288e6b510ac69f", "packages": [ { - "name": "illuminate/collections", - "version": "v11.46.1", + "name": "laravel/prompts", + "version": "v0.3.8", "source": { "type": "git", - "url": "https://github.com/illuminate/collections.git", - "reference": "856b1da953e46281ba61d7c82d337072d3ee1825" + "url": "https://github.com/laravel/prompts.git", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/856b1da953e46281ba61d7c82d337072d3ee1825", - "reference": "856b1da953e46281ba61d7c82d337072d3ee1825", + "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", "shasum": "" }, "require": { - "illuminate/conditionable": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "php": "^8.2" + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^7.0)." + "ext-pcntl": "Required for the spinner to be animated." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-main": "0.3.x-dev" } }, "autoload": { "files": [ - "functions.php", - "helpers.php" + "src/helpers.php" ], "psr-4": { - "Illuminate\\Support\\": "" + "Laravel\\Prompts\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Collections package.", - "homepage": "https://laravel.com", + "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.8" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-11-21T20:52:52+00:00" }, { - "name": "illuminate/conditionable", - "version": "v11.46.1", + "name": "laravel/serializable-closure", + "version": "v2.0.7", "source": { "type": "git", - "url": "https://github.com/illuminate/conditionable.git", - "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9" + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/319b717e0587bd7c8a3b44464f0e27867b4bcda9", - "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { - "php": "^8.0.2" + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "Illuminate\\Support\\": "" + "Laravel\\SerializableClosure\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -98,167 +108,151 @@ { "name": "Taylor Otwell", "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" } ], - "description": "The Illuminate Conditionable package.", - "homepage": "https://laravel.com", + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { - "name": "illuminate/contracts", - "version": "v11.46.1", + "name": "php-di/invoker", + "version": "2.3.7", "source": { "type": "git", - "url": "https://github.com/illuminate/contracts.git", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8" + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/4b2a67d1663f50085bc91e6371492697a5d2d4e8", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", "shasum": "" }, "require": { - "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1", - "psr/simple-cache": "^1.0|^2.0|^3.0" + "php": ">=7.3", + "psr/container": "^1.0|^2.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - } + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" }, + "type": "library", "autoload": { "psr-4": { - "Illuminate\\Contracts\\": "" + "Invoker\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" ], - "description": "The Illuminate Contracts package.", - "homepage": "https://laravel.com", "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" }, - "time": "2025-03-24T11:54:20+00:00" - }, - { - "name": "illuminate/macroable", - "version": "v11.46.1", - "source": { - "type": "git", - "url": "https://github.com/illuminate/macroable.git", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", - "shasum": "" - }, - "require": { - "php": "^8.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Support\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "description": "The Illuminate Macroable package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2024-06-28T20:10:30+00:00" + "time": "2025-08-30T10:22:22+00:00" }, { - "name": "laravel/prompts", - "version": "v0.1.25", + "name": "php-di/php-di", + "version": "7.1.1", "source": { "type": "git", - "url": "https://github.com/laravel/prompts.git", - "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", - "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", "shasum": "" }, "require": { - "ext-mbstring": "*", - "illuminate/collections": "^10.0|^11.0", - "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" }, - "conflict": { - "illuminate/console": ">=10.17.0 <10.25.0", - "laravel/framework": ">=10.17.0 <10.25.0" + "provide": { + "psr/container-implementation": "^1.0" }, "require-dev": { - "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1" + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" }, "suggest": { - "ext-pcntl": "Required for the spinner to be animated." + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "0.1.x-dev" - } - }, "autoload": { "files": [ - "src/helpers.php" + "src/functions.php" ], "psr-4": { - "Laravel\\Prompts\\": "src/" + "DI\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Add beautiful and user-friendly forms to your command-line applications.", + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], "support": { - "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.25" + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" }, - "time": "2024-08-12T22:06:33+00:00" + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-08-16T11:10:48+00:00" }, { "name": "psr/container", @@ -363,69 +357,18 @@ }, "time": "2024-09-11T13:17:53+00:00" }, - { - "name": "psr/simple-cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ], - "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" - }, - "time": "2021-10-29T13:26:27+00:00" - }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", "shasum": "" }, "require": { @@ -433,7 +376,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -447,16 +390,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -490,7 +433,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.0" }, "funding": [ { @@ -510,7 +453,7 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/deprecation-contracts", @@ -916,16 +859,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -957,7 +900,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.0" }, "funding": [ { @@ -977,7 +920,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/service-contracts", @@ -1068,34 +1011,34 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "f929eccf09531078c243df72398560e32fa4cf4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", + "reference": "f929eccf09531078c243df72398560e32fa4cf4f", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -1134,7 +1077,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.0" }, "funding": [ { @@ -1154,32 +1097,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-09-11T14:37:55+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -1210,7 +1153,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.0" }, "funding": [ { @@ -1230,7 +1173,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "zhamao/logger", @@ -2113,7 +2056,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.25.11", + "version": "5.27.3", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2167,7 +2110,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.25.11" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.27.3" }, "funding": [ { @@ -2864,16 +2807,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.89.2", + "version": "v3.91.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf" + "reference": "c4a25f20390337789c26b693ae46faa125040352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7569658f91e475ec93b99bd5964b059ad1336dcf", - "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c4a25f20390337789c26b693ae46faa125040352", + "reference": "c4a25f20390337789c26b693ae46faa125040352", "shasum": "" }, "require": { @@ -2891,17 +2834,17 @@ "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", - "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0", - "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", "symfony/polyfill-php84": "^1.33", - "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", - "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", @@ -2913,8 +2856,8 @@ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", - "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" + "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2955,7 +2898,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.91.0" }, "funding": [ { @@ -2963,7 +2906,7 @@ "type": "github" } ], - "time": "2025-11-06T21:12:50+00:00" + "time": "2025-11-28T22:07:42+00:00" }, { "name": "humbug/box", @@ -3212,16 +3155,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.1", + "version": "6.6.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/3c25fe750c1599716ef26aa997f7c026cee8c4b7", + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7", "shasum": "" }, "require": { @@ -3281,9 +3224,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.2" }, - "time": "2025-11-07T18:30:29+00:00" + "time": "2025-11-28T15:24:03+00:00" }, { "name": "kelunik/certificate", @@ -3345,33 +3288,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3399,6 +3347,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -3411,9 +3360,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -3423,7 +3374,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -3431,26 +3382,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -3458,6 +3408,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3482,7 +3433,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -3507,7 +3458,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -3515,7 +3466,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "marc-mabe/php-enum", @@ -4147,16 +4098,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.5", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", "shasum": "" }, "require": { @@ -4205,22 +4156,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-11-27T19:50:05+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { @@ -4263,9 +4214,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-21T15:09:14+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -5104,16 +5055,16 @@ }, { "name": "react/dns", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { @@ -5168,7 +5119,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { @@ -5176,20 +5127,20 @@ "type": "open_collective" } ], - "time": "2024-06-13T14:18:03+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { "name": "react/event-loop", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { @@ -5240,7 +5191,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { @@ -5248,7 +5199,7 @@ "type": "open_collective" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { "name": "react/promise", @@ -5325,16 +5276,16 @@ }, { "name": "react/socket", - "version": "v1.16.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { @@ -5393,7 +5344,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { @@ -5401,7 +5352,7 @@ "type": "open_collective" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { "name": "react/stream", @@ -5483,16 +5434,16 @@ }, { "name": "revolt/event-loop", - "version": "v1.0.7", + "version": "v1.0.8", "source": { "type": "git", "url": "https://github.com/revoltphp/event-loop.git", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", "shasum": "" }, "require": { @@ -5549,9 +5500,9 @@ ], "support": { "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" }, - "time": "2025-01-25T19:27:39+00:00" + "time": "2025-08-27T21:33:23+00:00" }, { "name": "sebastian/cli-parser", @@ -6620,24 +6571,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -6646,13 +6597,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -6680,7 +6632,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -6700,7 +6652,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6780,16 +6732,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -6798,7 +6750,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6826,7 +6778,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" }, "funding": [ { @@ -6846,27 +6798,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6894,7 +6846,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -6914,24 +6866,24 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -6965,7 +6917,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -6985,7 +6937,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-iconv", @@ -7153,20 +7105,20 @@ }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -7195,7 +7147,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" }, "funding": [ { @@ -7206,25 +7158,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-24T10:49:57+00:00" + "time": "2025-08-04T07:36:47+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -7236,10 +7192,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -7278,7 +7234,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -7298,7 +7254,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "thecodingmachine/safe", @@ -7441,16 +7397,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -7479,7 +7435,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.0" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -7487,7 +7443,7 @@ "type": "github" } ], - "time": "2025-11-13T13:44:09+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/assert", @@ -7554,10 +7510,10 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">= 8.3", + "php": ">=8.4", "ext-mbstring": "*", "ext-zlib": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/artifact.json b/config/artifact.json new file mode 100644 index 000000000..a2c6ba4fb --- /dev/null +++ b/config/artifact.json @@ -0,0 +1,1046 @@ +{ + "vswhere": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe", + "extract": "{pkg_root_path}/bin/vswhere.exe" + } + } + }, + "php-src": { + "source": { + "type": "php-release" + } + }, + "php-sdk-binary-tools": { + "binary": { + "windows-x86_64": { + "type": "git", + "rev": "master", + "url": "https://github.com/php/php-sdk-binary-tools.git", + "extract": "{php_sdk_path}" + } + } + }, + "go-xcaddy": { + "binary": "custom" + }, + "musl-toolchain": { + "binary": { + "linux-x86_64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", + "linux-aarch64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz" + } + }, + "pkg-config": { + "source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz", + "binary": { + "linux-x86_64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "linux-aarch64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "macos-x86_64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-x86_64-darwin.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "macos-aarch64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-aarch64-darwin.txz", + "extract": "{pkg_root_path}" + } + } + }, + "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" + } + }, + "upx": { + "binary": { + "linux-x86_64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-amd64_linux\\.tar\\.xz", + "extract": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "linux-aarch64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-arm64_linux\\.tar\\.xz", + "extract": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "windows-x86_64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-win64\\.zip", + "extract": { + "upx.exe": "{pkg_root_path}/bin/upx.exe" + } + } + } + }, + "zig": { + "binary": "custom" + }, + "nasm": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", + "extract": { + "nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + } + } + } + }, + "amqp": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/amqp", + "filename": "amqp.tgz", + "extract": "php-src/ext/amqp" + } + }, + "apcu": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/APCu", + "filename": "apcu.tgz", + "extract": "php-src/ext/apcu" + } + }, + "ast": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ast", + "filename": "ast.tgz", + "extract": "php-src/ext/ast" + } + }, + "attr": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz" + }, + "source-mirror": { + "type": "url", + "url": "https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz" + } + }, + "brotli": { + "binary": "hosted", + "source": { + "type": "ghtagtar", + "repo": "google/brotli", + "match": "v1\\.\\d.*" + } + }, + "bzip2": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz" + }, + "source-mirror": { + "type": "filelist", + "url": "https://sourceware.org/pub/bzip2/", + "regex": "/href=\"(?bzip2-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "curl": { + "source": { + "type": "ghrel", + "repo": "curl/curl", + "match": "curl.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "dio": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/dio", + "filename": "dio.tgz", + "extract": "php-src/ext/dio" + } + }, + "ev": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ev", + "filename": "ev.tgz", + "extract": "php-src/ext/ev" + } + }, + "ext-brotli": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/kjdev/php-ext-brotli", + "extract": "php-src/ext/brotli" + } + }, + "ext-ds": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ds", + "filename": "ds.tgz", + "extract": "php-src/ext/ds" + } + }, + "ext-event": { + "source": { + "type": "url", + "url": "https://bitbucket.org/osmanov/pecl-event/get/3.0.8.tar.gz", + "extract": "php-src/ext/event" + } + }, + "ext-glfw": { + "source": { + "type": "git", + "url": "https://github.com/mario-deluna/php-glfw", + "rev": "master" + } + }, + "ext-gmssl": { + "source": { + "type": "ghtar", + "repo": "gmssl/GmSSL-PHP", + "extract": "php-src/ext/gmssl" + } + }, + "ext-imagick": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/imagick", + "filename": "imagick.tgz", + "extract": "php-src/ext/imagick" + } + }, + "ext-imap": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/imap", + "filename": "imap.tgz", + "extract": "php-src/ext/imap" + } + }, + "ext-lz4": { + "source": { + "type": "ghtagtar", + "repo": "kjdev/php-ext-lz4", + "extract": "php-src/ext/lz4" + } + }, + "ext-memcache": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/memcache", + "filename": "memcache.tgz", + "extract": "php-src/ext/memcache" + } + }, + "ext-rdkafka": { + "source": { + "type": "ghtar", + "repo": "arnaud-lb/php-rdkafka", + "extract": "php-src/ext/rdkafka" + } + }, + "ext-simdjson": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/simdjson", + "filename": "simdjson.tgz", + "extract": "php-src/ext/simdjson" + } + }, + "ext-snappy": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/kjdev/php-ext-snappy", + "extract": "php-src/ext/snappy" + } + }, + "ext-ssh2": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ssh2", + "filename": "ssh2.tgz", + "extract": "php-src/ext/ssh2" + } + }, + "ext-trader": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/trader", + "filename": "trader.tgz", + "extract": "php-src/ext/trader" + } + }, + "ext-uuid": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/uuid", + "filename": "uuid.tgz", + "extract": "php-src/ext/uuid" + } + }, + "ext-uv": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/uv", + "filename": "uv.tgz", + "extract": "php-src/ext/uv" + } + }, + "ext-xz": { + "source": { + "type": "git", + "rev": "main", + "url": "https://github.com/codemasher/php-ext-xz", + "extract": "php-src/ext/xz" + } + }, + "ext-zip": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/zip", + "filename": "ext-zip.tgz" + } + }, + "ext-zstd": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/kjdev/php-ext-zstd", + "extract": "php-src/ext/zstd" + } + }, + "fastlz": { + "source": { + "type": "git", + "url": "https://github.com/ariya/FastLZ.git", + "rev": "master" + } + }, + "freetype": { + "source": { + "type": "git", + "rev": "VER-2-13-2", + "url": "https://github.com/freetype/freetype" + } + }, + "gettext": { + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/pub/gnu/gettext/", + "regex": "/href=\"(?gettext-(?[^\"]+)\\.tar\\.xz)\"/" + } + }, + "gmp": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://gmplib.org/download/gmp/", + "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/" + }, + "source-mirror": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz" + } + }, + "gmssl": { + "binary": "hosted", + "source": { + "type": "ghtar", + "repo": "guanzhi/GmSSL" + } + }, + "grpc": { + "binary": "hosted", + "source": { + "type": "git", + "rev": "v1.75.x", + "url": "https://github.com/grpc/grpc.git" + } + }, + "icu": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "unicode-org/icu", + "match": "icu4c.+-src\\.tgz", + "prefer-stable": true + } + }, + "icu-static-win": { + "source": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/icu-static-windows-x64/icu-static-windows-x64.zip" + } + }, + "igbinary": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/igbinary", + "filename": "igbinary.tgz", + "extract": "php-src/ext/igbinary" + } + }, + "imagemagick": { + "source": { + "type": "ghtar", + "repo": "ImageMagick/ImageMagick" + } + }, + "imap": { + "source": { + "type": "git", + "url": "https://github.com/static-php/imap.git", + "rev": "master" + } + }, + "inotify": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/inotify", + "filename": "inotify.tgz", + "extract": "php-src/ext/inotify" + } + }, + "jbig": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/jbig/jbigkit-2.1.tar.gz" + }, + "source-mirror": { + "type": "url", + "url": "https://www.cl.cam.ac.uk/~mgk25/jbigkit/download/jbigkit-2.1.tar.gz" + } + }, + "ldap": { + "source": { + "type": "filelist", + "url": "https://www.openldap.org/software/download/OpenLDAP/openldap-release/", + "regex": "/href=\"(?openldap-(?[^\"]+)\\.tgz)\"/" + } + }, + "lerc": { + "binary": "hosted", + "source": { + "type": "ghtar", + "repo": "Esri/lerc", + "prefer-stable": true + } + }, + "libacl": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://download.savannah.nongnu.org/releases/acl/acl-2.3.2.tar.gz" + }, + "source-mirror": { + "type": "url", + "url": "https://mirror.souseiseki.middlendian.com/nongnu/acl/acl-2.3.2.tar.gz" + } + }, + "libaom": { + "binary": "hosted", + "source": { + "type": "git", + "rev": "main", + "url": "https://aomedia.googlesource.com/aom" + } + }, + "libargon2": { + "binary": "hosted", + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/static-php/phc-winner-argon2" + } + }, + "libavif": { + "binary": "hosted", + "source": { + "type": "ghtar", + "repo": "AOMediaCodec/libavif" + } + }, + "libcares": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "c-ares/c-ares", + "match": "c-ares-.+\\.tar\\.gz", + "prefer-stable": true + }, + "source-mirror": { + "type": "filelist", + "url": "https://c-ares.org/download/", + "regex": "/href=\"\\/download\\/(?c-ares-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "libde265": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "strukturag/libde265", + "match": "libde265-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libedit": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://thrysoee.dk/editline/", + "regex": "/href=\"(?libedit-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "libevent": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "libevent/libevent", + "match": "libevent.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libffi": { + "source": { + "type": "ghrel", + "repo": "libffi/libffi", + "match": "libffi.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libffi-win": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/static-php/libffi-win.git" + } + }, + "libheif": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "strukturag/libheif", + "match": "libheif-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libiconv": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/gnu/libiconv/", + "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/" + }, + "source-mirror": "https://dl.static-php.dev/static-php-cli/deps/spc-download-mirror/libiconv/libiconv-spc-mirror.tar.gz" + }, + "libiconv-win": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/static-php/libiconv-win.git" + } + }, + "libjpeg": { + "source": { + "type": "ghtar", + "repo": "libjpeg-turbo/libjpeg-turbo" + } + }, + "libjxl": { + "source": { + "type": "git", + "url": "https://github.com/libjxl/libjxl", + "rev": "main", + "submodules": [ + "third_party/highway", + "third_party/libjpeg-turbo", + "third_party/sjpeg", + "third_party/skcms" + ] + } + }, + "liblz4": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "lz4/lz4", + "match": "lz4-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libmemcached": { + "source": { + "type": "ghtagtar", + "repo": "awesomized/libmemcached", + "match": "1.\\d.\\d" + } + }, + "libpng": { + "binary": "hosted", + "source": { + "type": "git", + "url": "https://github.com/glennrp/libpng.git", + "rev": "libpng16" + } + }, + "librabbitmq": { + "source": { + "type": "git", + "url": "https://github.com/alanxz/rabbitmq-c.git", + "rev": "master" + } + }, + "librdkafka": { + "source": { + "type": "ghtar", + "repo": "confluentinc/librdkafka" + } + }, + "libsodium": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "jedisct1/libsodium", + "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", + "prefer-stable": true + } + }, + "libssh2": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "libssh2/libssh2", + "match": "libssh2.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libtiff": { + "source": { + "type": "filelist", + "url": "https://download.osgeo.org/libtiff/", + "regex": "/href=\"(?tiff-(?[^\"]+)\\.tar\\.xz)\"/" + } + }, + "liburing": { + "source": { + "type": "ghtar", + "repo": "axboe/liburing", + "prefer-stable": true + } + }, + "libuuid": { + "source": { + "type": "git", + "url": "https://github.com/static-php/libuuid.git", + "rev": "master" + } + }, + "libuv": { + "source": { + "type": "ghtar", + "repo": "libuv/libuv" + } + }, + "libwebp": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.3.2.tar.gz" + } + }, + "libxml2": { + "source": { + "type": "url", + "url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.12.5.tar.gz" + } + }, + "libxslt": { + "source": { + "type": "filelist", + "url": "https://download.gnome.org/sources/libxslt/1.1/", + "regex": "/href=\"(?libxslt-(?[^\"]+)\\.tar\\.xz)\"/" + } + }, + "libyaml": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "yaml/libyaml", + "match": "yaml-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libzip": { + "source": { + "type": "ghrel", + "repo": "nih-at/libzip", + "match": "libzip.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "memcached": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/memcached", + "filename": "memcached.tgz", + "extract": "php-src/ext/memcached" + } + }, + "mimalloc": { + "source": { + "type": "ghtagtar", + "repo": "microsoft/mimalloc", + "match": "v2\\.\\d\\.[^3].*" + } + }, + "micro": { + "source": { + "type": "git", + "extract": "php-src/sapi/micro", + "rev": "master", + "url": "https://github.com/static-php/phpmicro" + } + }, + "mongodb": { + "source": { + "type": "ghrel", + "repo": "mongodb/mongo-php-driver", + "match": "mongodb.+\\.tgz", + "prefer-stable": true, + "extract": "php-src/ext/mongodb" + } + }, + "msgpack": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/msgpack", + "filename": "msgpack.tgz", + "extract": "php-src/ext/msgpack" + } + }, + "ncurses": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/pub/gnu/ncurses/", + "regex": "/href=\"(?ncurses-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "net-snmp": { + "source": { + "type": "ghtagtar", + "repo": "net-snmp/net-snmp" + } + }, + "nghttp2": { + "source": { + "type": "ghrel", + "repo": "nghttp2/nghttp2", + "match": "nghttp2.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "nghttp3": { + "source": { + "type": "ghrel", + "repo": "ngtcp2/nghttp3", + "match": "nghttp3.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "ngtcp2": { + "source": { + "type": "ghrel", + "repo": "ngtcp2/ngtcp2", + "match": "ngtcp2.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "onig": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "kkos/oniguruma", + "match": "onig-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "openssl": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "openssl/openssl", + "match": "openssl.+\\.tar\\.gz", + "prefer-stable": true + }, + "source-mirror": { + "type": "filelist", + "url": "https://www.openssl.org/source/", + "regex": "/href=\"(?openssl-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "opentelemetry": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/opentelemetry", + "filename": "opentelemetry.tgz", + "extract": "php-src/ext/opentelemetry" + } + }, + "parallel": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/parallel", + "filename": "parallel.tgz", + "extract": "php-src/ext/parallel" + } + }, + "pdo_sqlsrv": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/pdo_sqlsrv", + "filename": "pdo_sqlsrv.tgz", + "extract": "php-src/ext/pdo_sqlsrv" + } + }, + "postgresql": { + "source": { + "type": "ghtagtar", + "repo": "postgres/postgres", + "match": "REL_18_\\d+" + } + }, + "postgresql-win": { + "source": { + "type": "url", + "url": "https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip" + } + }, + "protobuf": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/protobuf", + "filename": "protobuf.tgz", + "extract": "php-src/ext/protobuf" + } + }, + "pthreads4w": { + "source": { + "type": "git", + "rev": "master", + "url": "https://git.code.sf.net/p/pthreads4w/code" + } + }, + "qdbm": { + "source": { + "type": "git", + "url": "https://github.com/static-php/qdbm.git", + "rev": "main" + } + }, + "rar": { + "source": { + "type": "git", + "url": "https://github.com/static-php/php-rar.git", + "rev": "issue-php82", + "extract": "php-src/ext/rar" + } + }, + "re2c": { + "source": { + "type": "ghrel", + "repo": "skvadrik/re2c", + "match": "re2c.+\\.tar\\.xz", + "prefer-stable": true + }, + "source-mirror": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/re2c/re2c-4.3.tar.xz" + } + }, + "readline": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/pub/gnu/readline/", + "regex": "/href=\"(?readline-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "redis": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/redis", + "filename": "redis.tgz", + "extract": "php-src/ext/redis" + } + }, + "snappy": { + "source": { + "type": "git", + "rev": "main", + "url": "https://github.com/google/snappy" + } + }, + "spx": { + "source": { + "type": "pie", + "repo": "noisebynorthwest/php-spx", + "extract": "php-src/ext/spx" + } + }, + "sqlite": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://www.sqlite.org/2024/sqlite-autoconf-3450200.tar.gz" + } + }, + "sqlsrv": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/sqlsrv", + "filename": "sqlsrv.tgz", + "extract": "php-src/ext/sqlsrv" + } + }, + "swoole": { + "source": { + "type": "ghtar", + "repo": "swoole/swoole-src", + "match": "v6\\.+", + "prefer-stable": true, + "extract": "php-src/ext/swoole" + } + }, + "swow": { + "source": { + "type": "ghtar", + "repo": "swow/swow", + "prefer-stable": true, + "extract": "php-src/ext/swow-src" + } + }, + "tidy": { + "source": { + "type": "ghtar", + "repo": "htacg/tidy-html5", + "prefer-stable": true + } + }, + "unixodbc": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://www.unixodbc.org/unixODBC-2.3.12.tar.gz", + "version": "2.3.12" + } + }, + "watcher": { + "source": { + "type": "ghtar", + "repo": "e-dant/watcher", + "prefer-stable": true + } + }, + "xdebug": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/xdebug", + "filename": "xdebug.tgz" + } + }, + "xhprof": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/xhprof", + "filename": "xhprof.tgz", + "extract": "php-src/ext/xhprof-src" + } + }, + "xlswriter": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/xlswriter", + "filename": "xlswriter.tgz", + "extract": "php-src/ext/xlswriter" + } + }, + "xz": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "tukaani-project/xz", + "match": "xz.+\\.tar\\.xz", + "prefer-stable": true + }, + "source-mirror": { + "type": "url", + "url": "https://github.com/tukaani-project/xz/releases/download/v5.8.1/xz-5.8.1.tar.gz" + } + }, + "yac": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/yac", + "filename": "yac.tgz", + "extract": "php-src/ext/yac" + } + }, + "yaml": { + "source": { + "type": "git", + "rev": "php7", + "url": "https://github.com/php/pecl-file_formats-yaml", + "extract": "php-src/ext/yaml" + } + }, + "zlib": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "madler/zlib", + "match": "zlib.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "zstd": { + "source": { + "type": "ghrel", + "repo": "facebook/zstd", + "match": "zstd.+\\.tar\\.gz", + "prefer-stable": true + } + } +} diff --git a/config/env.ini b/config/env.ini index 7448cc373..8e25aa6e7 100644 --- a/config/env.ini +++ b/config/env.ini @@ -32,9 +32,10 @@ ; GNU_ARCH: the GNU arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`) ; MAC_ARCH: the MAC arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `arm64`) ; PKG_CONFIG: (*nix only) static-php-cli will set `$BUILD_BIN_PATH/pkg-config` to PKG_CONFIG. -; SPC_LINUX_DEFAULT_CC: (linux only) the default compiler for linux. (For alpine linux: `gcc`, default: `$GNU_ARCH-linux-musl-gcc`) -; SPC_LINUX_DEFAULT_CXX: (linux only) the default c++ compiler for linux. (For alpine linux: `g++`, default: `$GNU_ARCH-linux-musl-g++`) -; SPC_LINUX_DEFAULT_AR: (linux only) the default archiver for linux. (For alpine linux: `ar`, default: `$GNU_ARCH-linux-musl-ar`) +; SPC_DEFAULT_CC: (*nix only) the default compiler for selected toolchain. +; SPC_DEFAULT_CXX: (*nix only) the default c++ compiler selected toolchain. +; SPC_DEFAULT_AR: (*nix only) the default archiver for selected toolchain. +; SPC_DEFAULT_LD: (*nix only) the default linker for selected toolchain. ; SPC_EXTRA_PHP_VARS: (linux only) the extra vars for building php, used in `configure` and `make` command. [global] @@ -48,6 +49,12 @@ SPC_SKIP_DOCTOR_CHECK_ITEMS="" SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" ; The display message for php version output (PHP >= 8.4 available) PHP_BUILD_PROVIDER="static-php-cli ${SPC_VERSION}" +; Whether to enable log file (if you are using vendor mode) +SPC_ENABLE_LOG_FILE="yes" +; The LOG DIR for spc logs +SPC_LOGS_DIR="${WORKING_DIR}/log" +; Preserve old logs when running new builds +SPC_PRESERVE_LOGS="no" ; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them ; only useful for builds targeting not pure-static linking @@ -120,11 +127,12 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" ; Currently we do not support universal and cross-compilation for macOS. SPC_TARGET=native-macos ; compiler environments -CC=clang -CXX=clang++ -AR=ar -LD=ld +CC=${SPC_LINUX_DEFAULT_CC} +CXX=${SPC_LINUX_DEFAULT_CXX} +AR=${SPC_LINUX_DEFAULT_AR} +LD=${SPC_LINUX_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build +; this will be added to all CFLAGS and CXXFLAGS for the library builds SPC_DEFAULT_C_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os" SPC_DEFAULT_CXX_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os" SPC_DEFAULT_LD_FLAGS="" @@ -142,8 +150,3 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable- SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" - -[freebsd] -; compiler environments -CC=clang -CXX=clang++ diff --git a/config/pkg.ext.json b/config/pkg.ext.json new file mode 100644 index 000000000..70fe34e63 --- /dev/null +++ b/config/pkg.ext.json @@ -0,0 +1,1541 @@ +{ + "ext-amqp": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "librabbitmq" + ], + "depends@windows": [ + "ext-openssl" + ], + "artifact": "amqp", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-apcu": { + "type": "php-extension", + "artifact": "apcu", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-ast": { + "type": "php-extension", + "artifact": "ast", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-bcmath": { + "type": "php-extension" + }, + "ext-brotli": { + "type": "php-extension", + "php-extension": { + "arg-type": "enable" + }, + "depends": [ + "brotli" + ], + "artifact": "ext-brotli", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-bz2": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "with", + "arg-type": "with-path" + }, + "depends": [ + "bzip2" + ] + }, + "ext-calendar": { + "type": "php-extension" + }, + "ext-ctype": { + "type": "php-extension" + }, + "ext-curl": { + "type": "php-extension", + "php-extension": { + "arg-type": "with", + "notes": true + }, + "depends": [ + "curl" + ], + "depends@windows": [ + "ext-zlib", + "ext-openssl" + ] + }, + "ext-dba": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom" + }, + "suggests": [ + "qdbm" + ] + }, + "ext-dio": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "artifact": "dio", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-dom": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with" + }, + "depends": [ + "libxml2", + "zlib" + ], + "depends@windows": [ + "ext-xml" + ] + }, + "ext-ds": { + "type": "php-extension", + "artifact": "ext-ds", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-enchant": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip", + "Darwin": "wip", + "Linux": "wip" + } + } + }, + "ext-ev": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "with" + }, + "depends": [ + "ext-sockets" + ], + "artifact": "ev", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-event": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "libevent", + "ext-openssl" + ], + "suggests": [ + "ext-sockets" + ], + "artifact": "ext-event", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-exif": { + "type": "php-extension" + }, + "ext-ffi": { + "type": "php-extension", + "php-extension": { + "support": { + "Linux": "partial", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends@windows": [ + "libffi-win" + ], + "depends": [ + "libffi" + ] + }, + "ext-fileinfo": { + "type": "php-extension" + }, + "ext-filter": { + "type": "php-extension" + }, + "ext-ftp": { + "type": "php-extension", + "suggests": [ + "openssl" + ] + }, + "ext-gd": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with", + "notes": true + }, + "depends": [ + "zlib", + "libpng", + "ext-zlib" + ], + "suggests": [ + "libavif", + "libwebp", + "libjpeg", + "freetype" + ] + }, + "ext-gettext": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "gettext" + ] + }, + "ext-glfw": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "no", + "Linux": "no" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "glfw" + ], + "depends@windows": [], + "artifact": "ext-glfw", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-gmp": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "gmp" + ] + }, + "ext-gmssl": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "depends": [ + "gmssl" + ], + "artifact": "ext-gmssl", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-grpc": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "enable-path" + }, + "depends": [ + "grpc" + ], + "lang": "cpp", + "artifact": "grpc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-iconv": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "with" + }, + "depends@windows": [ + "libiconv-win" + ], + "depends": [ + "libiconv" + ] + }, + "ext-igbinary": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "suggests": [ + "ext-session", + "ext-apcu" + ], + "artifact": "igbinary", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "ext-imagick": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "imagemagick" + ], + "artifact": "ext-imagick", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-imap": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "imap" + ], + "suggests": [ + "ext-openssl" + ], + "artifact": "ext-imap", + "license": { + "type": "file", + "path": [ + "LICENSE" + ] + } + }, + "ext-inotify": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip", + "Darwin": "no" + } + }, + "artifact": "inotify", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-intl": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "depends@windows": [ + "icu-static-win" + ], + "depends": [ + "icu" + ] + }, + "ext-ldap": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "ldap" + ], + "suggests": [ + "gmp", + "libsodium", + "ext-openssl" + ] + }, + "ext-libxml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "none", + "build-shared": false, + "build-static": true, + "build-with-php": true + }, + "depends": [ + "ext-xml" + ] + }, + "ext-lz4": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "liblz4" + ], + "artifact": "ext-lz4", + "license": { + "type": "file", + "path": [ + "LICENSE" + ] + } + }, + "ext-mbregex": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom", + "build-shared": false, + "build-static": true + }, + "depends": [ + "onig", + "ext-mbstring" + ] + }, + "ext-mbstring": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom" + } + }, + "ext-mcrypt": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "no", + "Darwin": "no", + "Linux": "no" + }, + "notes": true + } + }, + "ext-memcache": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "ext-zlib", + "ext-session" + ], + "artifact": "ext-memcache", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-memcached": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "libmemcached", + "fastlz", + "ext-session", + "ext-zlib" + ], + "suggests": [ + "zstd", + "ext-igbinary", + "ext-msgpack", + "ext-session" + ], + "lang": "cpp", + "artifact": "memcached", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-mongodb": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "arg-type": "custom" + }, + "suggests": [ + "icu", + "openssl", + "zstd", + "zlib" + ], + "frameworks": [ + "CoreFoundation", + "Security" + ], + "artifact": "mongodb", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-msgpack": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type@windows": "enable", + "arg-type": "with" + }, + "depends": [ + "ext-session" + ], + "artifact": "msgpack", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-mysqli": { + "type": "php-extension", + "php-extension": { + "arg-type": "with", + "build-with-php": true + }, + "depends": [ + "ext-mysqlnd" + ] + }, + "ext-mysqlnd": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "with", + "build-with-php": true + }, + "depends": [ + "zlib" + ] + }, + "ext-oci8": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "no", + "Darwin": "no", + "Linux": "no" + }, + "notes": true + } + }, + "ext-odbc": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "unixodbc" + ] + }, + "ext-opcache": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "enable", + "arg-type": "custom", + "zend-extension": true + } + }, + "ext-openssl": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom", + "arg-type@windows": "with", + "build-with-php": true, + "notes": true + }, + "depends": [ + "openssl", + "zlib", + "ext-zlib" + ] + }, + "ext-opentelemetry": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "artifact": "opentelemetry", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-parallel": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type@windows": "with", + "notes": true + }, + "depends@windows": [ + "pthreads4w" + ], + "artifact": "parallel", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-password-argon2": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "libargon2", + "openssl" + ] + }, + "ext-pcntl": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no" + } + } + }, + "ext-pdo": { + "type": "php-extension" + }, + "ext-pdo_mysql": { + "type": "php-extension", + "php-extension": { + "arg-type": "with" + }, + "depends": [ + "ext-pdo", + "ext-mysqlnd" + ] + }, + "ext-pdo_odbc": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "unixodbc", + "ext-pdo", + "ext-odbc" + ] + }, + "ext-pdo_pgsql": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "custom" + }, + "depends@windows": [ + "postgresql-win" + ], + "depends": [ + "postgresql", + "ext-pdo", + "ext-pgsql" + ] + }, + "ext-pdo_sqlite": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "depends": [ + "sqlite", + "ext-pdo", + "ext-sqlite3" + ] + }, + "ext-pdo_sqlsrv": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "depends": [ + "ext-pdo", + "ext-sqlsrv" + ], + "artifact": "pdo_sqlsrv", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-pgsql": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends@windows": [ + "postgresql-win" + ], + "depends": [ + "postgresql" + ] + }, + "ext-phar": { + "type": "php-extension", + "depends": [ + "ext-zlib" + ] + }, + "ext-posix": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no" + } + } + }, + "ext-protobuf": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + } + }, + "artifact": "protobuf", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-rar": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Darwin": "partial" + }, + "notes": true + }, + "lang": "cpp", + "artifact": "rar", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-rdkafka": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "librdkafka" + ], + "lang": "cpp", + "artifact": "ext-rdkafka", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-readline": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path", + "build-shared": false, + "build-static": true + }, + "depends": [ + "libedit" + ] + }, + "ext-redis": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "suggests": [ + "zstd", + "liblz4", + "ext-session", + "ext-igbinary", + "ext-msgpack" + ], + "artifact": "redis", + "license": { + "type": "file", + "path": [ + "LICENSE", + "COPYING" + ] + } + }, + "ext-session": { + "type": "php-extension", + "php-extension": { + "build-with-php": true + } + }, + "ext-shmop": { + "type": "php-extension", + "php-extension": { + "build-with-php": true + } + }, + "ext-simdjson": { + "type": "php-extension", + "lang": "cpp", + "artifact": "ext-simdjson", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-simplexml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml" + ] + }, + "ext-snappy": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "snappy" + ], + "suggests": [ + "ext-apcu" + ], + "lang": "cpp", + "artifact": "ext-snappy", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-snmp": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type@windows": "with", + "arg-type": "with" + }, + "depends": [ + "net-snmp" + ] + }, + "ext-soap": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "ext-libxml", + "ext-session" + ] + }, + "ext-sockets": { + "type": "php-extension" + }, + "ext-sodium": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "depends": [ + "libsodium" + ] + }, + "ext-spx": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "no" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "zlib" + ], + "artifact": "spx", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-sqlite3": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "with", + "build-with-php": true + }, + "depends": [ + "sqlite" + ] + }, + "ext-sqlsrv": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "depends": [ + "unixodbc" + ], + "depends@linux": [ + "ext-pcntl" + ], + "lang": "cpp", + "artifact": "sqlsrv", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-ssh2": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "with" + }, + "depends": [ + "libssh2", + "ext-openssl", + "ext-zlib" + ], + "artifact": "ext-ssh2", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-swoole": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "libcares", + "brotli", + "nghttp2", + "zlib", + "ext-openssl", + "ext-curl" + ], + "suggests": [ + "zstd", + "ext-sockets", + "ext-swoole-hook-pgsql", + "ext-swoole-hook-mysql", + "ext-swoole-hook-sqlite", + "ext-swoole-hook-odbc" + ], + "suggests@linux": [ + "zstd", + "liburing" + ], + "lang": "cpp", + "artifact": "swoole", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-swoole-hook-mysql": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "ext-mysqlnd", + "ext-pdo", + "ext-pdo_mysql", + "ext-swoole" + ], + "suggests": [ + "ext-mysqli" + ] + }, + "ext-swoole-hook-odbc": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "unixodbc", + "ext-pdo", + "ext-swoole" + ] + }, + "ext-swoole-hook-pgsql": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip", + "Darwin": "partial" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "ext-pgsql", + "ext-pdo", + "ext-swoole" + ] + }, + "ext-swoole-hook-sqlite": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "ext-sqlite3", + "ext-pdo", + "ext-swoole" + ] + }, + "ext-swow": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "suggests": [ + "openssl", + "curl", + "ext-openssl", + "ext-curl" + ], + "artifact": "swow", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-sysvmsg": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + } + } + }, + "ext-sysvsem": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + } + } + }, + "ext-sysvshm": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + } + }, + "ext-tidy": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "tidy" + ] + }, + "ext-tokenizer": { + "type": "php-extension", + "php-extension": { + "build-with-php": true + } + }, + "ext-trader": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + } + }, + "artifact": "ext-trader", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-uuid": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "libuuid" + ], + "artifact": "ext-uuid", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-uv": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "libuv", + "ext-sockets" + ], + "artifact": "ext-uv", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xdebug": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "no", + "Darwin": "partial", + "Linux": "partial" + }, + "build-shared": true, + "build-static": false, + "notes": true, + "zend-extension": true + }, + "artifact": "xdebug", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xhprof": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "build-with-php": true, + "notes": true + }, + "depends": [ + "ext-ctype" + ], + "artifact": "xhprof", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xlswriter": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "suggests": [ + "openssl" + ], + "depends": [ + "ext-zlib", + "ext-zip" + ], + "artifact": "xlswriter", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with", + "build-with-php": true, + "notes": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-iconv" + ] + }, + "ext-xmlreader": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml", + "ext-dom" + ] + }, + "ext-xmlwriter": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml" + ] + }, + "ext-xsl": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "libxslt", + "ext-xml", + "ext-dom" + ] + }, + "ext-xz": { + "type": "php-extension", + "php-extension": { + "arg-type": "with" + }, + "depends": [ + "xz" + ], + "artifact": "ext-xz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-yac": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "fastlz", + "ext-igbinary" + ], + "artifact": "yac", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-yaml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type@windows": "with", + "arg-type": "with-path" + }, + "depends": [ + "libyaml" + ], + "artifact": "yaml", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-zip": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "enable" + }, + "depends@windows": [ + "libzip", + "zlib", + "bzip2", + "xz", + "ext-zlib", + "ext-bz2" + ], + "depends": [ + "libzip" + ], + "artifact": "ext-zip", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-zlib": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom", + "arg-type@windows": "enable", + "build-shared": false, + "build-static": true, + "build-with-php": true + }, + "depends": [ + "zlib" + ] + }, + "ext-zstd": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "zstd" + ], + "artifact": "ext-zstd", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} diff --git a/config/pkg.json b/config/pkg.json deleted file mode 100644 index d3b4fb909..000000000 --- a/config/pkg.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "go-xcaddy-aarch64-linux": { - "type": "custom" - }, - "go-xcaddy-aarch64-macos": { - "type": "custom" - }, - "go-xcaddy-x86_64-linux": { - "type": "custom" - }, - "go-xcaddy-x86_64-macos": { - "type": "custom" - }, - "musl-toolchain-aarch64-linux": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz" - }, - "musl-toolchain-x86_64-linux": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz" - }, - "nasm-x86_64-win": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", - "extract-files": { - "nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" - } - }, - "pkg-config-aarch64-linux": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "pkg-config-aarch64-macos": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-darwin.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "pkg-config-x86_64-linux": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "pkg-config-x86_64-macos": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-darwin.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "strawberry-perl-x86_64-win": { - "type": "url", - "url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip" - }, - "upx-aarch64-linux": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-arm64_linux\\.tar\\.xz", - "extract-files": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "upx-x86_64-linux": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-amd64_linux\\.tar\\.xz", - "extract-files": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "upx-x86_64-win": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-win64\\.zip", - "extract-files": { - "upx.exe": "{pkg_root_path}/bin/upx.exe" - } - }, - "zig-aarch64-linux": { - "type": "custom" - }, - "zig-aarch64-macos": { - "type": "custom" - }, - "zig-x86_64-linux": { - "type": "custom" - }, - "zig-x86_64-macos": { - "type": "custom" - }, - "zig-x86_64-win": { - "type": "custom" - } -} diff --git a/config/pkg.lib.json b/config/pkg.lib.json new file mode 100644 index 000000000..52531b214 --- /dev/null +++ b/config/pkg.lib.json @@ -0,0 +1,989 @@ +{ + "attr": { + "type": "library", + "artifact": "attr", + "license": { + "type": "file", + "path": "doc/COPYING.LGPL" + } + }, + "brotli": { + "type": "library", + "headers": [ + "brotli" + ], + "pkg-configs": [ + "libbrotlicommon", + "libbrotlidec", + "libbrotlienc" + ], + "artifact": "brotli", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "bzip2": { + "type": "library", + "headers": [ + "bzlib.h" + ], + "artifact": "bzip2", + "license": { + "type": "text", + "text": "This program, \"bzip2\", the associated library \"libbzip2\", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\n 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\n 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nJulian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010\n\nPATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement." + } + }, + "curl": { + "type": "library", + "depends@windows": [ + "zlib", + "libssh2", + "nghttp2" + ], + "depends": [ + "openssl", + "zlib" + ], + "suggests@windows": [ + "brotli", + "zstd" + ], + "suggests": [ + "libssh2", + "brotli", + "nghttp2", + "nghttp3", + "ngtcp2", + "zstd", + "libcares", + "ldap" + ], + "headers": [ + "curl" + ], + "frameworks": [ + "CoreFoundation", + "CoreServices", + "SystemConfiguration" + ], + "artifact": "curl", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "fastlz": { + "type": "library", + "headers": [ + "fastlz/fastlz.h" + ], + "artifact": "fastlz", + "license": { + "type": "file", + "path": "LICENSE.MIT" + } + }, + "freetype": { + "type": "library", + "depends": [ + "zlib" + ], + "suggests": [ + "libpng", + "bzip2", + "brotli" + ], + "headers": [ + "freetype2/freetype/freetype.h", + "freetype2/ft2build.h" + ], + "artifact": "freetype", + "license": { + "type": "file", + "path": "LICENSE.TXT" + } + }, + "gettext": { + "type": "library", + "depends": [ + "libiconv" + ], + "suggests": [ + "ncurses", + "libxml2" + ], + "frameworks": [ + "CoreFoundation" + ], + "artifact": "gettext", + "license": { + "type": "file", + "path": "gettext-runtime/intl/COPYING.LIB" + } + }, + "glfw": { + "type": "library", + "frameworks": [ + "CoreVideo", + "OpenGL", + "Cocoa", + "IOKit" + ], + "artifact": "ext-glfw", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "gmp": { + "type": "library", + "headers": [ + "gmp.h" + ], + "artifact": "gmp", + "license": { + "type": "text", + "text": "Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs." + } + }, + "gmssl": { + "type": "library", + "frameworks": [ + "Security" + ], + "artifact": "gmssl", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "grpc": { + "type": "library", + "depends": [ + "zlib", + "openssl", + "libcares" + ], + "pkg-configs": [ + "grpc" + ], + "frameworks": [ + "CoreFoundation" + ], + "artifact": "grpc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "icu": { + "type": "library", + "pkg-configs": [ + "icu-uc", + "icu-i18n", + "icu-io" + ], + "artifact": "icu", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "icu-static-win": { + "type": "library", + "headers@windows": [ + "unicode" + ], + "artifact": "icu-static-win", + "license": { + "type": "text", + "text": "none" + } + }, + "imagemagick": { + "type": "library", + "depends": [ + "zlib", + "libjpeg", + "libjxl", + "libpng", + "libwebp", + "freetype", + "libtiff", + "libheif", + "bzip2" + ], + "suggests": [ + "zstd", + "xz", + "libzip", + "libxml2" + ], + "pkg-configs": [ + "Magick++-7.Q16HDRI", + "MagickCore-7.Q16HDRI", + "MagickWand-7.Q16HDRI" + ], + "artifact": "imagemagick", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "imap": { + "type": "library", + "suggests": [ + "openssl" + ], + "artifact": "imap", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "jbig": { + "type": "library", + "headers": [ + "jbig.h", + "jbig85.h", + "jbig_ar.h" + ], + "artifact": "jbig", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "ldap": { + "type": "library", + "depends": [ + "openssl", + "zlib", + "gmp", + "libsodium" + ], + "pkg-configs": [ + "ldap", + "lber" + ], + "artifact": "ldap", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "lerc": { + "type": "library", + "artifact": "lerc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libacl": { + "type": "library", + "depends": [ + "attr" + ], + "artifact": "libacl", + "license": { + "type": "file", + "path": "doc/COPYING.LGPL" + } + }, + "libaom": { + "type": "library", + "artifact": "libaom", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libargon2": { + "type": "library", + "artifact": "libargon2", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libavif": { + "type": "library", + "artifact": "libavif", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libcares": { + "type": "library", + "headers": [ + "ares.h", + "ares_dns.h", + "ares_nameser.h" + ], + "artifact": "libcares", + "license": { + "type": "file", + "path": "LICENSE.md" + } + }, + "libde265": { + "type": "library", + "artifact": "libde265", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libedit": { + "type": "library", + "depends": [ + "ncurses" + ], + "artifact": "libedit", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libevent": { + "type": "library", + "depends": [ + "openssl" + ], + "artifact": "libevent", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libffi": { + "type": "library", + "headers@windows": [ + "ffi.h", + "fficonfig.h", + "ffitarget.h" + ], + "headers": [ + "ffi.h", + "ffitarget.h" + ], + "artifact": "libffi", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libffi-win": { + "type": "library", + "headers@windows": [ + "ffi.h", + "ffitarget.h", + "fficonfig.h" + ], + "artifact": "libffi-win", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libheif": { + "type": "library", + "depends": [ + "libde265", + "libwebp", + "libaom", + "zlib", + "brotli" + ], + "artifact": "libheif", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libiconv": { + "type": "library", + "headers": [ + "iconv.h", + "libcharset.h", + "localcharset.h" + ], + "artifact": "libiconv", + "license": { + "type": "file", + "path": "COPYING.LIB" + } + }, + "libiconv-win": { + "type": "library", + "artifact": "libiconv-win", + "license": { + "type": "file", + "path": "source/COPYING" + } + }, + "libjpeg": { + "type": "library", + "suggests@windows": [ + "zlib" + ], + "artifact": "libjpeg", + "license": { + "type": "file", + "path": "LICENSE.md" + } + }, + "libjxl": { + "type": "library", + "depends": [ + "brotli", + "libjpeg", + "libpng", + "libwebp" + ], + "pkg-configs": [ + "libjxl", + "libjxl_cms", + "libjxl_threads", + "libhwy" + ], + "artifact": "libjxl", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "liblz4": { + "type": "library", + "artifact": "liblz4", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libmemcached": { + "type": "library", + "artifact": "libmemcached", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libpng": { + "type": "library", + "depends": [ + "zlib" + ], + "headers@windows": [ + "png.h", + "pngconf.h" + ], + "headers": [ + "png.h", + "pngconf.h", + "pnglibconf.h" + ], + "artifact": "libpng", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "librabbitmq": { + "type": "library", + "depends": [ + "openssl" + ], + "artifact": "librabbitmq", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "librdkafka": { + "type": "library", + "suggests": [ + "curl", + "liblz4", + "openssl", + "zlib", + "zstd" + ], + "pkg-configs": [ + "rdkafka++-static", + "rdkafka-static" + ], + "artifact": "librdkafka", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libsodium": { + "type": "library", + "artifact": "libsodium", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libssh2": { + "type": "library", + "depends": [ + "openssl" + ], + "headers": [ + "libssh2.h", + "libssh2_publickey.h", + "libssh2_sftp.h" + ], + "artifact": "libssh2", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libtiff": { + "type": "library", + "depends": [ + "zlib", + "libjpeg" + ], + "suggests": [ + "lerc", + "libwebp", + "jbig", + "xz", + "zstd" + ], + "artifact": "libtiff", + "license": { + "type": "file", + "path": "LICENSE.md" + } + }, + "liburing": { + "type": "library", + "headers@linux": [ + "liburing/", + "liburing.h" + ], + "pkg-configs": [ + "liburing", + "liburing-ffi" + ], + "artifact": "liburing", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libuuid": { + "type": "library", + "headers": [ + "uuid/uuid.h" + ], + "artifact": "libuuid", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libuv": { + "type": "library", + "artifact": "libuv", + "license": [ + { + "type": "file", + "path": "LICENSE" + }, + { + "type": "file", + "path": "LICENSE-extra" + } + ] + }, + "libwebp": { + "type": "library", + "pkg-configs": [ + "libwebp", + "libwebpdecoder", + "libwebpdemux", + "libwebpmux", + "libsharpyuv" + ], + "artifact": "libwebp", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libxml2": { + "type": "library", + "depends@windows": [ + "libiconv-win" + ], + "depends": [ + "libiconv" + ], + "suggests@windows": [ + "zlib" + ], + "suggests": [ + "xz", + "zlib" + ], + "headers": [ + "libxml2" + ], + "pkg-configs": [ + "libxml-2.0" + ], + "artifact": "libxml2", + "license": { + "type": "file", + "path": "Copyright" + } + }, + "libxslt": { + "type": "library", + "depends": [ + "libxml2" + ], + "artifact": "libxslt", + "license": { + "type": "file", + "path": "Copyright" + } + }, + "libyaml": { + "type": "library", + "headers": [ + "yaml.h" + ], + "artifact": "libyaml", + "license": { + "type": "file", + "path": "License" + } + }, + "libzip": { + "type": "library", + "depends@windows": [ + "zlib", + "bzip2", + "xz" + ], + "depends": [ + "zlib" + ], + "suggests@windows": [ + "zstd", + "openssl" + ], + "suggests": [ + "bzip2", + "xz", + "zstd", + "openssl" + ], + "headers": [ + "zip.h", + "zipconf.h" + ], + "artifact": "libzip", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "mimalloc": { + "type": "library", + "artifact": "mimalloc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ncurses": { + "type": "library", + "artifact": "ncurses", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "net-snmp": { + "type": "library", + "depends": [ + "openssl", + "zlib" + ], + "pkg-configs": [ + "netsnmp", + "netsnmp-agent" + ], + "artifact": "net-snmp", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "nghttp2": { + "type": "library", + "depends": [ + "zlib", + "openssl" + ], + "suggests": [ + "libxml2", + "nghttp3", + "ngtcp2" + ], + "headers": [ + "nghttp2" + ], + "artifact": "nghttp2", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "nghttp3": { + "type": "library", + "depends": [ + "openssl" + ], + "headers": [ + "nghttp3" + ], + "artifact": "nghttp3", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "ngtcp2": { + "type": "library", + "depends": [ + "openssl" + ], + "suggests": [ + "nghttp3", + "brotli" + ], + "headers": [ + "ngtcp2" + ], + "artifact": "ngtcp2", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "onig": { + "type": "library", + "headers": [ + "oniggnu.h", + "oniguruma.h" + ], + "artifact": "onig", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "openssl": { + "type": "library", + "depends": [ + "zlib" + ], + "headers": [ + "openssl" + ], + "artifact": "openssl", + "license": { + "type": "file", + "path": "LICENSE.txt" + } + }, + "postgresql": { + "type": "library", + "depends": [ + "libiconv", + "libxml2", + "openssl", + "zlib", + "libedit" + ], + "suggests": [ + "icu", + "libxslt", + "ldap", + "zstd" + ], + "pkg-configs": [ + "libpq" + ], + "artifact": "postgresql", + "license": { + "type": "file", + "path": "COPYRIGHT" + } + }, + "postgresql-win": { + "type": "library", + "artifact": "postgresql-win", + "license": { + "type": "text", + "text": "PostgreSQL Database Management System\n(also known as Postgres, formerly as Postgres95)\n\nPortions Copyright (c) 1996-2025, The PostgreSQL Global Development Group\n\nPortions Copyright (c) 1994, The Regents of the University of California\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written\nagreement is hereby granted, provided that the above copyright notice\nand this paragraph and the following two paragraphs appear in all\ncopies.\n\nIN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY\nFOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nINCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\nDOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\nTHE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS\nTO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS." + } + }, + "pthreads4w": { + "type": "library", + "artifact": "pthreads4w", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "qdbm": { + "type": "library", + "headers@windows": [ + "depot.h" + ], + "artifact": "qdbm", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "re2c": { + "type": "library", + "artifact": "re2c", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "readline": { + "type": "library", + "depends": [ + "ncurses" + ], + "artifact": "readline", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "snappy": { + "type": "library", + "depends": [ + "zlib" + ], + "headers": [ + "snappy.h", + "snappy-c.h", + "snappy-sinksource.h", + "snappy-stubs-public.h" + ], + "artifact": "snappy", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "sqlite": { + "type": "library", + "headers": [ + "sqlite3.h", + "sqlite3ext.h" + ], + "artifact": "sqlite", + "license": { + "type": "text", + "text": "The author disclaims copyright to this source code. In place of\na legal notice, here is a blessing:\n\n * May you do good and not evil.\n * May you find forgiveness for yourself and forgive others.\n * May you share freely, never taking more than you give." + } + }, + "tidy": { + "type": "library", + "artifact": "tidy", + "license": { + "type": "file", + "path": "README/LICENSE.md" + } + }, + "unixodbc": { + "type": "library", + "depends": [ + "libiconv" + ], + "artifact": "unixodbc", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "watcher": { + "type": "library", + "headers": [ + "wtr/watcher-c.h" + ], + "artifact": "watcher", + "license": { + "type": "file", + "path": "license" + } + }, + "xz": { + "type": "library", + "depends": [ + "libiconv" + ], + "headers@windows": [ + "lzma", + "lzma.h" + ], + "headers": [ + "lzma" + ], + "artifact": "xz", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "zlib": { + "type": "library", + "headers": [ + "zlib.h", + "zconf.h" + ], + "artifact": "zlib", + "license": { + "type": "text", + "text": "(C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly Mark Adler\njloup@gzip.org madler@alumni.caltech.edu" + } + }, + "zstd": { + "type": "library", + "headers@windows": [ + "zstd.h", + "zstd_errors.h" + ], + "headers": [ + "zdict.h", + "zstd.h", + "zstd_errors.h" + ], + "artifact": "zstd", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} diff --git a/config/pkg.target.json b/config/pkg.target.json new file mode 100644 index 000000000..b5e4a7c8b --- /dev/null +++ b/config/pkg.target.json @@ -0,0 +1,95 @@ +{ + "vswhere": { + "type": "target", + "artifact": "vswhere" + }, + "pkg-config": { + "type": "target", + "static-bins": [ + "pkg-config" + ], + "artifact": "pkg-config" + }, + "php": { + "type": "target", + "artifact": "php-src", + "depends@macos": [ + "libxml2" + ] + }, + "php-cli": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "php-micro": { + "type": "virtual-target", + "artifact": "micro", + "depends": [ + "php" + ] + }, + "php-cgi": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "php-fpm": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "php-embed": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "frankenphp": { + "type": "virtual-target", + "artifact": "frankenphp", + "depends": [ + "php-embed", + "go-xcaddy" + ], + "depends@macos": [ + "php-embed", + "go-xcaddy", + "libxml2" + ] + }, + "go-xcaddy": { + "type": "target", + "artifact": "go-xcaddy", + "static-bins": [ + "xcaddy" + ] + }, + "musl-toolchain": { + "type": "target", + "artifact": "musl-toolchain" + }, + "strawberry-perl": { + "type": "target", + "artifact": "strawberry-perl" + }, + "upx": { + "type": "target", + "artifact": "upx" + }, + "zig": { + "type": "target", + "artifact": "zig" + }, + "nasm": { + "type": "target", + "artifact": "nasm" + }, + "php-sdk-binary-tools": { + "type": "target", + "artifact": "php-sdk-binary-tools" + } +} diff --git a/phpstan.neon b/phpstan.neon index a8c1c72c5..cf6e49742 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,3 +17,4 @@ parameters: - ./src/globals/ext-tests/swoole.php - ./src/globals/ext-tests/swoole.phpt - ./src/globals/test-extensions.php + - ./src/SPC/ diff --git a/spc.registry.json b/spc.registry.json new file mode 100644 index 000000000..7c5a8ce7b --- /dev/null +++ b/spc.registry.json @@ -0,0 +1,32 @@ +{ + "name": "internal", + "autoload": "vendor/autoload.php", + "doctor": { + "psr-4": { + "StaticPHP\\Doctor\\Item": "src/StaticPHP/Doctor/Item" + } + }, + "package": { + "psr-4": { + "Package": "src/Package" + }, + "config": [ + "config/pkg.ext.json", + "config/pkg.lib.json", + "config/pkg.target.json" + ] + }, + "artifact": { + "config": [ + "config/artifact.json" + ], + "psr-4": { + "Package\\Artifact": "src/Package/Artifact" + } + }, + "command": { + "psr-4": { + "Package\\Command": "src/Package/Command" + } + } +} diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php new file mode 100644 index 000000000..fd7ef2a4f --- /dev/null +++ b/src/Package/Artifact/go_xcaddy.php @@ -0,0 +1,82 @@ + 'amd64', + 'aarch64' => 'arm64', + default => throw new ValidationException('Unsupported architecture: ' . $name), + }; + $os = match (explode('-', $name)[0]) { + 'linux' => 'linux', + 'macos' => 'darwin', + default => throw new ValidationException('Unsupported OS: ' . $name), + }; + $hash = match ("{$os}-{$arch}") { + 'linux-amd64' => '2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613', + 'linux-arm64' => '05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae', + 'darwin-amd64' => '5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef', + 'darwin-arm64' => '544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c', + }; + $go_version = '1.25.0'; + $url = "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "go{$go_version}.{$os}-{$arch}.tar.gz"; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + // verify hash + $file_hash = hash_file('sha256', $path); + if ($file_hash !== $hash) { + throw new ValidationException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); + } + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $go_version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $go_version); + } + + #[AfterBinaryExtract('go-xcaddy', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function afterExtract(string $target_path): void + { + if (file_exists("{$target_path}/bin/go") && file_exists("{$target_path}/bin/xcaddy")) { + return; + } + + $sanitizedPath = getenv('PATH'); + if (PHP_OS_FAMILY === 'Linux' && !LinuxUtil::isMuslDist()) { + $sanitizedPath = preg_replace('#(:?/?[^:]*musl[^:]*)#', '', $sanitizedPath); + $sanitizedPath = preg_replace('#^:|:$|::#', ':', $sanitizedPath); // clean up colons + } + + shell()->appendEnv([ + 'PATH' => "{$target_path}/bin:{$sanitizedPath}", + 'GOROOT' => "{$target_path}", + 'GOBIN' => "{$target_path}/bin", + 'GOPATH' => "{$target_path}/go", + ])->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); + GlobalEnvManager::addPathIfNotExists("{$target_path}/bin"); + } +} diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php new file mode 100644 index 000000000..0e40c43ea --- /dev/null +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -0,0 +1,123 @@ +addArgument( + 'php-version', + InputArgument::REQUIRED, + 'PHP version (e.g., 8.4, 8.3, 8.2, 8.1, 8.0, 7.4, or specific like 8.4.5)', + ); + + // Downloader options + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions()); + + // Additional options + $this->addOption('keep-source', null, null, 'Keep extracted source directory (do not remove source/php-src)'); + } + + public function handle(): int + { + $php_ver = $this->getArgument('php-version'); + + // Validate version format + if (!$this->isValidPhpVersion($php_ver)) { + $this->output->writeln("Invalid PHP version '{$php_ver}'!"); + $this->output->writeln('Supported formats: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, or specific version like 8.4.5'); + return static::FAILURE; + } + + $cache = ApplicationContext::get(ArtifactCache::class); + + // Check if php-src is already locked + $source_info = $cache->getSourceInfo('php-src'); + if ($source_info !== null) { + $current_version = $source_info['version'] ?? 'unknown'; + $this->output->writeln("Current PHP version: {$current_version}, removing old PHP source cache..."); + + // Remove cache entry and optionally the downloaded file + $cache->removeSource('php-src', delete_file: true); + } + + // Remove extracted source directory if exists and --keep-source not set + $source_dir = SOURCE_PATH . '/php-src'; + if (!$this->getOption('keep-source') && is_dir($source_dir)) { + $this->output->writeln('Removing extracted PHP source directory...'); + InteractiveTerm::indicateProgress('Removing: ' . $source_dir); + FileSystem::removeDir($source_dir); + InteractiveTerm::finish('Removed: ' . $source_dir); + } + + // Set the PHP version for download + // This defines the version that will be used when resolving php-src artifact + define('SPC_BUILD_PHP_VERSION', $php_ver); + + // Download new PHP source + $this->output->writeln("Downloading PHP {$php_ver} source..."); + + $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->input->getOptions()); + $downloader = new ArtifactDownloader($downloaderOptions); + + // Get php-src artifact from php package + $php_package = PackageLoader::getPackage('php'); + $artifact = $php_package->getArtifact(); + + if ($artifact === null) { + $this->output->writeln('Failed to get php-src artifact!'); + return static::FAILURE; + } + + $downloader->add($artifact); + $downloader->download(); + + // Get the new version info + $new_source_info = $cache->getSourceInfo('php-src'); + $new_version = $new_source_info['version'] ?? $php_ver; + + $this->output->writeln(''); + $this->output->writeln("Successfully switched to PHP {$new_version}!"); + + return static::SUCCESS; + } + + /** + * Validate PHP version format. + * + * Accepts: + * - Major.Minor format: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 + * - Full version format: 8.4.5, 8.3.12, etc. + */ + private function isValidPhpVersion(string $version): bool + { + // Check major.minor format (e.g., 8.4) + if (in_array($version, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'], true)) { + return true; + } + + // Check full version format (e.g., 8.4.5) + if (preg_match('/^\d+\.\d+\.\d+$/', $version)) { + return true; + } + + return false; + } +} diff --git a/src/Package/Library/libiconv.php b/src/Package/Library/libiconv.php new file mode 100644 index 000000000..ac91d188a --- /dev/null +++ b/src/Package/Library/libiconv.php @@ -0,0 +1,27 @@ +configure( + '--enable-extra-encodings', + '--enable-year2038', + ) + ->make('install-lib', with_install: false) + ->make('install-lib', with_install: false, dir: "{$package->getSourceDir()}/libcharset"); + $package->patchLaDependencyPrefix(); + } +} diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php new file mode 100644 index 000000000..4767efcb0 --- /dev/null +++ b/src/Package/Library/libxml2.php @@ -0,0 +1,54 @@ +optionalPackage( + 'zlib', + '-DLIBXML2_WITH_ZLIB=ON ' . + "-DZLIB_LIBRARY={$package->getLibDir()}/libz.a " . + "-DZLIB_INCLUDE_DIR={$package->getIncludeDir()}", + '-DLIBXML2_WITH_ZLIB=OFF', + ) + ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) + ->addConfigureArgs( + '-DLIBXML2_WITH_ICONV=ON', + '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md + '-DLIBXML2_WITH_PYTHON=OFF', + '-DLIBXML2_WITH_PROGRAMS=OFF', + '-DLIBXML2_WITH_TESTS=OFF', + ); + + if (SystemTarget::getTargetOS() === 'Linux') { + $cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF'); + } + + $cmake->build(); + + FileSystem::replaceFileStr( + BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', + '-lxml2 -liconv', + '-lxml2' + ); + FileSystem::replaceFileStr( + BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', + '-lxml2', + '-lxml2 -liconv' + ); + } +} diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php new file mode 100644 index 000000000..6636ccad5 --- /dev/null +++ b/src/Package/Library/postgresql.php @@ -0,0 +1,22 @@ +cd($package->getSourceDir()) + ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + } +} diff --git a/src/Package/README.md b/src/Package/README.md new file mode 100644 index 000000000..81a45fa6f --- /dev/null +++ b/src/Package/README.md @@ -0,0 +1,3 @@ +# Package Implementation + +This directory contains the implementation of the `Package` module, which provides functionality for managing and manipulating packages within the system. diff --git a/src/Package/Target/go_xcaddy.php b/src/Package/Target/go_xcaddy.php new file mode 100644 index 000000000..cafaacf7e --- /dev/null +++ b/src/Package/Target/go_xcaddy.php @@ -0,0 +1,24 @@ +getSourceDir()}/main/php_version.h")) { + throw new WrongUsageException('PHP source files are not available, you need to download them first'); + } + + $file = file_get_contents("{$artifact->getSourceDir()}/main/php_version.h"); + if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { + return intval($match[1]); + } + + throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); + } + + #[InitPackage] + public function init(TargetPackage $package): void + { + // universal build options (may move to base class later) + $package->addBuildOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside'); + + // basic build argument and options for PHP + $package->addBuildArgument('extensions', InputArgument::REQUIRED, 'Comma-separated list of static extensions to build'); + $package->addBuildOption('no-strip', null, null, 'build without strip, keep symbols to debug'); + $package->addBuildOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)'); + + // php configure and extra patch options + $package->addBuildOption('disable-opcache-jit', null, null, 'Disable opcache jit'); + $package->addBuildOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php'); + $package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d'); + $package->addBuildOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); + $package->addBuildOption('enable-zts', null, null, 'Enable thread safe support'); + + // phpmicro build options + if ($package->getName() === 'php' || $package->getName() === 'php-micro') { + $package->addBuildOption('with-micro-fake-cli', null, null, 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"'); + $package->addBuildOption('without-micro-ext-test', null, null, 'Disable phpmicro with extension test code'); + $package->addBuildOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)'); + $package->addBuildOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); + } + + // frankenphp build options + if ($package->getName() === 'php' || $package->getName() === 'frankenphp') { + $package->addBuildOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); + } + + // embed build options + if ($package->getName() === 'php' || $package->getName() === 'php-embed') { + $package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); + } + + // legacy php target build options + V2CompatLayer::addLegacyBuildOptionsForPhp($package); + if ($package->getName() === 'php') { + $package->addBuildOption('build-micro', null, null, 'Build micro SAPI'); + $package->addBuildOption('build-cli', null, null, 'Build cli SAPI'); + $package->addBuildOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); + $package->addBuildOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); + $package->addBuildOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); + $package->addBuildOption('build-cgi', null, null, 'Build cgi SAPI'); + $package->addBuildOption('build-all', null, null, 'Build all SAPI'); + } + } + + #[ResolveBuild] + public function resolveBuild(TargetPackage $package): array + { + // Parse extensions and additional packages for all php-* targets + $static_extensions = parse_extension_list($package->getBuildArgument('extensions')); + $additional_libraries = parse_comma_list($package->getBuildOption('with-libs')); + $additional_packages = parse_comma_list($package->getBuildOption('with-packages')); + $additional_packages = array_merge($additional_libraries, $additional_packages); + $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); + + $extensions_pkg = array_map( + fn ($x) => "ext-{$x}", + array_values(array_unique([...$static_extensions, ...$shared_extensions])) + ); + + // get instances + foreach ($extensions_pkg as $extension) { + $extname = substr($extension, 4); + if (!PackageLoader::hasPackage($extension)) { + throw new WrongUsageException("Extension [{$extname}] does not exist. Please check your extension name."); + } + $instance = PackageLoader::getPackage($extension); + if (!$instance instanceof PhpExtensionPackage) { + throw new WrongUsageException("Package [{$extension}] is not a PHP extension package"); + } + // set build static/shared + if (in_array($extname, $static_extensions)) { + $instance->setBuildStatic(); + } + if (in_array($extname, $shared_extensions)) { + $instance->setBuildShared(); + } + } + + return [...$extensions_pkg, ...$additional_packages]; + } + + #[Validate] + public function validate(Package $package): void + { + // frankenphp + if ($package->getName() === 'frankenphp' && $package instanceof TargetPackage) { + if (!$package->getBuildOption('enable-zts')) { + throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); + } + // frankenphp doesn't support windows, BSD is currently not supported by static-php-cli + if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { + throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); + } + } + // linux does not support loading shared libraries when target is pure static + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') { + throw new WrongUsageException( + 'Linux does not support loading shared libraries when linking libc statically. ' . + 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' + ); + } + } + + #[Info] + public function info(Package $package, PackageInstaller $installer): array + { + /** @var TargetPackage $package */ + if ($package->getName() !== 'php') { + return []; + } + $sapis = array_filter([ + $installer->getBuildPackage('php-cli') ? 'cli' : null, + $installer->getBuildPackage('php-fpm') ? 'fpm' : null, + $installer->getBuildPackage('php-micro') ? 'micro' : null, + $installer->getBuildPackage('php-cgi') ? 'cgi' : null, + $installer->getBuildPackage('php-embed') ? 'embed' : null, + $installer->getBuildPackage('frankenphp') ? 'frankenphp' : null, + ]); + $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() === 'php-extension'); + $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); + $install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-')); + return [ + 'Build OS' => SystemTarget::getTargetOS() . ' (' . SystemTarget::getTargetArch() . ')', + 'Build Target' => getenv('SPC_TARGET') ?: '', + 'Build Toolchain' => ToolchainManager::getToolchainClass(), + 'Build SAPI' => implode(', ', $sapis), + 'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)), + 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), + 'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)), + ]; + } + + #[BeforeStage('php', 'build')] + public function beforeBuild(PackageBuilder $builder, Package $package): void + { + // Process -I option + $custom_ini = []; + foreach ($builder->getOption('with-hardcoded-ini', []) as $value) { + [$source_name, $ini_value] = explode('=', $value, 2); + $custom_ini[$source_name] = $ini_value; + logger()->info("Adding hardcoded INI [{$source_name} = {$ini_value}]"); + } + if (!empty($custom_ini)) { + SourcePatcher::patchHardcodedINI($package->getSourceDir(), $custom_ini); + } + + // Patch StaticPHP version + // detect patch (remove this when 8.3 deprecated) + $file = FileSystem::readFile("{$package->getSourceDir()}/main/main.c"); + if (!str_contains($file, 'static-php-cli.version')) { + $version = SPC_VERSION; + logger()->debug('Inserting static-php-cli.version to php-src'); + $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); + FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file); + } + + // clean old modules that may conflict with the new php build + FileSystem::removeDir(BUILD_MODULES_PATH); + } + + #[BeforeStage('php', 'unix-buildconf')] + #[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('unix-buildconf')] + 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('unix-configure')] + 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->isBuildPackage('php-cli') ? '--enable-cli' : '--disable-cli'; + $args[] = $installer->isBuildPackage('php-fpm') ? '--enable-fpm' : '--disable-fpm'; + $args[] = $installer->isBuildPackage('php-micro') ? match (SystemTarget::getTargetOS()) { + 'Linux' => '--enable-micro=all-static', + default => '--enable-micro', + } : null; + $args[] = $installer->isBuildPackage('php-cgi') ? '--enable-cgi' : '--disable-cgi'; + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + $args[] = $installer->isBuildPackage('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('unix-make')] + 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->isBuildPackage('php-cli')) { + $package->runStage('unix-make-cli'); + } + if ($installer->isBuildPackage('php-fpm')) { + $package->runStage('unix-make-fpm'); + } + if ($installer->isBuildPackage('php-cgi')) { + $package->runStage('unix-make-cgi'); + } + } + + #[Stage('unix-make-cli')] + 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"); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(TargetPackage $package): void + { + // virtual target, do nothing + if ($package->getName() !== 'php') { + return; + } + + $package->runStage('unix-buildconf'); + $package->runStage('unix-configure'); + $package->runStage('unix-make'); + } + + /** + * 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 = []; + foreach ($installer->getResolvedPackages() as $package) { + /** @var PhpExtensionPackage $package */ + if ($package->getType() !== 'php-extension' || !$package instanceof PhpExtensionPackage) { + continue; + } + + // build-shared=true, build-static=false, build-with-php=true + if ($package->isBuildShared() && !$package->isBuildStatic() && $package->isBuildWithPhp()) { + $arg[] = $package->getPhpConfigureArg(SystemTarget::getTargetOS(), true); + } elseif ($package->isBuildStatic()) { + $arg[] = $package->getPhpConfigureArg(SystemTarget::getTargetOS(), false); + } + } + $str = implode(' ', $arg); + logger()->debug("Static extension configure args: {$str}"); + return $str; + } + + 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/pkgconfig.php b/src/Package/Target/pkgconfig.php new file mode 100644 index 000000000..e99e2d7c0 --- /dev/null +++ b/src/Package/Target/pkgconfig.php @@ -0,0 +1,45 @@ +appendEnv([ + 'CFLAGS' => '-Wimplicit-function-declaration -Wno-int-conversion', + 'LDFLAGS' => $toolchain->isStatic() ? '--static' : '', + ]) + ->configure( + '--with-internal-glib', + '--disable-host-tool', + '--without-sysroot', + '--without-system-include-path', + '--without-system-library-path', + '--without-pc-path', + ) + ->make(with_install: 'install-exec'); + + shell()->exec('strip ' . BUILD_ROOT_PATH . '/bin/pkg-config'); + } +} diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php new file mode 100644 index 000000000..778e9c600 --- /dev/null +++ b/src/StaticPHP/Artifact/Artifact.php @@ -0,0 +1,544 @@ + Bind custom binary fetcher callbacks */ + protected mixed $custom_binary_callbacks = []; + + /** @var null|callable Bind custom source extract callback (completely takes over extraction) */ + protected mixed $source_extract_callback = null; + + /** @var null|array{callback: callable, platforms: string[]} Bind custom binary extract callback (completely takes over extraction) */ + protected ?array $binary_extract_callback = null; + + /** @var array After source extract hooks */ + protected array $after_source_extract_callbacks = []; + + /** @var array After binary extract hooks */ + protected array $after_binary_extract_callbacks = []; + + public function __construct(protected readonly string $name, ?array $config = null) + { + $this->config = $config ?? ArtifactConfig::get($name); + if ($this->config === null) { + throw new WrongUsageException("Artifact '{$name}' not found."); + } + } + + public function getName(): string + { + return $this->name; + } + + /** + * Checks if the source of an artifact is already downloaded. + * + * @param bool $compare_hash Whether to compare hash of the downloaded source + */ + public function isSourceDownloaded(bool $compare_hash = false): bool + { + return ApplicationContext::get(ArtifactCache::class)->isSourceDownloaded($this->name, $compare_hash); + } + + /** + * Checks if the binary of an artifact is already downloaded for the specified target OS. + * + * @param null|string $target_os Target OS platform string, null for current platform + * @param bool $compare_hash Whether to compare hash of the downloaded binary + */ + public function isBinaryDownloaded(?string $target_os = null, bool $compare_hash = false): bool + { + $target_os = $target_os ?? SystemTarget::getCurrentPlatformString(); + return ApplicationContext::get(ArtifactCache::class)->isBinaryDownloaded($this->name, $target_os, $compare_hash); + } + + public function shouldUseBinary(): bool + { + $platform = SystemTarget::getCurrentPlatformString(); + return $this->isBinaryDownloaded($platform) && $this->hasPlatformBinary(); + } + + /** + * Checks if the source of an artifact is already extracted. + * + * @param bool $compare_hash Whether to compare hash of the extracted source + */ + public function isSourceExtracted(bool $compare_hash = false): bool + { + $target_path = $this->getSourceDir(); + + if (!is_dir($target_path)) { + return false; + } + + if (!$compare_hash) { + return true; + } + + // Get expected hash from cache + $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + if ($cache_info === null) { + return false; + } + + $expected_hash = $cache_info['hash'] ?? null; + + // Local source: always consider extracted if directory exists + if ($expected_hash === null) { + return true; + } + + // Check hash marker file + $hash_file = "{$target_path}/.spc-hash"; + if (!file_exists($hash_file)) { + return false; + } + + return FileSystem::readFile($hash_file) === $expected_hash; + } + + /** + * Checks if the binary of an artifact is already extracted for the specified target OS. + * + * @param null|string $target_os Target OS platform string, null for current platform + * @param bool $compare_hash Whether to compare hash of the extracted binary + */ + public function isBinaryExtracted(?string $target_os = null, bool $compare_hash = false): bool + { + $target_os = $target_os ?? SystemTarget::getCurrentPlatformString(); + $extract_config = $this->getBinaryExtractConfig(); + $mode = $extract_config['mode']; + + // For merge mode, check marker file + if ($mode === 'merge') { + $target_path = $extract_config['path']; + $marker_file = "{$target_path}/.spc-{$this->name}-installed"; + + if (!file_exists($marker_file)) { + return false; + } + + if (!$compare_hash) { + return true; + } + + // Get expected hash from cache + $cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os); + if ($cache_info === null) { + return false; + } + + $expected_hash = $cache_info['hash'] ?? null; + if ($expected_hash === null) { + return true; // Local binary + } + + $installed_hash = FileSystem::readFile($marker_file); + return $installed_hash === $expected_hash; + } + + // For selective mode, cannot reliably check extraction status + if ($mode === 'selective') { + return false; + } + + // For standalone mode, check directory and hash + $target_path = $extract_config['path']; + + if (!is_dir($target_path)) { + return false; + } + + if (!$compare_hash) { + return true; + } + + // Get expected hash from cache + $cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os); + if ($cache_info === null) { + return false; + } + + $expected_hash = $cache_info['hash'] ?? null; + + // Local binary: always consider extracted if directory exists + if ($expected_hash === null) { + return true; + } + + // Check hash marker file + $hash_file = "{$target_path}/.spc-hash"; + if (!file_exists($hash_file)) { + return false; + } + + return FileSystem::readFile($hash_file) === $expected_hash; + } + + /** + * Checks if the artifact has a source defined. + */ + public function hasSource(): bool + { + return isset($this->config['source']) || $this->custom_source_callback !== null; + } + + /** + * Checks if the artifact has a local binary defined for the current system target. + */ + public function hasPlatformBinary(): bool + { + $target = SystemTarget::getCurrentPlatformString(); + return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]); + } + + public function getDownloadConfig(string $type): mixed + { + return $this->config[$type] ?? null; + } + + /** + * Get source extraction directory. + * + * Rules: + * 1. If extract is not specified: SOURCE_PATH/{artifact_name} + * 2. If extract is relative path: SOURCE_PATH/{value} + * 3. If extract is absolute path: {value} + * 4. If extract is array (dict): handled by extractor (selective extraction) + */ + public function getSourceDir(): string + { + // defined in config + $extract = $this->config['source']['extract'] ?? null; + + if ($extract === null) { + return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); + } + + // Array (dict) mode - return default path, actual handling is in extractor + if (is_array($extract)) { + return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); + } + + // String path + $path = $this->replaceExtractPathVariables($extract); + + // Absolute path + if (!FileSystem::isRelativePath($path)) { + return FileSystem::convertPath($path); + } + + // Relative path: based on SOURCE_PATH + return FileSystem::convertPath(SOURCE_PATH . '/' . $path); + } + + /** + * Get binary extraction directory and mode. + * + * Rules: + * 1. If extract is not specified: PKG_ROOT_PATH (standard mode) + * 2. If extract is "hosted": BUILD_ROOT_PATH (standard mode, for pre-built libraries) + * 3. If extract is relative path: PKG_ROOT_PATH/{value} (standard mode) + * 4. If extract is absolute path: {value} (standard mode) + * 5. If extract is array (dict): selective extraction mode + * + * @return array{path: ?string, mode: 'merge'|'selective'|'standard', files?: array} + */ + public function getBinaryExtractConfig(array $cache_info = []): array + { + if (is_string($cache_info['extract'] ?? null)) { + return ['path' => $this->replaceExtractPathVariables($cache_info['extract']), 'mode' => 'standard']; + } + + $platform = SystemTarget::getCurrentPlatformString(); + $binary_config = $this->config['binary'][$platform] ?? null; + + if ($binary_config === null) { + return ['path' => PKG_ROOT_PATH, 'mode' => 'standard']; + } + + $extract = $binary_config['extract'] ?? null; + + // Not specified: PKG_ROOT_PATH merge + if ($extract === null) { + return ['path' => PKG_ROOT_PATH, 'mode' => 'standard']; + } + + // "hosted" mode: BUILD_ROOT_PATH merge (for pre-built libraries) + if ($extract === 'hosted' || ($binary_config['type'] ?? '') === 'hosted') { + return ['path' => BUILD_ROOT_PATH, 'mode' => 'standard']; + } + + // Array (dict) mode: selective extraction + if (is_array($extract)) { + return [ + 'path' => null, + 'mode' => 'selective', + 'files' => $extract, + ]; + } + + // String path + $path = $this->replaceExtractPathVariables($extract); + + // Absolute path: standalone mode + if (!FileSystem::isRelativePath($path)) { + return ['path' => FileSystem::convertPath($path), 'mode' => 'standard']; + } + + // Relative path: PKG_ROOT_PATH/{value} standalone mode + return ['path' => FileSystem::convertPath(PKG_ROOT_PATH . '/' . $path), 'mode' => 'standard']; + } + + /** + * Get the binary extraction directory. + * For merge mode, returns the base path. + * For standalone mode, returns the specific directory. + */ + public function getBinaryDir(): string + { + $config = $this->getBinaryExtractConfig(); + return $config['path']; + } + + /** + * Set custom source fetcher callback. + */ + public function setCustomSourceCallback(callable $callback): void + { + $this->custom_source_callback = $callback; + } + + public function getCustomSourceCallback(): ?callable + { + return $this->custom_source_callback ?? null; + } + + public function getCustomBinaryCallback(): ?callable + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_callbacks[$current_platform] ?? null; + } + + public function emitCustomBinary(): void + { + $current_platform = SystemTarget::getCurrentPlatformString(); + if (!isset($this->custom_binary_callbacks[$current_platform])) { + throw new SPCInternalException("No custom binary callback defined for artifact '{$this->name}' on target OS '{$current_platform}'."); + } + $callback = $this->custom_binary_callbacks[$current_platform]; + ApplicationContext::invoke($callback, [Artifact::class => $this]); + } + + /** + * Set custom binary fetcher callback for a specific target OS. + * + * @param string $target_os Target OS platform string (e.g. linux-x86_64) + * @param callable $callback Custom binary fetcher callback + */ + public function setCustomBinaryCallback(string $target_os, callable $callback): void + { + ConfigValidator::validatePlatformString($target_os); + $this->custom_binary_callbacks[$target_os] = $callback; + } + + // ==================== Extraction Callbacks ==================== + + /** + * Set custom source extract callback. + * This callback completely takes over the source extraction process. + * + * Callback signature: function(Artifact $artifact, string $source_file, string $target_path): void + */ + public function setSourceExtractCallback(callable $callback): void + { + $this->source_extract_callback = $callback; + } + + /** + * Get the source extract callback. + */ + public function getSourceExtractCallback(): ?callable + { + return $this->source_extract_callback; + } + + /** + * Check if a custom source extract callback is set. + */ + public function hasSourceExtractCallback(): bool + { + return $this->source_extract_callback !== null; + } + + /** + * Set custom binary extract callback. + * This callback completely takes over the binary extraction process. + * + * Callback signature: function(Artifact $artifact, string $source_file, string $target_path, string $platform): void + * + * @param callable $callback The callback function + * @param string[] $platforms Platform filters (empty = all platforms) + */ + public function setBinaryExtractCallback(callable $callback, array $platforms = []): void + { + $this->binary_extract_callback = [ + 'callback' => $callback, + 'platforms' => $platforms, + ]; + } + + /** + * Get the binary extract callback for current platform. + * + * @return null|callable The callback if set and matches current platform, null otherwise + */ + public function getBinaryExtractCallback(): ?callable + { + if ($this->binary_extract_callback === null) { + return null; + } + + $platforms = $this->binary_extract_callback['platforms']; + $current_platform = SystemTarget::getCurrentPlatformString(); + + // Empty platforms array means all platforms + if (empty($platforms) || in_array($current_platform, $platforms, true)) { + return $this->binary_extract_callback['callback']; + } + + return null; + } + + /** + * Check if a custom binary extract callback is set for current platform. + */ + public function hasBinaryExtractCallback(): bool + { + return $this->getBinaryExtractCallback() !== null; + } + + /** + * Add a callback to run after source extraction completes. + * + * Callback signature: function(string $target_path): void + */ + public function addAfterSourceExtractCallback(callable $callback): void + { + $this->after_source_extract_callbacks[] = $callback; + } + + /** + * Add a callback to run after binary extraction completes. + * + * Callback signature: function(string $target_path, string $platform): void + * + * @param callable $callback The callback function + * @param string[] $platforms Platform filters (empty = all platforms) + */ + public function addAfterBinaryExtractCallback(callable $callback, array $platforms = []): void + { + $this->after_binary_extract_callbacks[] = [ + 'callback' => $callback, + 'platforms' => $platforms, + ]; + } + + /** + * Emit all after source extract callbacks. + * + * @param string $target_path The directory where source was extracted + */ + public function emitAfterSourceExtract(string $target_path): void + { + if (empty($this->after_source_extract_callbacks)) { + logger()->debug("No after-source-extract hooks registered for [{$this->name}]"); + return; + } + + logger()->debug('Executing ' . count($this->after_source_extract_callbacks) . " after-source-extract hook(s) for [{$this->name}]"); + foreach ($this->after_source_extract_callbacks as $callback) { + $callback_name = is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'); + logger()->debug(" 🪝 Running hook: {$callback_name}"); + ApplicationContext::invoke($callback, ['target_path' => $target_path, Artifact::class => $this]); + } + } + + /** + * Emit all after binary extract callbacks for the specified platform. + * + * @param null|string $target_path The directory where binary was extracted + * @param string $platform The platform string (e.g., 'linux-x86_64') + */ + public function emitAfterBinaryExtract(?string $target_path, string $platform): void + { + if (empty($this->after_binary_extract_callbacks)) { + logger()->debug("No after-binary-extract hooks registered for [{$this->name}]"); + return; + } + + $executed = 0; + foreach ($this->after_binary_extract_callbacks as $item) { + $callback_platforms = $item['platforms']; + + // Empty platforms array means all platforms + if (empty($callback_platforms) || in_array($platform, $callback_platforms, true)) { + $callback = $item['callback']; + $callback_name = is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'); + logger()->debug(" 🪝 Running hook: {$callback_name} (platform: {$platform})"); + ApplicationContext::invoke($callback, [ + 'target_path' => $target_path, + 'platform' => $platform, + Artifact::class => $this, + ]); + ++$executed; + } + } + + logger()->debug("Executed {$executed} after-binary-extract hook(s) for [{$this->name}] on platform [{$platform}]"); + } + + /** + * Replaces variables in the extract path. + * + * @param string $extract the extract path with variables + */ + private function replaceExtractPathVariables(string $extract): string + { + $replacement = [ + '{artifact_name}' => $this->name, + '{pkg_root_path}' => PKG_ROOT_PATH, + '{build_root_path}' => BUILD_ROOT_PATH, + '{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: WORKING_DIR . '/php-sdk-binary-tools', + '{working_dir}' => WORKING_DIR, + '{download_path}' => DOWNLOAD_PATH, + '{source_path}' => SOURCE_PATH, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $extract); + } +} diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php new file mode 100644 index 000000000..3302a37bc --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -0,0 +1,305 @@ + + */ + protected array $cache = []; + + /** + * @param string $cache_file Lock file position + */ + public function __construct(protected string $cache_file = DOWNLOAD_PATH . '/.cache.json') + { + if (!file_exists($this->cache_file)) { + logger()->debug("Cache file does not exist, creating new one at {$this->cache_file}"); + FileSystem::createDir(dirname($this->cache_file)); + file_put_contents($this->cache_file, json_encode([])); + } else { + $content = file_get_contents($this->cache_file); + $this->cache = json_decode($content ?: '{}', true) ?? []; + } + } + + /** + * Checks if the source of an artifact is already downloaded. + * + * @param string $artifact_name Artifact name + * @param bool $compare_hash Whether to compare hash of the downloaded source + */ + public function isSourceDownloaded(string $artifact_name, bool $compare_hash = false): bool + { + $item = $this->cache[$artifact_name] ?? null; + if ($item === null) { + return false; + } + return $this->isObjectDownloaded($this->cache[$artifact_name]['source'] ?? null, $compare_hash); + } + + /** + * Check if the binary of an artifact for target OS is already downloaded. + * + * @param string $artifact_name Artifact name + * @param string $target_os Target OS (accepts {windows|linux|macos}-{x86_64|aarch64}) + * @param bool $compare_hash Whether to compare hash of the downloaded binary + */ + public function isBinaryDownloaded(string $artifact_name, string $target_os, bool $compare_hash = false): bool + { + $item = $this->cache[$artifact_name] ?? null; + if ($item === null) { + return false; + } + return $this->isObjectDownloaded($this->cache[$artifact_name]['binary'][$target_os] ?? null, $compare_hash); + } + + /** + * Lock the downloaded artifact info into cache. + * + * @param Artifact|string $artifact Artifact instance + * @param 'binary'|'source' $lock_type Lock type ('source'|'binary') + * @param DownloadResult $download_result Download result object + * @param null|string $platform Target platform string for binary lock, null for source lock + */ + public function lock(Artifact|string $artifact, string $lock_type, DownloadResult $download_result, ?string $platform = null): void + { + $artifact_name = $artifact instanceof Artifact ? $artifact->getName() : $artifact; + if (!isset($this->cache[$artifact_name])) { + $this->cache[$artifact_name] = [ + 'source' => null, + 'binary' => [], + ]; + } + $obj = null; + if ($download_result->cache_type === 'archive') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'archive', + 'filename' => $download_result->filename, + 'extract' => $download_result->extract, + 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } elseif ($download_result->cache_type === 'file') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'file', + 'filename' => $download_result->filename, + 'extract' => $download_result->extract, + 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } elseif ($download_result->cache_type === 'git') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'git', + 'dirname' => $download_result->dirname, + 'extract' => $download_result->extract, + 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } elseif ($download_result->cache_type === 'local') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'local', + 'dirname' => $download_result->dirname, + 'extract' => $download_result->extract, + 'hash' => null, + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } + if ($obj === null) { + throw new SPCInternalException("Invalid download result for locking artifact {$artifact_name}"); + } + if ($lock_type === 'binary') { + if ($platform === null) { + throw new SPCInternalException("Invalid download result for locking binary artifact {$artifact_name}: platform cannot be null"); + } + $obj['platform'] = $platform; + } + if ($lock_type === 'source') { + $this->cache[$artifact_name]['source'] = $obj; + } elseif ($lock_type === 'binary') { + $this->cache[$artifact_name]['binary'][$platform] = $obj; + } else { + throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}"); + } + // save cache to file + file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + } + + /** + * Get source cache info for an artifact. + * + * @param string $artifact_name Artifact name + * @return null|array Cache info array or null if not found + */ + public function getSourceInfo(string $artifact_name): ?array + { + return $this->cache[$artifact_name]['source'] ?? null; + } + + /** + * Get binary cache info for an artifact on specific platform. + * + * @param string $artifact_name Artifact name + * @param string $platform Platform string (e.g., 'linux-x86_64') + * @return null|array{ + * lock_type: 'binary'|'source', + * cache_type: 'archive'|'git'|'local', + * filename?: string, + * extract: null|'&custom'|string, + * hash: null|string, + * dirname?: string, + * version?: null|string + * } Cache info array or null if not found + */ + public function getBinaryInfo(string $artifact_name, string $platform): ?array + { + return $this->cache[$artifact_name]['binary'][$platform] ?? null; + } + + /** + * Get the full path to the cached file/directory. + * + * @param array $cache_info Cache info from getSourceInfo() or getBinaryInfo() + * @return string Full path to the cached file or directory + */ + public function getCacheFullPath(array $cache_info): string + { + return match ($cache_info['cache_type']) { + 'archive', 'file' => DOWNLOAD_PATH . '/' . $cache_info['filename'], + 'git' => DOWNLOAD_PATH . '/' . $cache_info['dirname'], + 'local' => $cache_info['dirname'], // local dirname is absolute path + default => throw new SPCInternalException("Unknown cache type: {$cache_info['cache_type']}"), + }; + } + + /** + * Remove source cache entry for an artifact. + * + * @param string $artifact_name Artifact name + * @param bool $delete_file Whether to also delete the cached file/directory + */ + public function removeSource(string $artifact_name, bool $delete_file = false): void + { + $source_info = $this->getSourceInfo($artifact_name); + if ($source_info === null) { + return; + } + + // Optionally delete the actual file/directory + if ($delete_file) { + $path = $this->getCacheFullPath($source_info); + if (in_array($source_info['cache_type'], ['archive', 'file']) && file_exists($path)) { + unlink($path); + logger()->debug("Deleted cached archive: {$path}"); + } elseif ($source_info['cache_type'] === 'git' && is_dir($path)) { + FileSystem::removeDir($path); + logger()->debug("Deleted cached git repository: {$path}"); + } + } + + // Remove from cache + $this->cache[$artifact_name]['source'] = null; + $this->save(); + logger()->debug("Removed source cache entry for [{$artifact_name}]"); + } + + /** + * Remove binary cache entry for an artifact on specific platform. + * + * @param string $artifact_name Artifact name + * @param string $platform Platform string (e.g., 'linux-x86_64') + * @param bool $delete_file Whether to also delete the cached file/directory + */ + public function removeBinary(string $artifact_name, string $platform, bool $delete_file = false): void + { + $binary_info = $this->getBinaryInfo($artifact_name, $platform); + if ($binary_info === null) { + return; + } + + // Optionally delete the actual file/directory + if ($delete_file) { + $path = $this->getCacheFullPath($binary_info); + if (in_array($binary_info['cache_type'], ['archive', 'file']) && file_exists($path)) { + unlink($path); + logger()->debug("Deleted cached binary archive: {$path}"); + } elseif ($binary_info['cache_type'] === 'git' && is_dir($path)) { + FileSystem::removeDir($path); + logger()->debug("Deleted cached binary git repository: {$path}"); + } + } + + // Remove from cache + unset($this->cache[$artifact_name]['binary'][$platform]); + $this->save(); + logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]"); + } + + /** + * Save cache to file. + */ + public function save(): void + { + file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + } + + private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool + { + if ($object === null) { + return false; + } + // check if source is cached and file/dir exists in downloads/ dir + return match ($object['cache_type'] ?? null) { + 'archive', 'file' => isset($object['filename']) && + file_exists(DOWNLOAD_PATH . '/' . $object['filename']) && + (!$compare_hash || ( + isset($object['hash']) && + sha1_file(DOWNLOAD_PATH . '/' . $object['filename']) === $object['hash'] + )), + 'git' => isset($object['dirname']) && + is_dir(DOWNLOAD_PATH . '/' . $object['dirname'] . '/.git') && + (!$compare_hash || ( + isset($object['hash']) && + trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $object['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')) === $object['hash'] + )), + 'local' => isset($object['dirname']) && + is_dir($object['dirname']), // local dirname is absolute path + default => false, + }; + } +} diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php new file mode 100644 index 000000000..b28c11dc0 --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -0,0 +1,665 @@ + Artifact objects */ + protected array $artifacts = []; + + /** @var int Parallel process number (1 and 0 as single-threaded mode) */ + protected int $parallel = 1; + + protected int $retry = 0; + + /** @var array Override custom download urls from options */ + protected array $custom_urls = []; + + /** @var array Override custom git options from options ([branch, git url]) */ + protected array $custom_gits = []; + + /** @var array Override custom local paths from options */ + protected array $custom_locals = []; + + /** @var int Fetch type preference */ + protected int $default_fetch_pref = Artifact::FETCH_PREFER_SOURCE; + + /** @var array Specific fetch preference */ + protected array $fetch_prefs = []; + + /** @var array|bool Whether to ignore cache for specific artifacts or all */ + protected array|bool $ignore_cache = false; + + /** @var bool Whether to enable alternative mirror downloads */ + protected bool $alt = true; + + private array $_before_files; + + /** + * @param array{ + * parallel?: int, + * retry?: int, + * custom-url?: array, + * custom-git?: array, + * custom-local?: array, + * prefer-source?: null|bool|string, + * prefer-pre-built?: null|bool|string, + * prefer-binary?: null|bool|string, + * source-only?: null|bool|string, + * binary-only?: null|bool|string, + * ignore-cache?: null|bool|string, + * ignore-cache-sources?: null|bool|string, + * no-alt?: bool, + * no-shallow-clone?: bool + * } $options Downloader options + */ + public function __construct(protected array $options = []) + { + // Allow setting concurrency via options + $this->parallel = max(1, (int) ($options['parallel'] ?? 1)); + // Allow setting retry via options + $this->retry = max(0, (int) ($options['retry'] ?? 0)); + // Prefer source (default) + if (array_key_exists('prefer-source', $options)) { + if (is_string($options['prefer-source'])) { + $ls = parse_comma_list($options['prefer-source']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE; + } + } elseif ($options['prefer-source'] === null) { + $this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE; + } + } + // Prefer binary (originally prefer-pre-built) + if (array_key_exists('prefer-binary', $options)) { + if (is_string($options['prefer-binary'])) { + $ls = parse_comma_list($options['prefer-binary']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY; + } + } elseif ($options['prefer-binary'] === null) { + $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY; + } + } + if (array_key_exists('prefer-pre-built', $options)) { + if (is_string($options['prefer-pre-built'])) { + $ls = parse_comma_list($options['prefer-pre-built']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY; + } + } elseif ($options['prefer-pre-built'] === null) { + $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY; + } + } + // Source only + if (array_key_exists('source-only', $options)) { + if (is_string($options['source-only'])) { + $ls = parse_comma_list($options['source-only']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE; + } + } elseif ($options['source-only'] === null) { + $this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE; + } + } + // Binary only + if (array_key_exists('binary-only', $options)) { + if (is_string($options['binary-only'])) { + $ls = parse_comma_list($options['binary-only']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY; + } + } elseif ($options['binary-only'] === null) { + $this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY; + } + } + // Ignore cache + if (array_key_exists('ignore-cache', $options)) { + if (is_string($options['ignore-cache'])) { + $this->ignore_cache = parse_comma_list($options['ignore-cache']); + } elseif ($options['ignore-cache'] === null) { + $this->ignore_cache = true; + } + } + // backward compatibility for ignore-cache-sources + if (array_key_exists('ignore-cache-sources', $options)) { + if (is_string($options['ignore-cache-sources'])) { + $this->ignore_cache = parse_comma_list($options['ignore-cache-sources']); + } elseif ($options['ignore-cache-sources'] === null) { + $this->ignore_cache = true; + } + } + // Allow setting custom urls via options + foreach (($options['custom-url'] ?? []) as $value) { + [$artifact_name, $url] = explode(':', $value, 2); + $this->custom_urls[$artifact_name] = $url; + $this->ignore_cache = match ($this->ignore_cache) { + true => true, + false => [$artifact_name], + default => array_merge($this->ignore_cache, [$artifact_name]), + }; + } + // Allow setting custom git options via options + foreach (($options['custom-git'] ?? []) as $value) { + [$artifact_name, $branch, $git_url] = explode(':', $value, 3) + [null, null, null]; + $this->custom_gits[$artifact_name] = [$branch ?? 'main', $git_url]; + $this->ignore_cache = match ($this->ignore_cache) { + true => true, + false => [$artifact_name], + default => array_merge($this->ignore_cache, [$artifact_name]), + }; + } + // Allow setting custom local paths via options + foreach (($options['custom-local'] ?? []) as $value) { + [$artifact_name, $local_path] = explode(':', $value, 2); + $this->custom_locals[$artifact_name] = $local_path; + $this->ignore_cache = match ($this->ignore_cache) { + true => true, + false => [$artifact_name], + default => array_merge($this->ignore_cache, [$artifact_name]), + }; + } + // no alt + if (array_key_exists('no-alt', $options) && $options['no-alt'] === true) { + $this->alt = false; + } + + // read downloads dir + $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + } + + /** + * Add an artifact to the download list. + * + * @param Artifact|string $artifact Artifact instance or artifact name + */ + public function add(Artifact|string $artifact): static + { + if (is_string($artifact)) { + $artifact_instance = ArtifactLoader::getArtifactInstance($artifact); + } else { + $artifact_instance = $artifact; + } + if ($artifact_instance === null) { + $name = $artifact; + throw new WrongUsageException("Artifact '{$name}' not found, please check the name."); + } + // only add if not already added + if (!isset($this->artifacts[$artifact_instance->getName()])) { + $this->artifacts[$artifact_instance->getName()] = $artifact_instance; + } + return $this; + } + + /** + * Add multiple artifacts to the download list. + * + * @param array $artifacts Multiple artifacts to add + */ + public function addArtifacts(array $artifacts): static + { + foreach ($artifacts as $artifact) { + $this->add($artifact); + } + return $this; + } + + /** + * Set the concurrency limit for parallel downloads. + * + * @param int $parallel Number of concurrent downloads (default: 3) + */ + public function setParallel(int $parallel): static + { + $this->parallel = max(1, $parallel); + return $this; + } + + /** + * Download all artifacts, with optional parallel processing. + * + * @param bool $interactive Enable interactive mode with Ctrl+C handling + */ + public function download(bool $interactive = true): void + { + if ($interactive) { + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + keyboard_interrupt_register(function () { + echo PHP_EOL; + InteractiveTerm::error('Download cancelled by user.'); + // scan changed files + $after_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + $new_files = array_diff($after_files, $this->_before_files); + + // remove new files + foreach ($new_files as $file) { + if ($file === '.cache.json') { + continue; + } + logger()->debug("Removing corrupted artifact: {$file}"); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $file; + if (is_dir($path)) { + FileSystem::removeDir($path); + } elseif (is_file($path)) { + FileSystem::removeFileIfExists($path); + } + } + exit(2); + }); + } + + $this->applyCustomDownloads(); + + $count = count($this->artifacts); + $artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts)); + // mute the first line if not interactive + if ($interactive) { + InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ..."); + } + try { + // Create dir + if (!is_dir(DOWNLOAD_PATH)) { + FileSystem::createDir(DOWNLOAD_PATH); + } + logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ..."); + // Download artifacts parallely + if ($this->parallel > 1) { + $this->downloadWithConcurrency(); + } else { + // normal sequential download + $current = 0; + $skipped = []; + foreach ($this->artifacts as $artifact) { + ++$current; + if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { + $skipped[] = $artifact->getName(); + continue; + } + $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + } + if ($interactive) { + $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; + InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}", true); + echo PHP_EOL; + } + } + } catch (SPCException $e) { + array_map(fn ($x) => InteractiveTerm::error($x), explode("\n", $e->getMessage())); + throw new WrongUsageException(); + } finally { + if ($interactive) { + Shell::passthruCallback(null); + keyboard_interrupt_unregister(); + } + } + } + + public function getRetry(): int + { + return $this->retry; + } + + public function getArtifacts(): array + { + return $this->artifacts; + } + + public function getOption(string $name, mixed $default = null): mixed + { + return $this->options[$name] ?? $default; + } + + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int + { + $queue = $this->generateQueue($artifact); + // already downloaded + if ($queue === []) { + logger()->debug("Artifact '{$artifact->getName()}' is already downloaded, skipping."); + return SPC_DOWNLOAD_STATUS_SKIPPED; + } + + $try = false; + foreach ($queue as $item) { + try { + $instance = null; + $call = match ($item['config']['type']) { + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar', 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + default => null, + }; + $type_display_name = match (true) { + $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader', + $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader', + default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'], + }; + $try_h = $try ? 'Try downloading' : 'Downloading'; + logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); + if ($parallel === false) { + InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); + } + // is valid download type + if ($item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null) { + $lock = ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + ArtifactDownloader::class => $this, + ]); + } elseif ($item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null) { + $lock = ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + ArtifactDownloader::class => $this, + ]); + } elseif (is_a($call, DownloadTypeInterface::class, true)) { + $instance = new $call(); + $lock = $instance->download($artifact->getName(), $item['config'], $this); + } else { + throw new ValidationException("Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}."); + } + if (!$lock instanceof DownloadResult) { + throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult."); + } + // verifying hash if possible + $hash_validator = $instance ?? null; + $verified = $lock->verified; + if ($hash_validator instanceof ValidatorInterface) { + if (!$hash_validator->validate($artifact->getName(), $item['config'], $this, $lock)) { + throw new ValidationException("Hash validation failed for artifact '{$artifact->getName()}' {$item['display']}."); + } + $verified = true; + } + // process lock + ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); + if ($parallel === false) { + $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : ''; + InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} ."); + } + return SPC_DOWNLOAD_STATUS_SUCCESS; + } catch (DownloaderException|ExecutionException $e) { + if ($parallel === false) { + InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); + InteractiveTerm::error("Failed message: {$e->getMessage()}", true); + } + $try = true; + continue; + } catch (ValidationException $e) { + if ($parallel === false) { + InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); + InteractiveTerm::error("Validation failed: {$e->getMessage()}"); + } + break; + } + } + $vvv = ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, and disable parallel downloading for more details." : ''; + throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}"); + } + + private function downloadWithConcurrency(): void + { + $skipped = []; + $fiber_pool = []; + $old_verbosity = null; + $old_debug = null; + try { + $count = count($this->artifacts); + // must mute + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + $old_verbosity = $output->getVerbosity(); + $old_debug = ApplicationContext::isDebug(); + logger()->warning('Parallel download is not supported in verbose mode, I will mute the output temporarily.'); + $output->setVerbosity(OutputInterface::VERBOSITY_NORMAL); + ApplicationContext::setDebug(false); + logger()->setLevel(LogLevel::ERROR); + } + $pool_count = $this->parallel; + $downloaded = 0; + $total = count($this->artifacts); + + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + \Fiber::suspend(); + }); + + InteractiveTerm::indicateProgress("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ..."); + $failed_downloads = []; + while (true) { + // fill pool + while (count($fiber_pool) < $pool_count && ($artifact = array_shift($this->artifacts)) !== null) { + $current = $count - count($this->artifacts); + $fiber = new \Fiber(function () use ($artifact, $current, $count) { + return [$artifact, $this->downloadWithType($artifact, $current, $count, true)]; + }); + $fiber->start(); + $fiber_pool[] = $fiber; + } + // check pool + foreach ($fiber_pool as $index => $fiber) { + if ($fiber->isTerminated()) { + try { + [$artifact, $int] = $fiber->getReturn(); + if ($int === SPC_DOWNLOAD_STATUS_SKIPPED) { + $skipped[] = $artifact->getName(); + } + } catch (\Throwable $e) { + $artifact_name = 'unknown'; + if (isset($artifact)) { + $artifact_name = $artifact->getName(); + } + $failed_downloads[] = ['artifact' => $artifact_name, 'error' => $e]; + InteractiveTerm::setMessage("[{$downloaded}/{$total}] Download failed: {$artifact_name}"); + InteractiveTerm::advance(); + } + // remove from pool + unset($fiber_pool[$index]); + ++$downloaded; + InteractiveTerm::setMessage("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ..."); + InteractiveTerm::advance(); + } else { + $fiber->resume(); + } + } + // all done + if (count($this->artifacts) === 0 && count($fiber_pool) === 0) { + if (!empty($failed_downloads)) { + InteractiveTerm::finish('Download completed with ' . count($failed_downloads) . ' failure(s).', false); + foreach ($failed_downloads as $failure) { + InteractiveTerm::error("Failed to download '{$failure['artifact']}': {$failure['error']->getMessage()}"); + } + throw new DownloaderException('Failed to download ' . count($failed_downloads) . ' artifact(s). Please check your internet connection and try again.'); + } + $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; + InteractiveTerm::finish("Downloaded all {$total} artifacts.{$skip_msg}"); + break; + } + } + } catch (\Throwable $e) { + // throw to all fibers to make them stop + foreach ($fiber_pool as $fiber) { + if (!$fiber->isTerminated()) { + try { + $fiber->throw($e); + } catch (\Throwable) { + // ignore errors when stopping fibers + } + } + } + InteractiveTerm::finish('Parallel download failed !', false); + throw $e; + } finally { + if ($old_verbosity !== null) { + ApplicationContext::get(OutputInterface::class)->setVerbosity($old_verbosity); + logger()->setLevel(match ($old_verbosity) { + OutputInterface::VERBOSITY_VERBOSE => LogLevel::INFO, + OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => LogLevel::DEBUG, + default => LogLevel::WARNING, + }); + } + if ($old_debug !== null) { + ApplicationContext::setDebug($old_debug); + } + Shell::passthruCallback(null); + } + } + + /** + * Generate download queue based on type preference. + */ + private function generateQueue(Artifact $artifact): array + { + /** @var array $queue */ + $queue = []; + $binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true); + $source_downloaded = $artifact->isSourceDownloaded(compare_hash: true); + + $item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')]; + $item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')]; + + // For binary config, handle both array configs and custom callbacks + $binary_config = $artifact->getDownloadConfig('binary'); + $has_custom_binary = $artifact->getCustomBinaryCallback() !== null; + $item_binary_config = null; + if (is_array($binary_config)) { + $item_binary_config = $binary_config[SystemTarget::getCurrentPlatformString()] ?? null; + } elseif ($has_custom_binary) { + // For custom binaries, create a dummy config to allow queue generation + $item_binary_config = ['type' => 'custom']; + } + $item_binary = ['display' => 'binary', 'lock' => 'binary', 'config' => $item_binary_config]; + + $binary_mirror_config = $artifact->getDownloadConfig('binary-mirror'); + $item_binary_mirror_config = null; + if (is_array($binary_mirror_config)) { + $item_binary_mirror_config = $binary_mirror_config[SystemTarget::getCurrentPlatformString()] ?? null; + } + $item_binary_mirror = ['display' => 'binary (mirror)', 'lock' => 'binary', 'config' => $item_binary_mirror_config]; + + $pref = $this->fetch_prefs[$artifact->getName()] ?? $this->default_fetch_pref; + if ($pref === Artifact::FETCH_PREFER_SOURCE) { + $queue[] = $item_source['config'] !== null ? $item_source : null; + $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null; + $queue[] = $item_binary['config'] !== null ? $item_binary : null; + $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null; + } elseif ($pref === Artifact::FETCH_PREFER_BINARY) { + $queue[] = $item_binary['config'] !== null ? $item_binary : null; + $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null; + $queue[] = $item_source['config'] !== null ? $item_source : null; + $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null; + } elseif ($pref === Artifact::FETCH_ONLY_SOURCE) { + $queue[] = $item_source['config'] !== null ? $item_source : null; + $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null; + } elseif ($pref === Artifact::FETCH_ONLY_BINARY) { + $queue[] = $item_binary['config'] !== null ? $item_binary : null; + $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null; + } + // filter nulls + $queue = array_values(array_filter($queue)); + + // always download + if ($this->ignore_cache === true || is_array($this->ignore_cache) && in_array($artifact->getName(), $this->ignore_cache)) { + // validate: ensure at least one download source is available + if (empty($queue)) { + throw new ValidationException("Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').'); + } + return $queue; + } + + // check if already downloaded + $has_usable_download = false; + if ($pref === Artifact::FETCH_PREFER_SOURCE) { + // prefer source: check source first, if not available check binary + $has_usable_download = $source_downloaded || $binary_downloaded; + } elseif ($pref === Artifact::FETCH_PREFER_BINARY) { + // prefer binary: check binary first, if not available check source + $has_usable_download = $binary_downloaded || $source_downloaded; + } elseif ($pref === Artifact::FETCH_ONLY_SOURCE) { + // source-only: only check if source is downloaded + $has_usable_download = $source_downloaded; + } elseif ($pref === Artifact::FETCH_ONLY_BINARY) { + // binary-only: only check if binary for current platform is downloaded + $has_usable_download = $binary_downloaded; + } + + // if already downloaded, skip + if ($has_usable_download) { + return []; + } + + // validate: ensure at least one download source is available + if (empty($queue)) { + if ($pref === Artifact::FETCH_ONLY_SOURCE) { + throw new ValidationException("Artifact '{$artifact->getName()}' does not provide source download, cannot use --source-only mode."); + } + if ($pref === Artifact::FETCH_ONLY_BINARY) { + throw new ValidationException("Artifact '{$artifact->getName()}' does not provide binary download for current platform (" . SystemTarget::getCurrentPlatformString() . '), cannot use --binary-only mode.'); + } + // prefer modes should also throw error if no download source available + throw new ValidationException("Validation failed: Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').'); + } + + return $queue; + } + + private function applyCustomDownloads(): void + { + foreach ($this->custom_urls as $artifact_name => $custom_url) { + if (isset($this->artifacts[$artifact_name])) { + $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) { + return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader); + }); + } + } + foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) { + if (isset($this->artifacts[$artifact_name])) { + $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) { + return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader); + }); + } + } + foreach ($this->custom_locals as $artifact_name => $local_path) { + if (isset($this->artifacts[$artifact_name])) { + $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) { + return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader); + }); + } + } + } +} diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php new file mode 100644 index 000000000..fd3204e8f --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -0,0 +1,619 @@ + Track extracted artifacts to avoid duplicate extraction */ + protected array $extracted = []; + + public function __construct( + protected ArtifactCache $cache, + protected bool $interactive = true + ) {} + + /** + * Extract all artifacts for a list of packages. + * + * @param array $packages Packages to extract artifacts for + * @param bool $force_source If true, always extract source (ignore binary) + */ + public function extractForPackages(array $packages, bool $force_source = false): void + { + // Collect all unique artifacts + $artifacts = []; + foreach ($packages as $package) { + $artifact = $package->getArtifact(); + if ($artifact !== null && !isset($artifacts[$artifact->getName()])) { + $artifacts[$artifact->getName()] = $artifact; + } + } + + // Sort: php-src should be extracted first (extensions depend on it) + uksort($artifacts, function (string $a, string $b): int { + if ($a === 'php-src') { + return -1; + } + if ($b === 'php-src') { + return 1; + } + return 0; + }); + + // Extract each artifact + foreach ($artifacts as $artifact) { + $this->extract($artifact, $force_source); + } + } + + /** + * Extract a single artifact. + * + * @param Artifact $artifact The artifact to extract + * @param bool $force_source If true, always extract source (ignore binary) + */ + public function extract(Artifact $artifact, bool $force_source = false): int + { + $name = $artifact->getName(); + + // Already extracted in this session + if (isset($this->extracted[$name])) { + logger()->debug("Artifact [{$name}] already extracted in this session, skip."); + return SPC_STATUS_ALREADY_EXTRACTED; + } + + // Determine: use binary or source? + $use_binary = !$force_source && $artifact->shouldUseBinary(); + + if ($this->interactive) { + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + } + + try { + V2CompatLayer::beforeExtractHook($artifact); + if ($use_binary) { + $status = $this->extractBinary($artifact); + } else { + $status = $this->extractSource($artifact); + } + V2CompatLayer::afterExtractHook($artifact); + } finally { + if ($this->interactive) { + Shell::passthruCallback(null); + } + } + + $this->extracted[$name] = true; + return $status; + } + + /** + * Extract source artifact. + */ + protected function extractSource(Artifact $artifact): int + { + $name = $artifact->getName(); + $cache_info = $this->cache->getSourceInfo($name); + + if ($cache_info === null) { + throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!"); + } + + $source_file = $this->cache->getCacheFullPath($cache_info); + $target_path = $artifact->getSourceDir(); + + // Check for custom extract callback + if ($artifact->hasSourceExtractCallback()) { + logger()->info("Extracting source [{$name}] using custom callback..."); + $callback = $artifact->getSourceExtractCallback(); + ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + 'source_file' => $source_file, + 'target_path' => $target_path, + ]); + // Emit after hooks + $artifact->emitAfterSourceExtract($target_path); + logger()->debug("Emitted after-source-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + // Check for selective extraction (dict mode) + $extract_config = $artifact->getDownloadConfig('source')['extract'] ?? null; + if (is_array($extract_config)) { + $this->doSelectiveExtract($name, $cache_info, $extract_config); + $artifact->emitAfterSourceExtract($target_path); + logger()->debug("Emitted after-source-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + // Standard extraction + $hash = $cache_info['hash'] ?? null; + + if ($this->isAlreadyExtracted($target_path, $hash)) { + logger()->debug("Source [{$name}] already extracted at {$target_path}, skip."); + return SPC_STATUS_ALREADY_EXTRACTED; + } + + // Remove old directory if hash mismatch + if (is_dir($target_path)) { + logger()->notice("Source [{$name}] hash mismatch, re-extracting..."); + FileSystem::removeDir($target_path); + } + + logger()->info("Extracting source [{$name}] to {$target_path}..."); + $this->doStandardExtract($name, $cache_info, $target_path); + + // Emit after hooks + $artifact->emitAfterSourceExtract($target_path); + logger()->debug("Emitted after-source-extract hooks for [{$name}]"); + + // Write hash marker + if ($hash !== null) { + FileSystem::writeFile("{$target_path}/.spc-hash", $hash); + } + return SPC_STATUS_EXTRACTED; + } + + /** + * Extract binary artifact. + */ + protected function extractBinary(Artifact $artifact): int + { + $name = $artifact->getName(); + $platform = SystemTarget::getCurrentPlatformString(); + $cache_info = $this->cache->getBinaryInfo($name, $platform); + + if ($cache_info === null) { + throw new WrongUsageException("Artifact binary [{$name}] for platform [{$platform}] not downloaded!"); + } + + $source_file = $this->cache->getCacheFullPath($cache_info); + $extract_config = $artifact->getBinaryExtractConfig($cache_info); + $target_path = $extract_config['path']; + + // Check for custom extract callback + if ($artifact->hasBinaryExtractCallback()) { + logger()->info("Extracting binary [{$name}] using custom callback..."); + $callback = $artifact->getBinaryExtractCallback(); + ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + 'source_file' => $source_file, + 'target_path' => $target_path, + 'platform' => $platform, + ]); + // Emit after hooks + $artifact->emitAfterBinaryExtract($target_path, $platform); + logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + // Handle different extraction modes + $mode = $extract_config['mode']; + + if ($mode === 'selective') { + $this->doSelectiveExtract($name, $cache_info, $extract_config['files']); + $artifact->emitAfterBinaryExtract($target_path, $platform); + logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + $hash = $cache_info['hash'] ?? null; + + if ($this->isAlreadyExtracted($target_path, $hash)) { + logger()->debug("Binary [{$name}] already extracted at {$target_path}, skip."); + return SPC_STATUS_ALREADY_EXTRACTED; + } + + logger()->info("Extracting binary [{$name}] to {$target_path}..."); + $this->doStandardExtract($name, $cache_info, $target_path); + + $artifact->emitAfterBinaryExtract($target_path, $platform); + logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + + if ($hash !== null && $cache_info['cache_type'] !== 'file') { + FileSystem::writeFile("{$target_path}/.spc-hash", $hash); + } + return SPC_STATUS_EXTRACTED; + } + + /** + * Standard extraction: extract entire archive to target directory. + */ + protected function doStandardExtract(string $name, array $cache_info, string $target_path): void + { + $source_file = $this->cache->getCacheFullPath($cache_info); + $cache_type = $cache_info['cache_type']; + + // Validate source file exists before extraction + $this->validateSourceFile($name, $source_file, $cache_type); + + $this->extractWithType($cache_type, $source_file, $target_path); + } + + /** + * Selective extraction: extract specific files to specific locations. + * + * @param string $name Artifact name + * @param array $cache_info Cache info + * @param array $file_map Map of source path => destination path + */ + protected function doSelectiveExtract(string $name, array $cache_info, array $file_map): void + { + // Extract to temp directory first + $temp_path = sys_get_temp_dir() . '/spc_extract_' . $name . '_' . bin2hex(random_bytes(8)); + + try { + logger()->info("Extracting [{$name}] with selective file mapping..."); + + $source_file = $this->cache->getCacheFullPath($cache_info); + $cache_type = $cache_info['cache_type']; + + // Validate source file exists before extraction + $this->validateSourceFile($name, $source_file, $cache_type); + + $this->extractWithType($cache_type, $source_file, $temp_path); + + // Process file mappings + foreach ($file_map as $src_pattern => $dst_path) { + $dst_path = $this->replacePathVariables($dst_path); + $src_full = "{$temp_path}/{$src_pattern}"; + + // Handle glob patterns + if (str_contains($src_pattern, '*')) { + $matches = glob($src_full); + if (empty($matches)) { + logger()->warning("No files matched pattern [{$src_pattern}] in [{$name}]"); + continue; + } + foreach ($matches as $match) { + $filename = basename($match); + $target = rtrim($dst_path, '/') . '/' . $filename; + $this->copyFileOrDir($match, $target); + } + } else { + // Direct file/directory copy + if (!file_exists($src_full) && !is_dir($src_full)) { + logger()->warning("Source [{$src_pattern}] not found in [{$name}]"); + continue; + } + $this->copyFileOrDir($src_full, $dst_path); + } + } + } finally { + // Cleanup temp directory + if (is_dir($temp_path)) { + FileSystem::removeDir($temp_path); + } + } + } + + /** + * Check if artifact is already extracted with correct hash. + */ + protected function isAlreadyExtracted(string $path, ?string $expected_hash): bool + { + if (!is_dir($path)) { + return false; + } + + // Local source: always re-extract + if ($expected_hash === null) { + return false; + } + + $hash_file = "{$path}/.spc-hash"; + if (!file_exists($hash_file)) { + return false; + } + + return FileSystem::readFile($hash_file) === $expected_hash; + } + + /** + * Validate that the source file/directory exists before extraction. + * + * @param string $name Artifact name (for error messages) + * @param string $source_file Path to the source file or directory + * @param string $cache_type Cache type: archive, git, local + * + * @throws WrongUsageException if source file does not exist + */ + protected function validateSourceFile(string $name, string $source_file, string $cache_type): void + { + $converted_path = FileSystem::convertPath($source_file); + + switch ($cache_type) { + case 'archive': + if (!file_exists($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source archive not found at: {$converted_path}\n" . + "The file may have been deleted or moved. Please run 'spc download {$name}' to re-download it." + ); + } + if (!is_file($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source path exists but is not a file: {$converted_path}\n" . + 'Expected an archive file. Please check your downloads directory.' + ); + } + break; + case 'file': + if (!file_exists($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source file not found at: {$converted_path}\n" . + "The file may have been deleted or moved. Please run 'spc download {$name}' to re-download it." + ); + } + if (!is_file($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source path exists but is not a file: {$converted_path}\n" . + 'Expected a regular file. Please check your downloads directory.' + ); + } + break; + case 'git': + if (!is_dir($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] git repository not found at: {$converted_path}\n" . + "The directory may have been deleted. Please run 'spc download {$name}' to re-clone it." + ); + } + // Optionally check for .git directory to ensure it's a valid git repo + if (!is_dir("{$converted_path}/.git")) { + logger()->warning("Artifact [{$name}] directory exists but may not be a valid git repository (missing .git)"); + } + break; + case 'local': + if (!file_exists($converted_path) && !is_dir($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] local source not found at: {$converted_path}\n" . + 'Please ensure the local path is correct and accessible.' + ); + } + break; + default: + throw new SPCInternalException("Unknown cache type: {$cache_type}"); + } + + logger()->debug("Validated source file for [{$name}]: {$converted_path} (type: {$cache_type})"); + } + + /** + * Copy file or directory to destination. + */ + protected function copyFileOrDir(string $src, string $dst): void + { + $dst_dir = dirname($dst); + if (!is_dir($dst_dir)) { + FileSystem::createDir($dst_dir); + } + + if (is_dir($src)) { + FileSystem::copyDir($src, $dst); + } else { + copy($src, $dst); + } + + logger()->debug("Copied {$src} -> {$dst}"); + } + + /** + * Extract source based on cache type. + * + * @param string $cache_type Cache type: archive, git, local + * @param string $source_file Path to source file or directory + * @param string $target_path Target extraction path + */ + protected function extractWithType(string $cache_type, string $source_file, string $target_path): void + { + match ($cache_type) { + 'archive' => $this->extractArchive($source_file, $target_path), + 'file' => $this->copyFile($source_file, $target_path), + 'git' => FileSystem::copyDir(FileSystem::convertPath($source_file), $target_path), + 'local' => symlink(FileSystem::convertPath($source_file), $target_path), + default => throw new SPCInternalException("Unknown cache type: {$cache_type}"), + }; + } + + /** + * Extract archive file to target directory. + * + * Supports: tar, tar.gz, tgz, tar.bz2, tar.xz, txz, zip, exe + */ + 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}"); + } + } + + /** + * Unzip file with stripping top-level directory. + */ + protected function unzipWithStrip(string $zip_file, string $extract_path): void + { + $temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); + $zip_file = FileSystem::convertPath($zip_file); + $extract_path = FileSystem::convertPath($extract_path); + + // Extract to temp dir + FileSystem::createDir($temp_dir); + + if (PHP_OS_FAMILY === 'Windows') { + default_shell()->execute7zExtract($zip_file, $temp_dir); + } else { + default_shell()->executeUnzip($zip_file, $temp_dir); + } + + // Scan first level dirs (relative, not recursive, include dirs) + $contents = FileSystem::scanDirFiles($temp_dir, false, true, true); + if ($contents === false) { + throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); + } + + // If extract path already exists, remove it + if (is_dir($extract_path)) { + FileSystem::removeDir($extract_path); + } + + // If only one dir, move its contents to extract_path + $subdir = FileSystem::convertPath("{$temp_dir}/{$contents[0]}"); + if (count($contents) === 1 && is_dir($subdir)) { + $this->moveFileOrDir($subdir, $extract_path); + } else { + // Else, if it contains only one dir, strip dir and copy other files + $dircount = 0; + $dir = []; + $top_files = []; + foreach ($contents as $item) { + if (is_dir(FileSystem::convertPath("{$temp_dir}/{$item}"))) { + ++$dircount; + $dir[] = $item; + } else { + $top_files[] = $item; + } + } + + // Extract dir contents to extract_path + FileSystem::createDir($extract_path); + + // Extract move dir + if ($dircount === 1) { + $sub_contents = FileSystem::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true); + if ($sub_contents === false) { + throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); + } + foreach ($sub_contents as $sub_item) { + $this->moveFileOrDir( + FileSystem::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), + FileSystem::convertPath("{$extract_path}/{$sub_item}") + ); + } + } else { + foreach ($dir as $item) { + $this->moveFileOrDir( + FileSystem::convertPath("{$temp_dir}/{$item}"), + FileSystem::convertPath("{$extract_path}/{$item}") + ); + } + } + + // Move top-level files to extract_path + foreach ($top_files as $top_file) { + $this->moveFileOrDir( + FileSystem::convertPath("{$temp_dir}/{$top_file}"), + FileSystem::convertPath("{$extract_path}/{$top_file}") + ); + } + } + + // Clean up temp directory + FileSystem::removeDir($temp_dir); + } + + /** + * Move file or directory to destination. + */ + protected function moveFileOrDir(string $source, string $dest): void + { + $source = FileSystem::convertPath($source); + $dest = FileSystem::convertPath($dest); + + // Try rename first (fast, atomic) + if (@rename($source, $dest)) { + return; + } + + if (is_dir($source)) { + FileSystem::copyDir($source, $dest); + FileSystem::removeDir($source); + } else { + if (!copy($source, $dest)) { + throw new FileSystemException("Failed to copy file from {$source} to {$dest}"); + } + if (!unlink($source)) { + throw new FileSystemException("Failed to remove source file: {$source}"); + } + } + } + + /** + * Replace path variables. + */ + protected function replacePathVariables(string $path): string + { + $replacement = [ + '{pkg_root_path}' => PKG_ROOT_PATH, + '{build_root_path}' => BUILD_ROOT_PATH, + '{source_path}' => SOURCE_PATH, + '{download_path}' => DOWNLOAD_PATH, + '{working_dir}' => WORKING_DIR, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $path); + } + + private function copyFile(string $source_file, string $target_path): void + { + FileSystem::createDir(dirname($target_path)); + FileSystem::copy(FileSystem::convertPath($source_file), $target_path); + } +} diff --git a/src/StaticPHP/Artifact/ArtifactLoader.php b/src/StaticPHP/Artifact/ArtifactLoader.php new file mode 100644 index 000000000..6a839cb4b --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactLoader.php @@ -0,0 +1,190 @@ + Artifact instances */ + private static ?array $artifacts = null; + + public static function initArtifactInstances(): void + { + if (self::$artifacts !== null) { + return; + } + foreach (ArtifactConfig::getAll() as $name => $item) { + $artifact = new Artifact($name, $item); + self::$artifacts[$name] = $artifact; + } + } + + public static function getArtifactInstance(string $artifact_name): ?Artifact + { + self::initArtifactInstances(); + return self::$artifacts[$artifact_name] ?? null; + } + + /** + * Load artifact definitions from PSR-4 directory. + * + * @param string $dir Directory path + * @param string $base_namespace Base namespace for dir's PSR-4 mapping + * @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload) + */ + public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void + { + self::initArtifactInstances(); + $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require); + foreach ($classes as $class) { + self::loadFromClass($class); + } + } + + public static function loadFromClass(string $class): void + { + $ref = new \ReflectionClass($class); + + $class_instance = $ref->newInstance(); + + foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + self::processCustomSourceAttribute($ref, $method, $class_instance); + self::processCustomBinaryAttribute($ref, $method, $class_instance); + self::processSourceExtractAttribute($ref, $method, $class_instance); + self::processBinaryExtractAttribute($ref, $method, $class_instance); + self::processAfterSourceExtractAttribute($ref, $method, $class_instance); + self::processAfterBinaryExtractAttribute($ref, $method, $class_instance); + } + } + + /** + * Process #[CustomSource] attribute. + */ + private static function processCustomSourceAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomSource::class); + foreach ($attributes as $attribute) { + /** @var CustomSource $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setCustomSourceCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSource] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[CustomBinary] attribute. + */ + private static function processCustomBinaryAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomBinary::class); + foreach ($attributes as $attribute) { + /** @var CustomBinary $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + foreach ($instance->support_os as $os) { + self::$artifacts[$artifact_name]->setCustomBinaryCallback($os, [$class_instance, $method->getName()]); + } + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinary] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[SourceExtract] attribute. + * This attribute allows completely taking over the source extraction process. + */ + private static function processSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(SourceExtract::class); + foreach ($attributes as $attribute) { + /** @var SourceExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setSourceExtractCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[SourceExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[BinaryExtract] attribute. + * This attribute allows completely taking over the binary extraction process. + */ + private static function processBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(BinaryExtract::class); + foreach ($attributes as $attribute) { + /** @var BinaryExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setBinaryExtractCallback( + [$class_instance, $method->getName()], + $instance->platforms + ); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[BinaryExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[AfterSourceExtract] attribute. + * This attribute registers a hook that runs after source extraction completes. + */ + private static function processAfterSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(AfterSourceExtract::class); + foreach ($attributes as $attribute) { + /** @var AfterSourceExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->addAfterSourceExtractCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterSourceExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[AfterBinaryExtract] attribute. + * This attribute registers a hook that runs after binary extraction completes. + */ + private static function processAfterBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(AfterBinaryExtract::class); + foreach ($attributes as $attribute) { + /** @var AfterBinaryExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->addAfterBinaryExtractCallback( + [$class_instance, $method->getName()], + $instance->platforms + ); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterBinaryExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } +} diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php new file mode 100644 index 000000000..aefc6716b --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -0,0 +1,146 @@ +cache_type) { + case 'archive': + $this->filename !== null ?: throw new DownloaderException('Archive 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; + case 'git': + case 'local': + $this->dirname !== null ?: throw new DownloaderException('Git/local download result must have a dirname.'); + $dn = FileSystem::isRelativePath($this->dirname) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->dirname) : $this->dirname; + file_exists($dn) ?: throw new DownloaderException("Downloaded directory does not exist: {$dn}"); + break; + } + } + + /** + * Create a download result for an archive file. + * + * @param string $filename Filename of the downloaded archive + * @param mixed $extract Extraction path or configuration + * @param bool $verified Whether the archive has been hash-verified + * @param null|string $version Version string of the downloaded artifact + * @param array $metadata Additional metadata + */ + public static function archive( + string $filename, + array $config, + mixed $extract = null, + bool $verified = false, + ?string $version = null, + array $metadata = [] + ): DownloadResult { + return new self('archive', config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + } + + /** + * Create a download result for a git clone. + * + * @param string $dirname Directory name of the cloned repository + * @param mixed $extract Extraction path or configuration + * @param null|string $version Version string (tag, branch, or commit) + * @param array $metadata Additional metadata (e.g., commit hash) + */ + public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + { + return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + } + + /** + * Create a download result for a local directory. + * + * @param string $dirname Directory name + * @param mixed $extract Extraction path or configuration + * @param null|string $version Version string if known + * @param array $metadata Additional metadata + */ + public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + { + return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + } + + /** + * Check if version information is available. + */ + public function hasVersion(): bool + { + return $this->version !== null; + } + + /** + * Get a metadata value by key. + * + * @param string $key Metadata key + * @param mixed $default Default value if key doesn't exist + */ + public function getMeta(string $key, mixed $default = null): mixed + { + return $this->metadata[$key] ?? $default; + } + + /** + * Create a new DownloadResult with updated version. + * (Immutable pattern - returns a new instance) + */ + public function withVersion(string $version): self + { + return new self( + $this->cache_type, + $this->config, + $this->filename, + $this->dirname, + $this->extract, + $this->verified, + $version, + $this->metadata + ); + } + + /** + * Create a new DownloadResult with additional metadata. + * (Immutable pattern - returns a new instance) + */ + public function withMeta(string $key, mixed $value): self + { + return new self( + $this->cache_type, + $this->config, + $this->filename, + $this->dirname, + $this->extract, + $this->verified, + $this->version, + array_merge($this->metadata, [$key => $value]) + ); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php new file mode 100644 index 000000000..30942fe17 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -0,0 +1,41 @@ +debug("Fetching {$name} API info from bitbucket tag"); + $data = default_shell()->executeCurl(str_replace('{repo}', $config['repo'], self::BITBUCKET_API_URL), retries: $downloader->getRetry()); + $data = json_decode($data ?: '', true); + $ver = $data['values'][0]['name'] ?? null; + if (!$ver) { + throw new DownloaderException("Failed to get {$name} version from BitBucket API"); + } + $download_url = str_replace(['{repo}', '{version}'], [$config['repo'], $ver], self::BITBUCKET_DOWNLOAD_URL); + + $headers = default_shell()->executeCurl($download_url, method: 'HEAD', retries: $downloader->getRetry()); + preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $headers, $matches); + if ($matches) { + $filename = $matches['filename']; + } else { + $filename = "{$name}-{$data['tag_name']}.tar.gz"; + } + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}"); + default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php b/src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php new file mode 100644 index 000000000..bc1dc8210 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php @@ -0,0 +1,18 @@ +debug("Fetching file list from {$config['url']}"); + $page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry()); + preg_match_all($config['regex'], $page ?: '', $matches); + if (!$matches) { + throw new DownloaderException("Failed to get {$name} file list from {$config['url']}"); + } + $versions = []; + foreach ($matches['version'] as $i => $version) { + $lower = strtolower($version); + foreach (['alpha', 'beta', 'rc', 'pre', 'nightly', 'snapshot', 'dev'] as $beta) { + if (str_contains($lower, $beta)) { + continue 2; + } + } + $versions[$version] = $matches['file'][$i]; + } + uksort($versions, 'version_compare'); + $filename = end($versions); + $version = array_key_last($versions); + if (isset($config['download-url'])) { + $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); + } else { + $url = $config['url'] . $filename; + } + $filename = end($versions); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php new file mode 100644 index 000000000..8b1f20d34 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -0,0 +1,22 @@ +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); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php new file mode 100644 index 000000000..2e8a499e3 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -0,0 +1,98 @@ +getGitHubTokenHeaders(); + $data2 = default_shell()->executeCurl($url, headers: $headers); + $data = json_decode($data2 ?: '', true); + if (!is_array($data)) { + throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); + } + foreach ($data as $release) { + if ($prefer_stable && $release['prerelease'] === true) { + continue; + } + foreach ($release['assets'] as $asset) { + if (preg_match("|{$match_asset}|", $asset['name'])) { + if (isset($asset['id'], $asset['name'])) { + // store ghrel asset array (id: ghrel.{$repo}.{stable|unstable}.{$match_asset}) + if ($asset['digest'] !== null && str_starts_with($asset['digest'], 'sha256:')) { + $this->sha256 = substr($asset['digest'], 7); + } + $this->version = $release['tag_name'] ?? null; + return $asset; + } + throw new DownloaderException("Failed to get asset name and id for {$repo}"); + } + } + } + throw new DownloaderException("No suitable GitHub release found for {$repo}"); + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + logger()->debug("Fetching GitHub release for {$name} from {$config['repo']}"); + if (!isset($config['match'])) { + throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); + } + $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match']); + + // download file using curl + $asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL); + $headers = array_merge( + $this->getGitHubTokenHeaders(), + ['Accept: application/octet-stream'] + ); + $filename = $rel['name']; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} asset from URL: {$asset_url}"); + default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version); + } + + public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool + { + if ($result->cache_type != 'archive') { + logger()->warning("GitHub release validator only supports archive download type for {$name} ."); + return false; + } + + if ($this->sha256 !== '') { + $calculated_hash = hash_file('sha256', DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $result->filename); + if ($this->sha256 !== $calculated_hash) { + logger()->error("Hash mismatch for downloaded GitHub release asset of {$name}: expected {$this->sha256}, got {$calculated_hash}"); + return false; + } + logger()->debug("Hash verified for downloaded GitHub release asset of {$name}"); + return true; + } + logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation"); + return true; + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php new file mode 100644 index 000000000..7917e4c01 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -0,0 +1,78 @@ +executeCurl($url, headers: $this->getGitHubTokenHeaders()); + $data = json_decode($data ?: '', true); + if (!is_array($data)) { + throw new DownloaderException("Failed to get GitHub tarball URL for {$repo} from {$url}"); + } + $url = null; + foreach ($data as $rel) { + if (($rel['prerelease'] ?? false) === true && $prefer_stable) { + continue; + } + if ($match_url === null) { + $url = $rel['tarball_url'] ?? null; + $version = $rel['tag_name'] ?? null; + break; + } + if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) { + $url = $rel['tarball_url']; + $version = $rel['tag_name'] ?? null; + break; + } + } + if (!$url) { + throw new DownloaderException("No suitable GitHub tarball found for {$repo}"); + } + $this->version = $version ?? null; + $head = default_shell()->executeCurl($url, 'HEAD', headers: $this->getGitHubTokenHeaders()) ?: ''; + preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $head, $matches); + if ($matches) { + $filename = $matches['filename']; + } else { + $basename = $basename ?? basename($repo); + $filename = "{$basename}-" . ($rel_type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz'; + } + return [$url, $filename]; + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + logger()->debug("Downloading GitHub tarball for {$name} from {$config['repo']}"); + $rel_type = match ($config['type']) { + 'ghtar' => 'releases', + 'ghtagtar' => 'tags', + default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), + }; + [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name); + $path = DOWNLOAD_PATH . "/{$filename}"; + default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php new file mode 100644 index 000000000..d773bde73 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -0,0 +1,22 @@ +debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); + return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")]; + } + if (($token = getenv('GITHUB_TOKEN')) !== false) { + logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + return ["Authorization: Bearer {$token}"]; + } + return []; + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php new file mode 100644 index 000000000..93315ce3a --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php @@ -0,0 +1,18 @@ +debug("Using local source directory for {$name} from {$config['dirname']}"); + return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php new file mode 100644 index 000000000..e4f1a1173 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -0,0 +1,47 @@ +debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); + if ($data === false) { + throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); + } + $data = json_decode($data, true); + if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + // get the first version + $first = $data['packages'][$config['repo']][0] ?? []; + // check 'type' => 'php-ext' or contains 'php-ext' key + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $config['repo']); + $version = $first['version'] ?? 'unknown'; + $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php new file mode 100644 index 000000000..ec6c33fa4 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -0,0 +1,76 @@ +getOption('with-php', '8.4'); + // Handle 'git' version to clone from php-src repository + if ($phpver === 'git') { + $this->sha256 = null; + return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); + } + + // Fetch PHP release info first + $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + if ($info === false) { + throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); + } + $info = json_decode($info, true); + if (!is_array($info) || !isset($info['version'])) { + throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + } + $version = $info['version']; + foreach ($info['source'] as $source) { + if (str_ends_with($source['filename'], '.tar.xz')) { + $this->sha256 = $source['sha256']; + $filename = $source['filename']; + break; + } + } + if (!isset($filename)) { + throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); + } + $url = str_replace('{version}', $version, self::DOWNLOAD_URL); + logger()->debug("Downloading PHP release {$version} from {$url}"); + $path = DOWNLOAD_PATH . "/{$filename}"; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + } + + public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool + { + if ($this->sha256 === null) { + logger()->debug('Php-src is downloaded from non-release source, skipping validation.'); + return true; + } + + if ($this->sha256 === '') { + logger()->error("No SHA256 checksum available for validation of {$name}."); + return false; + } + + $path = DOWNLOAD_PATH . "/{$result->filename}"; + $hash = hash_file('sha256', $path); + if ($hash !== $this->sha256) { + logger()->error("SHA256 checksum mismatch for {$name}: expected {$this->sha256}, got {$hash}"); + return false; + } + logger()->debug("SHA256 checksum validated successfully for {$name}."); + return true; + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php new file mode 100644 index 000000000..a56f4dc71 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -0,0 +1,23 @@ +debug("Downloading {$name} from URL: {$url}"); + $version = $config['version'] ?? null; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php b/src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php new file mode 100644 index 000000000..1180ff78e --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php @@ -0,0 +1,13 @@ +addOption('debug', null, null, '(deprecated) Enable debug mode'); + $this->addOption('no-motd', null, null, 'Disable motd'); + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + $this->input = $input; + $this->output = $output; + + // Bind command context to ApplicationContext + ApplicationContext::bindCommandContext($input, $output); + + if ($input->getOption('no-motd')) { + $this->no_motd = true; + } + + set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) { + $tips = [ + E_WARNING => ['PHP Warning: ', 'warning'], + E_NOTICE => ['PHP Notice: ', 'notice'], + E_USER_ERROR => ['PHP Error: ', 'error'], + E_USER_WARNING => ['PHP Warning: ', 'warning'], + E_USER_NOTICE => ['PHP Notice: ', 'notice'], + E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'], + E_DEPRECATED => ['PHP Deprecated: ', 'notice'], + E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'], + ]; + $level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error']; + $error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line; + logger()->{$level_tip[1]}($error); + // 如果 return false 则错误会继续递交给 PHP 标准错误处理 + return true; + }); + $version = $this->getVersionWithCommit(); + if (!$this->no_motd) { + echo str_replace('{version}', $version, self::$motd); + } + } + + abstract public function handle(): int; + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + // handle verbose option + $level = match ($this->output->getVerbosity()) { + OutputInterface::VERBOSITY_VERBOSE => 'info', + OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug', + default => 'warning', + }; + logger()->setLevel($level); + + // ansi + if ($this->input->getOption('no-ansi')) { + logger()->setDecorated(false); + } + + // Set debug mode in ApplicationContext + $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE; + ApplicationContext::setDebug($isDebug); + + // show raw argv list for logger()->debug + logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); + return $this->handle(); + } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { + // Handle SPCException and log it + ExceptionHandler::handleSPCException($e); + return static::FAILURE; + } catch (\Throwable $e) { + // Handle any other exceptions + ExceptionHandler::handleDefaultException($e); + return static::FAILURE; + } + } + + protected function getOption(string $name): mixed + { + return $this->input->getOption($name); + } + + protected function getArgument(string $name): mixed + { + return $this->input->getArgument($name); + } + + /** + * Get version string with git commit short ID if available. + */ + private function getVersionWithCommit(): string + { + $version = $this->getApplication()->getVersion(); + + // Don't show commit ID when running in phar + if (\Phar::running()) { + return $version; + } + + $commitId = $this->getGitCommitShortId(); + if ($commitId) { + return "{$version} ({$commitId})"; + } + + return $version; + } + + /** + * Get git commit short ID without executing git command. + */ + private function getGitCommitShortId(): ?string + { + try { + $gitDir = ROOT_DIR . '/.git'; + + if (!is_dir($gitDir)) { + return null; + } + + $headFile = $gitDir . '/HEAD'; + if (!file_exists($headFile)) { + return null; + } + + $head = trim(file_get_contents($headFile)); + + // If HEAD contains 'ref:', it's a branch reference + if (str_starts_with($head, 'ref: ')) { + $ref = substr($head, 5); + $refFile = $gitDir . '/' . $ref; + + if (file_exists($refFile)) { + $commit = trim(file_get_contents($refFile)); + return substr($commit, 0, 7); + } + } else { + // HEAD contains the commit hash directly (detached HEAD) + return substr($head, 0, 7); + } + } catch (\Throwable) { + // Silently fail if we can't read git info + } + + return null; + } +} diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php new file mode 100644 index 000000000..2ff36b49a --- /dev/null +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -0,0 +1,29 @@ +addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated'); + } + + public function handle(): int + { + $libs = parse_comma_list($this->input->getArgument('libraries')); + + $installer = new \StaticPHP\Package\PackageInstaller($this->input->getOptions()); + foreach ($libs as $lib) { + $installer->addBuildPackage($lib); + } + $installer->run(); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php new file mode 100644 index 000000000..5efb9f1af --- /dev/null +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -0,0 +1,56 @@ +setAliases(['build']); + } + $this->setDescription($description ?? "Build {$target} target from source"); + $pkg = PackageLoader::getTargetPackage($target); + $this->getDefinition()->addOptions($pkg->_exportBuildOptions()); + $this->getDefinition()->addArguments($pkg->_exportBuildArguments()); + + // Builder options + $this->getDefinition()->addOptions([ + new InputOption('with-suggests', ['L', 'E'], null, 'Resolve and install suggested packages as well'), + new InputOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional packages to install/build, comma separated', ''), + new InputOption('no-download', null, null, 'Skip downloading artifacts (use existing cached files)'), + ...V2CompatLayer::getLegacyBuildOptions(), + ]); + + // Downloader options (with 'dl-' prefix to avoid conflicts) + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions('dl')); + } + + public function handle(): int + { + // resolve legacy options to new options + V2CompatLayer::convertOptions($this->input); + + $starttime = microtime(true); + // run installer + $installer = new PackageInstaller($this->input->getOptions()); + $installer->addBuildPackage($this->target); + $installer->run(); + + $usedtime = round(microtime(true) - $starttime, 1); + $this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); + $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php new file mode 100644 index 000000000..30475a5ee --- /dev/null +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -0,0 +1,34 @@ +addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false); + } + + public function handle(): int + { + $fix_policy = match ($this->input->getOption('auto-fix')) { + 'never' => FIX_POLICY_DIE, + true, null => FIX_POLICY_AUTOFIX, + default => FIX_POLICY_PROMPT, + }; + $doctor = new Doctor($this->output, $fix_policy); + if ($doctor->checkAll()) { + $this->output->writeln('Doctor check complete !'); + return static::SUCCESS; + } + + return static::FAILURE; + } +} diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php new file mode 100644 index 000000000..967d38858 --- /dev/null +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -0,0 +1,116 @@ +addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to download, comma separated, e.g "php-src,openssl,curl"'); + + // 2.x compatible options + $this->addOption('shallow-clone', null, null, '(deprecated) Clone shallowly repositories when downloading sources'); + $this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Fetch by extensions, e.g "openssl,mbstring"'); + $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Fetch by libraries, e.g "libcares,openssl,onig"'); + $this->addOption('without-suggests', null, null, 'Do not fetch suggested sources when using --for-extensions'); + + // download command specific options + $this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); + $this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Fetch by packages, e.g "php,libssl,libcurl"'); + + // shared downloader options (no prefix for download command) + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions()); + } + + public function handle(): int + { + // handle --clean option + if ($this->getOption('clean')) { + return $this->handleClean(); + } + + $downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions())); + + // arguments + if ($artifacts = $this->getArgument('artifacts')) { + $artifacts = parse_comma_list($artifacts); + $downloader->addArtifacts($artifacts); + } + // for-extensions + $packages = []; + if ($exts = $this->getOption('for-extensions')) { + $packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts)); + // when using for-extensions, also include php package + array_unshift($packages, 'php'); + array_unshift($packages, 'php-micro'); + array_unshift($packages, 'php-embed'); + array_unshift($packages, 'php-fpm'); + } + // for-libs / for-packages + if ($libs = $this->getOption('for-libs')) { + $packages = array_merge($packages, parse_comma_list($libs)); + } + if ($libs = $this->getOption('for-packages')) { + $packages = array_merge($packages, parse_comma_list($libs)); + } + + // resolve package dependencies and get artifacts directly + $resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests')); + foreach ($resolved as $pkg_name) { + $pkg = PackageLoader::getPackage($pkg_name); + if ($artifact = $pkg->getArtifact()) { + $downloader->add($artifact); + } + } + $starttime = microtime(true); + $downloader->download(); + + $endtime = microtime(true); + $elapsed = round($endtime - $starttime); + $this->output->writeln(''); + $this->output->writeln('Download completed in ' . $elapsed . ' s.'); + return static::SUCCESS; + } + + private function handleClean(): int + { + logger()->warning('You are doing some operations that are not recoverable:'); + logger()->warning('- Removing directory: ' . SOURCE_PATH); + logger()->warning('- Removing directory: ' . DOWNLOAD_PATH); + logger()->warning('- Removing directory: ' . BUILD_ROOT_PATH); + logger()->alert('I will remove these directories after 5 seconds!'); + sleep(5); + + if (is_dir(SOURCE_PATH)) { + InteractiveTerm::indicateProgress('Removing: ' . SOURCE_PATH); + FileSystem::removeDir(SOURCE_PATH); + InteractiveTerm::finish('Removed: ' . SOURCE_PATH); + } + if (is_dir(DOWNLOAD_PATH)) { + InteractiveTerm::indicateProgress('Removing: ' . DOWNLOAD_PATH); + FileSystem::removeDir(DOWNLOAD_PATH); + InteractiveTerm::finish('Removed: ' . DOWNLOAD_PATH); + } + if (is_dir(BUILD_ROOT_PATH)) { + InteractiveTerm::indicateProgress('Removing: ' . BUILD_ROOT_PATH); + FileSystem::removeDir(BUILD_ROOT_PATH); + InteractiveTerm::finish('Removed: ' . BUILD_ROOT_PATH); + } + + InteractiveTerm::notice('Clean completed.'); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php new file mode 100644 index 000000000..b897e59af --- /dev/null +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -0,0 +1,108 @@ +setDescription('Extract downloaded artifacts to their target locations'); + + $this->addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to extract, comma separated, e.g "php-src,openssl,curl"'); + + $this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Extract artifacts for extensions, e.g "openssl,mbstring"'); + $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Extract artifacts for libraries, e.g "libcares,openssl"'); + $this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Extract artifacts for packages, e.g "php,libssl,libcurl"'); + $this->addOption('without-suggests', null, null, 'Do not include suggested packages when using --for-extensions'); + $this->addOption('force-source', null, null, 'Force extract source even if binary is available'); + } + + public function handle(): int + { + $cache = ApplicationContext::get(ArtifactCache::class); + $extractor = new ArtifactExtractor($cache); + $force_source = (bool) $this->getOption('force-source'); + + $artifacts = []; + + // Direct artifact names + if ($artifact_arg = $this->getArgument('artifacts')) { + $artifact_names = parse_comma_list($artifact_arg); + foreach ($artifact_names as $name) { + $artifact = ArtifactLoader::getArtifactInstance($name); + if ($artifact === null) { + $this->output->writeln("Artifact '{$name}' not found."); + return static::FAILURE; + } + $artifacts[$name] = $artifact; + } + } + + // Resolve packages and get their artifacts + $packages = []; + if ($exts = $this->getOption('for-extensions')) { + $packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts)); + // Include php package when using for-extensions + array_unshift($packages, 'php'); + } + if ($libs = $this->getOption('for-libs')) { + $packages = array_merge($packages, parse_comma_list($libs)); + } + if ($pkgs = $this->getOption('for-packages')) { + $packages = array_merge($packages, parse_comma_list($pkgs)); + } + + if (!empty($packages)) { + $resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests')); + foreach ($resolved as $pkg_name) { + $pkg = PackageLoader::getPackage($pkg_name); + if ($artifact = $pkg->getArtifact()) { + $artifacts[$artifact->getName()] = $artifact; + } + } + } + + if (empty($artifacts)) { + $this->output->writeln('No artifacts specified. Use artifact names or --for-extensions/--for-libs/--for-packages options.'); + $this->output->writeln(''); + $this->output->writeln('Examples:'); + $this->output->writeln(' spc extract php-src,openssl'); + $this->output->writeln(' spc extract --for-extensions=openssl,mbstring'); + $this->output->writeln(' spc extract --for-libs=libcurl,libssl'); + return static::SUCCESS; + } + + // make php-src always extracted first + uksort($artifacts, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : 0)); + + try { + InteractiveTerm::notice('Extracting ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . '...'); + InteractiveTerm::indicateProgress('Extracting artifacts'); + foreach ($artifacts as $artifact) { + InteractiveTerm::setMessage('Extracting artifact: ' . ConsoleColor::green($artifact->getName())); + $extractor->extract($artifact, $force_source); + } + InteractiveTerm::finish('Extracted all artifacts successfully.'); + } catch (\Exception $e) { + InteractiveTerm::finish('Extraction failed!', false); + throw $e; + } + + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php new file mode 100644 index 000000000..37cb04b37 --- /dev/null +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -0,0 +1,27 @@ +addArgument('package', null, 'The package to install (name or path)'); + } + + public function handle(): int + { + ApplicationContext::set('elephant', true); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); + $installer->addInstallPackage($this->input->getArgument('package')); + $installer->run(true, true); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/SPCConfigCommand.php b/src/StaticPHP/Command/SPCConfigCommand.php new file mode 100644 index 000000000..399e1eae2 --- /dev/null +++ b/src/StaticPHP/Command/SPCConfigCommand.php @@ -0,0 +1,56 @@ +addArgument('extensions', InputArgument::OPTIONAL, 'The extensions will be compiled, comma separated'); + $this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', ''); + $this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs'); + $this->addOption('with-suggests', null, null, 'Build with suggested packages for selected exts and libs'); + $this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts'); + $this->addOption('includes', null, null, 'Add additional include path'); + $this->addOption('libs', null, null, 'Add additional libs path'); + $this->addOption('libs-only-deps', null, null, 'Output dependent libraries with -l prefix'); + $this->addOption('absolute-libs', null, null, 'Output absolute paths for libraries'); + $this->addOption('no-php', null, null, 'Link to PHP library'); + } + + public function handle(): int + { + // transform string to array + $libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs')))); + // transform string to array + $extensions = $this->getArgument('extensions') ? parse_extension_list($this->getArgument('extensions')) : []; + $include_suggests = $this->getOption('with-suggests') ?: $this->getOption('with-suggested-libs') || $this->getOption('with-suggested-exts'); + + $util = new SPCConfigUtil(options: [ + 'no_php' => $this->getOption('no-php'), + 'libs_only_deps' => $this->getOption('libs-only-deps'), + 'absolute_libs' => $this->getOption('absolute-libs'), + ]); + $packages = array_merge(array_map(fn ($x) => "ext-{$x}", $extensions), $libraries); + $config = $util->config($packages, $include_suggests); + + $this->output->writeln(match (true) { + $this->getOption('includes') => $config['cflags'], + $this->getOption('libs-only-deps') => $config['libs'], + $this->getOption('libs') => "{$config['ldflags']} {$config['libs']}", + default => "{$config['cflags']} {$config['ldflags']} {$config['libs']}", + }); + + return 0; + } +} diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php new file mode 100644 index 000000000..d25c6dd1a --- /dev/null +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -0,0 +1,68 @@ + $config) { + self::$artifact_configs[$artifact_name] = $config; + } + } + + /** + * Get all loaded artifact configurations. + * + * @return array an associative array of artifact configurations + */ + public static function getAll(): array + { + return self::$artifact_configs; + } + + /** + * Get the configuration for a specific artifact by name. + * + * @param string $artifact_name the name of the artifact + * @return null|array the configuration array for the specified artifact, or null if not found + */ + public static function get(string $artifact_name): ?array + { + return self::$artifact_configs[$artifact_name] ?? null; + } +} diff --git a/src/StaticPHP/Config/ConfigType.php b/src/StaticPHP/Config/ConfigType.php new file mode 100644 index 000000000..31f96ee41 --- /dev/null +++ b/src/StaticPHP/Config/ConfigType.php @@ -0,0 +1,52 @@ + match (true) { + !isset($value['path']), !is_string($value['path']) && !is_array($value['path']) => false, + default => true, + }, + 'text' => match (true) { + !isset($value['text']), !is_string($value['text']) => false, + default => true, + }, + default => false, + }; + } +} diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php new file mode 100644 index 000000000..3ddb9bab0 --- /dev/null +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -0,0 +1,370 @@ + ConfigType::STRING, + 'depends' => ConfigType::LIST_ARRAY, // @ + 'suggests' => ConfigType::LIST_ARRAY, // @ + 'artifact' => ConfigType::STRING, + 'license' => [ConfigType::class, 'validateLicenseField'], + 'lang' => ConfigType::STRING, + 'frameworks' => ConfigType::LIST_ARRAY, // @ + + // php-extension type fields + 'php-extension' => ConfigType::ASSOC_ARRAY, + 'zend-extension' => ConfigType::BOOL, + 'support' => ConfigType::ASSOC_ARRAY, + 'arg-type' => ConfigType::STRING, + 'build-shared' => ConfigType::BOOL, + 'build-static' => ConfigType::BOOL, + 'build-with-php' => ConfigType::BOOL, + 'notes' => ConfigType::BOOL, + + // library and target fields + 'headers' => ConfigType::LIST_ARRAY, // @ + 'static-libs' => ConfigType::LIST_ARRAY, // @ + 'pkg-configs' => ConfigType::LIST_ARRAY, + 'static-bins' => ConfigType::LIST_ARRAY, // @ + ]; + + public const array PACKAGE_FIELDS = [ + 'type' => true, + 'depends' => false, // @ + 'suggests' => false, // @ + 'artifact' => false, + 'license' => false, + 'lang' => false, + 'frameworks' => false, // @ + + // php-extension type fields + 'php-extension' => false, + + // library and target fields + 'headers' => false, // @ + 'static-libs' => false, // @ + 'pkg-configs' => false, + 'static-bins' => false, // @ + ]; + + public const array SUFFIX_ALLOWED_FIELDS = [ + 'depends', + 'suggests', + 'headers', + 'static-libs', + 'static-bins', + ]; + + public const array PHP_EXTENSION_FIELDS = [ + 'zend-extension' => false, + 'support' => false, + 'arg-type' => false, // @ + 'build-shared' => false, + 'build-static' => false, + 'build-with-php' => false, + 'notes' => false, + ]; + + public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] + 'filelist' => [['url', 'regex'], ['extract']], + 'git' => [['url', 'rev'], ['extract', 'submodules']], + 'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']], + 'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']], + 'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']], + 'url' => [['url'], ['filename', 'extract', 'version']], + 'bitbuckettag' => [['repo'], ['extract']], + 'local' => [['dirname'], ['extract']], + 'pie' => [['repo'], ['extract']], + 'php-release' => [[], ['extract']], + 'custom' => [[], ['func']], + ]; + + /** + * Validate and standardize artifacts configuration data. + * + * @param string $config_file_name Name of the configuration file (for error messages) + * @param mixed $data The configuration data to validate + */ + public static function validateAndLintArtifacts(string $config_file_name, mixed &$data): void + { + if (!is_array($data)) { + throw new ValidationException("{$config_file_name} is broken"); + } + foreach ($data as $name => $artifact) { + foreach ($artifact as $k => $v) { + // check source field + if ($k === 'source' || $k === 'source-mirror') { + // source === custom is allowed + if ($v === 'custom') { + continue; + } + // expand string to url type (start with http:// or https://) + if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) { + $data[$name][$k] = [ + 'type' => 'url', + 'url' => $v, + ]; + continue; + } + // source: object with type field + if (is_assoc_array($v)) { + self::validateArtifactObjectField($name, $v); + } + continue; + } + // check binary field + if ($k === 'binary') { + // binary === custom is allowed + if ($v === 'custom') { + $data[$name][$k] = [ + 'linux-x86_64' => ['type' => 'custom'], + 'linux-aarch64' => ['type' => 'custom'], + 'windows-x86_64' => ['type' => 'custom'], + 'macos-x86_64' => ['type' => 'custom'], + 'macos-aarch64' => ['type' => 'custom'], + ]; + continue; + } + // TODO: expand hosted to static-php hosted download urls + if ($v === 'hosted') { + continue; + } + if (is_assoc_array($v)) { + foreach ($v as $platform => $v_obj) { + self::validatePlatformString($platform); + // expand string to url type (start with http:// or https://) + if (is_string($v_obj) && (str_starts_with($v_obj, 'http://') || str_starts_with($v_obj, 'https://'))) { + $data[$name][$k][$platform] = [ + 'type' => 'url', + 'url' => $v_obj, + ]; + continue; + } + // binary: object with type field + if (is_assoc_array($v_obj)) { + self::validateArtifactObjectField("{$name}::{$platform}", $v_obj); + } + } + } + } + } + } + } + + /** + * Validate packages configuration data. + * + * @param string $config_file_name Name of the configuration file (for error messages) + * @param mixed $data The configuration data to validate + */ + public static function validateAndLintPackages(string $config_file_name, mixed &$data): void + { + if (!is_array($data)) { + throw new ValidationException("{$config_file_name} is broken"); + } + foreach ($data as $name => $pkg) { + if (!is_assoc_array($pkg)) { + throw new ValidationException("Package [{$name}] in {$config_file_name} is not a valid associative array"); + } + // check if package has valid type + if (!isset($pkg['type']) || !in_array($pkg['type'], ConfigType::PACKAGE_TYPES)) { + throw new ValidationException("Package [{$name}] in {$config_file_name} has invalid or missing 'type' field"); + } + + // validate basic fields using unified method + self::validatePackageFields($name, $pkg); + + // validate list of suffix-allowed fields + $suffixes = ['', '@windows', '@unix', '@macos', '@linux']; + $fields = self::SUFFIX_ALLOWED_FIELDS; + self::validateSuffixAllowedFields($name, $pkg, $fields, $suffixes); + + // check if "library|target" package has artifact field for target and library types + if (in_array($pkg['type'], ['target', 'library']) && !isset($pkg['artifact'])) { + throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field"); + } + + // check if "php-extension" package has php-extension specific fields and validate inside + if ($pkg['type'] === 'php-extension') { + self::validatePhpExtensionFields($name, $pkg); + } + + // check for unknown fields + self::validateNoInvalidFields('package', $name, $pkg, array_keys(self::PACKAGE_FIELD_TYPES)); + } + } + + /** + * Validate platform string format. + * + * @param string $platform Platform string, like windows-x86_64 + */ + public static function validatePlatformString(string $platform): void + { + $valid_platforms = ['windows', 'linux', 'macos']; + $valid_arch = ['x86_64', 'aarch64']; + $parts = explode('-', $platform); + if (count($parts) !== 2) { + throw new ValidationException("Invalid platform format '{$platform}', expected format 'os-arch'"); + } + [$os, $arch] = $parts; + if (!in_array($os, $valid_platforms)) { + throw new ValidationException("Invalid platform OS '{$os}' in platform '{$platform}'"); + } + if (!in_array($arch, $valid_arch)) { + throw new ValidationException("Invalid platform architecture '{$arch}' in platform '{$platform}'"); + } + } + + /** + * Validate an artifact download object field. + * + * @param string $item_name Artifact name (for error messages) + * @param array $data Artifact source object data + */ + private static function validateArtifactObjectField(string $item_name, array $data): void + { + if (!isset($data['type']) || !is_string($data['type'])) { + throw new ValidationException("Artifact source object must have a valid 'type' field"); + } + $type = $data['type']; + if (!isset(self::ARTIFACT_TYPE_FIELDS[$type])) { + throw new ValidationException("Artifact source object has unknown type '{$type}'"); + } + [$required_fields, $optional_fields] = self::ARTIFACT_TYPE_FIELDS[$type]; + // check required fields + foreach ($required_fields as $field) { + if (!isset($data[$field])) { + throw new ValidationException("Artifact source object of type '{$type}' must have required field '{$field}'"); + } + } + // check for unknown fields + $allowed_fields = array_merge(['type'], $required_fields, $optional_fields); + self::validateNoInvalidFields('artifact object', $item_name, $data, $allowed_fields); + } + + /** + * Unified method to validate config fields based on field definitions + * + * @param string $package_name Package name + * @param mixed $pkg The package configuration array + */ + private static function validatePackageFields(string $package_name, mixed $pkg): void + { + foreach (self::PACKAGE_FIELDS as $field => $required) { + if ($required && !isset($pkg[$field])) { + throw new ValidationException("Package {$package_name} must have [{$field}] field"); + } + + if (isset($pkg[$field])) { + self::validatePackageFieldType($field, $pkg[$field], $package_name); + } + } + } + + /** + * Validate a field based on its global type definition + * + * @param string $field Field name + * @param mixed $value Field value + * @param string $package_name Package name (for error messages) + */ + private static function validatePackageFieldType(string $field, mixed $value, string $package_name): void + { + // Check if field exists in FIELD_TYPES + if (!isset(self::PACKAGE_FIELD_TYPES[$field])) { + // Try to strip suffix and check base field name + $suffixes = ['@windows', '@unix', '@macos', '@linux']; + $base_field = $field; + foreach ($suffixes as $suffix) { + if (str_ends_with($field, $suffix)) { + $base_field = substr($field, 0, -strlen($suffix)); + break; + } + } + + if (!isset(self::PACKAGE_FIELD_TYPES[$base_field])) { + // Unknown field is not allowed - strict validation + throw new ValidationException("Package {$package_name} has unknown field [{$field}]"); + } + + // Use base field type for validation + $expected_type = self::PACKAGE_FIELD_TYPES[$base_field]; + } else { + $expected_type = self::PACKAGE_FIELD_TYPES[$field]; + } + + match ($expected_type) { + ConfigType::STRING => is_string($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be string"), + ConfigType::BOOL => is_bool($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be boolean"), + ConfigType::LIST_ARRAY => is_list_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be a list"), + ConfigType::ASSOC_ARRAY => is_assoc_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be an object"), + default => $expected_type($value) ?: throw new ValidationException("Package {$package_name} [{$field}] has invalid type specification"), + }; + } + + /** + * Validate that fields with suffixes are list arrays + */ + private static function validateSuffixAllowedFields(int|string $name, mixed $item, array $fields, array $suffixes): void + { + foreach ($fields as $field) { + foreach ($suffixes as $suffix) { + $key = $field . $suffix; + if (isset($item[$key])) { + self::validatePackageFieldType($key, $item[$key], $name); + } + } + } + } + + /** + * Validate php-extension specific fields for php-extension package + */ + private static function validatePhpExtensionFields(int|string $name, mixed $pkg): void + { + if (!isset($pkg['php-extension'])) { + return; + } + if (!is_assoc_array($pkg['php-extension'])) { + throw new ValidationException("Package {$name} [php-extension] must be an object"); + } + foreach (self::PHP_EXTENSION_FIELDS as $field => $required) { + if (isset($pkg['php-extension'][$field])) { + self::validatePackageFieldType($field, $pkg['php-extension'][$field], $name); + } + } + // check for unknown fields in php-extension + self::validateNoInvalidFields('php-extension', $name, $pkg['php-extension'], array_keys(self::PHP_EXTENSION_FIELDS)); + } + + private static function validateNoInvalidFields(string $config_type, int|string $item_name, mixed $item_content, array $allowed_fields): void + { + foreach ($item_content as $k => $v) { + // remove suffixes for checking + $base_k = $k; + $suffixes = ['@windows', '@unix', '@macos', '@linux']; + foreach ($suffixes as $suffix) { + if (str_ends_with($k, $suffix)) { + $base_k = substr($k, 0, -strlen($suffix)); + break; + } + } + if (!in_array($base_k, $allowed_fields)) { + throw new ValidationException("{$config_type} [{$item_name}] has invalid field [{$base_k}]"); + } + } + } +} diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php new file mode 100644 index 000000000..dc0b3d546 --- /dev/null +++ b/src/StaticPHP/Config/PackageConfig.php @@ -0,0 +1,102 @@ + $config) { + self::$package_configs[$pkg_name] = $config; + } + } + + /** + * Check if a package configuration exists. + */ + public static function isPackageExists(string $pkg_name): bool + { + return isset(self::$package_configs[$pkg_name]); + } + + public static function getAll(): array + { + return self::$package_configs; + } + + /** + * Get a specific field from a package configuration. + * + * @param string $pkg_name Package name + * @param null|string $field_name Package config field name + * @param null|mixed $default Default value if field not found + * @return mixed The value of the specified field or the default value + */ + public static function get(string $pkg_name, ?string $field_name = null, mixed $default = null): mixed + { + if (!self::isPackageExists($pkg_name)) { + return $default; + } + // use suffixes to find field + $suffixes = match (SystemTarget::getTargetOS()) { + 'Windows' => ['@windows', ''], + 'Darwin' => ['@macos', '@unix', ''], + 'Linux' => ['@linux', '@unix', ''], + 'BSD' => ['@freebsd', '@bsd', '@unix', ''], + }; + if ($field_name === null) { + return self::$package_configs[$pkg_name]; + } + if (in_array($field_name, ConfigValidator::SUFFIX_ALLOWED_FIELDS)) { + foreach ($suffixes as $suffix) { + $suffixed_field = $field_name . $suffix; + if (isset(self::$package_configs[$pkg_name][$suffixed_field])) { + return self::$package_configs[$pkg_name][$suffixed_field]; + } + } + return $default; + } + return self::$package_configs[$pkg_name][$field_name] ?? $default; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php new file mode 100644 index 000000000..b60fd10eb --- /dev/null +++ b/src/StaticPHP/ConsoleApplication.php @@ -0,0 +1,64 @@ + $package) { + // only add target that contains artifact.source + if ($package->hasStage('build')) { + logger()->debug("Registering build target command for package: {$name}"); + $this->add(new BuildTargetCommand($name)); + } + } + + $this->addCommands([ + new DownloadCommand(), + new DoctorCommand(), + new InstallPackageCommand(), + new BuildLibsCommand(), + new ExtractCommand(), + new SPCConfigCommand(), + ]); + + // add additional commands from registries + if (!empty(self::$additional_commands)) { + $this->addCommands(self::$additional_commands); + } + } + + /** + * @internal + */ + public static function _addAdditionalCommands(array $additional_commands): void + { + self::$additional_commands = array_merge(self::$additional_commands, $additional_commands); + } +} diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php new file mode 100644 index 000000000..c27203532 --- /dev/null +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -0,0 +1,195 @@ +set() calls throughout the codebase. + */ +class ApplicationContext +{ + private static ?Container $container = null; + + private static ?CallbackInvoker $invoker = null; + + private static bool $debug = false; + + /** + * Initialize the container with configuration. + * Should only be called once at application startup. + * + * @param array $options Initialization options + * - 'debug': Enable debug mode (disables compilation) + * - 'definitions': Additional container definitions + * + * @throws \RuntimeException If already initialized + */ + public static function initialize(array $options = []): Container + { + if (self::$container !== null) { + throw new \RuntimeException('ApplicationContext already initialized. Use reset() first if you need to reinitialize.'); + } + + $builder = new ContainerBuilder(); + $builder->useAutowiring(true); + $builder->useAttributes(true); + + // Load default definitions + self::configureDefaults($builder); + + // Add custom definitions if provided + if (isset($options['definitions']) && is_array($options['definitions'])) { + $builder->addDefinitions($options['definitions']); + } + + // Set debug mode + self::$debug = $options['debug'] ?? false; + + self::$container = $builder->build(); + self::$invoker = new CallbackInvoker(self::$container); + + return self::$container; + } + + /** + * Get the container instance. + * If not initialized, initializes with default configuration. + */ + public static function getContainer(): Container + { + if (self::$container === null) { + self::initialize(); + } + return self::$container; + } + + /** + * Get a service from the container. + * + * @template T + * + * @param class-string $id Service identifier + * + * @return T + */ + public static function get(string $id): mixed + { + return self::getContainer()->get($id); + } + + /** + * Check if a service exists in the container. + */ + public static function has(string $id): bool + { + return self::getContainer()->has($id); + } + + /** + * Set a service in the container. + * Use sparingly - prefer configuration-based definitions. + */ + public static function set(string $id, mixed $value): void + { + self::getContainer()->set($id, $value); + } + + /** + * Bind command-line context to the container. + * Called at the start of each command execution. + */ + public static function bindCommandContext(InputInterface $input, OutputInterface $output): void + { + $container = self::getContainer(); + $container->set(InputInterface::class, $input); + $container->set(OutputInterface::class, $output); + self::$debug = $output->isDebug(); + } + + /** + * Get the callback invoker instance. + */ + public static function getInvoker(): CallbackInvoker + { + if (self::$invoker === null) { + self::$invoker = new CallbackInvoker(self::getContainer()); + } + return self::$invoker; + } + + /** + * Invoke a callback with automatic dependency injection and context. + * + * @param callable $callback The callback to invoke + * @param array $context Context parameters for injection + */ + public static function invoke(callable $callback, array $context = []): mixed + { + logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + return self::getInvoker()->invoke($callback, $context); + } + + /** + * Check if debug mode is enabled. + */ + public static function isDebug(): bool + { + return self::$debug; + } + + /** + * Set debug mode. + */ + public static function setDebug(bool $debug): void + { + self::$debug = $debug; + } + + /** + * Reset the container. + * Primarily used for testing to ensure isolation between tests. + */ + public static function reset(): void + { + self::$container = null; + self::$invoker = null; + self::$debug = false; + } + + /** + * Configure default container definitions. + */ + private static function configureDefaults(ContainerBuilder $builder): void + { + $builder->addDefinitions([ + // Self-reference for container + ContainerInterface::class => factory(function (Container $c) { + return $c; + }), + Container::class => factory(function (Container $c) { + return $c; + }), + + // CallbackInvoker is created separately to avoid circular dependency + CallbackInvoker::class => factory(function (Container $c) { + return new CallbackInvoker($c); + }), + + // Command context (set at runtime via bindCommandContext) + InputInterface::class => \DI\value(null), + OutputInterface::class => \DI\value(null), + ]); + } +} diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php new file mode 100644 index 000000000..f14f94680 --- /dev/null +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -0,0 +1,98 @@ + value or name => value) + * + * @return mixed The return value of the callback + * + * @throws \RuntimeException If a required parameter cannot be resolved + */ + public function invoke(callable $callback, array $context = []): mixed + { + $reflection = new \ReflectionFunction(\Closure::fromCallable($callback)); + $args = []; + + foreach ($reflection->getParameters() as $param) { + $type = $param->getType(); + $typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null; + $paramName = $param->getName(); + + // 1. Look up by type name in context + if ($typeName !== null && array_key_exists($typeName, $context)) { + $args[] = $context[$typeName]; + continue; + } + + // 2. Look up by parameter name in context + if (array_key_exists($paramName, $context)) { + $args[] = $context[$paramName]; + continue; + } + + // 3. Look up in container by type + if ($typeName !== null && !$this->isBuiltinType($typeName) && $this->container->has($typeName)) { + $args[] = $this->container->get($typeName); + continue; + } + + // 4. Use default value if available + if ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + continue; + } + + // 5. Allow null if nullable + if ($param->allowsNull()) { + $args[] = null; + continue; + } + + // Cannot resolve parameter + throw new \RuntimeException( + "Cannot resolve parameter '{$paramName}'" . + ($typeName ? " of type '{$typeName}'" : '') . + ' for callback invocation' + ); + } + + return $callback(...$args); + } + + /** + * Check if a type name is a PHP builtin type. + */ + private function isBuiltinType(string $typeName): bool + { + return in_array($typeName, [ + 'string', 'int', 'float', 'bool', 'array', + 'object', 'callable', 'iterable', 'mixed', + 'void', 'null', 'false', 'true', 'never', + ], true); + } +} diff --git a/src/StaticPHP/Doctor/CheckResult.php b/src/StaticPHP/Doctor/CheckResult.php new file mode 100644 index 000000000..327b1e8b7 --- /dev/null +++ b/src/StaticPHP/Doctor/CheckResult.php @@ -0,0 +1,46 @@ +message; + } + + public function getFixItem(): string + { + return $this->fix_item; + } + + public function getFixParams(): array + { + return $this->fix_params; + } + + public function isOK(): bool + { + return $this->ok; + } + + public function setFixItem(string $fix_item = '', array $fix_params = []): void + { + $this->fix_item = $fix_item; + $this->fix_params = $fix_params; + } +} diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php new file mode 100644 index 000000000..8141fa32f --- /dev/null +++ b/src/StaticPHP/Doctor/Doctor.php @@ -0,0 +1,161 @@ + $i->item_name, array_map(fn ($x) => $x[0], $items)); + logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); + } + + /** + * Check all valid check items. + * @return bool true if all checks passed, false otherwise + */ + public function checkAll(bool $interactive = true): bool + { + if ($interactive) { + InteractiveTerm::notice('Starting doctor checks ...'); + } + foreach ($this->getValidCheckList() as $check) { + if (!$this->checkItem($check)) { + return false; + } + } + return true; + } + + /** + * Check a single check item. + * + * @param CheckItem|string $check The check item to be checked + * @return bool True if the check passed or was fixed, false otherwise + */ + public function checkItem(CheckItem|string $check): bool + { + if (is_string($check)) { + $found = null; + foreach (DoctorLoader::getDoctorItems() as $item) { + if ($item[0]->item_name === $check) { + $found = $item[0]; + break; + } + } + if ($found === null) { + $this->output?->writeln("Check item '{$check}' not found."); + return false; + } + $check = $found; + } + $this->output?->write("Checking {$check->item_name} ... "); + + // call check + $result = call_user_func($check->callback); + + if ($result === null) { + $this->output?->writeln('skipped'); + return true; + } + if (!$result instanceof CheckResult) { + $this->output?->writeln('Skipped due to invalid return value'); + return true; + } + if ($result->isOK()) { + /* @phpstan-ignore-next-line */ + $this->output?->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓')); + return true; + } + $this->output?->writeln('' . $result->getMessage() . ''); + + // if the check item is not fixable, fail immediately + if ($result->getFixItem() === '') { + $this->output?->writeln('This check item can not be fixed automatically !'); + return false; + } + // unknown fix item + if (!DoctorLoader::getFixItem($result->getFixItem())) { + $this->output?->writeln("Internal error: Unknown fix item: {$result->getFixItem()}"); + return false; + } + // skip fix + if ($this->auto_fix === FIX_POLICY_DIE) { + $this->output?->writeln('Auto-fix is disabled. Please fix this issue manually.'); + return false; + } + // prompt for fix + if ($this->auto_fix === FIX_POLICY_PROMPT && !confirm('Do you want to try to fix this issue now?')) { + $this->output?->writeln('You canceled fix.'); + return false; + } + // perform fix + InteractiveTerm::indicateProgress("Fixing {$result->getFixItem()} ... "); + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + // $this->output?->writeln("Fixing {$check->item_name} ... "); + if ($this->emitFix($result->getFixItem(), $result->getFixParams())) { + InteractiveTerm::finish('Fix applied successfully!'); + return true; + } + InteractiveTerm::finish('Failed to apply fix!', false); + return false; + } + + private function emitFix(string $fix_item, array $fix_item_params = []): bool + { + keyboard_interrupt_register(function () { + $this->output?->writeln('You cancelled fix'); + }); + try { + return ApplicationContext::invoke(DoctorLoader::getFixItem($fix_item), $fix_item_params); + } catch (SPCException $e) { + $this->output?->writeln('Fix failed: ' . $e->getMessage() . ''); + return false; + } catch (\Throwable $e) { + $this->output?->writeln('Fix failed with an unexpected error: ' . $e->getMessage() . ''); + return false; + } finally { + keyboard_interrupt_unregister(); + } + } + + /** + * Get a list of valid check items for current environment. + */ + private function getValidCheckList(): iterable + { + foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) { + /* @var CheckItem $item */ + // optional check + if ($optional !== null && !call_user_func($optional)) { + continue; // skip this when the optional check is false + } + // limit_os check + if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) { + continue; + } + // skipped items by env + $skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: '')); + if (in_array($item->item_name, $skip_items)) { + continue; // skip this item + } + yield $item; + } + } +} diff --git a/src/StaticPHP/Doctor/DoctorLoader.php b/src/StaticPHP/Doctor/DoctorLoader.php new file mode 100644 index 000000000..2bbbbd624 --- /dev/null +++ b/src/StaticPHP/Doctor/DoctorLoader.php @@ -0,0 +1,123 @@ + $doctor_items Loaded doctor check item instances + */ + private static array $doctor_items = []; + + /** + * @var array $fix_items loaded doctor fix item instances + */ + private static array $fix_items = []; + + /** + * Load doctor check items from PSR-4 directory. + * + * @param string $dir Directory path + * @param string $base_namespace Base namespace for dir's PSR-4 mapping + * @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload) + */ + public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void + { + $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require); + foreach ($classes as $class) { + self::loadFromClass($class, false); + } + + // sort check items by level + usort(self::$doctor_items, function ($a, $b) { + return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1); + }); + } + + /** + * Load doctor check items from a class. + * + * @param string $class Class name to load doctor check items from + * @param bool $sort Whether to re-sort Doctor items (default: true) + */ + public static function loadFromClass(string $class, bool $sort = true): void + { + // passthough to all the functions if #[OptionalCheck] is set on class level + $optional_passthrough = null; + $reflection = new \ReflectionClass($class); + $class_instance = $reflection->newInstance(); + // parse #[OptionalCheck] + $optional = $reflection->getAttributes(OptionalCheck::class)[0] ?? null; + if ($optional !== null) { + /** @var OptionalCheck $instance */ + $instance = $optional->newInstance(); + if (is_callable($instance->check)) { + $optional_passthrough = $instance->check; + } + } + + $doctor_items = []; + $fix_item_map = []; + + // finx check items and fix items from methods in class + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + // passthrough for this method if #[OptionalCheck] is set on method level + $optional = $optional_passthrough ?? null; + foreach ($method->getAttributes(OptionalCheck::class) as $method_attr) { + $optional_check = $method_attr->newInstance(); + if (is_callable($optional_check->check)) { + $optional = $optional_check->check; + } + } + + // parse #[CheckItem] + foreach ($method->getAttributes(CheckItem::class) as $attr) { + /** @var CheckItem $instance */ + $instance = $attr->newInstance(); + $instance->callback = [$class_instance, $method->getName()]; + // put CheckItem instance and optional check callback (or null) to $doctor_items + $doctor_items[] = [$instance, $optional]; + } + + // parse #[FixItem] + $fix_item = $method->getAttributes(FixItem::class)[0] ?? null; + if ($fix_item !== null) { + $instance = $fix_item->newInstance(); + $fix_item_map[$instance->name] = [$class_instance, $method->getName()]; + } + } + + // add to array + self::$doctor_items = array_merge(self::$doctor_items, $doctor_items); + self::$fix_items = array_merge(self::$fix_items, $fix_item_map); + + if ($sort) { + // sort check items by level + usort(self::$doctor_items, function ($a, $b) { + return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1); + }); + } + } + + /** + * Returns loaded doctor check items. + * + * @return array + */ + public static function getDoctorItems(): array + { + return self::$doctor_items; + } + + public static function getFixItem(string $name): ?callable + { + return self::$fix_items[$name] ?? null; + } +} diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php new file mode 100644 index 000000000..a48300e7b --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -0,0 +1,73 @@ +execWithResult("{$bison} --version", false); + if (preg_match('/bison \(GNU Bison\) (\d+)\.(\d+)(?:\.(\d+))?/', $version[1][0], $matches)) { + $major = (int) $matches[1]; + // major should be 3 or later + if ($major < 3) { + // find homebrew keg-only bison + if ($command_path !== []) { + return CheckResult::fail("Current {$bison} version is too old: " . $matches[0]); + } + return $this->checkBisonVersion(['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin']); + } + return CheckResult::ok($matches[0]); + } + return CheckResult::fail('bison version cannot be determined'); + } + + #[FixItem('brew')] + public function fixBrew(): bool + { + shell(true)->exec('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'); + return true; + } + + #[FixItem('build-tools')] + public function fixBuildTools(array $missing): bool + { + $replacement = [ + 'glibtoolize' => 'libtool', + ]; + foreach ($missing as $cmd) { + if (isset($replacement[$cmd])) { + $cmd = $replacement[$cmd]; + } + shell()->exec('brew install --formula ' . escapeshellarg($cmd)); + } + return true; + } +} diff --git a/src/StaticPHP/Doctor/Item/OSCheck.php b/src/StaticPHP/Doctor/Item/OSCheck.php new file mode 100644 index 000000000..9e1c1809c --- /dev/null +++ b/src/StaticPHP/Doctor/Item/OSCheck.php @@ -0,0 +1,23 @@ +execWithResult("{$pkgconf} --version", false); + if ($ret !== 0) { + return CheckResult::fail('pkg-config is not functional', 'install-pkg-config'); + } + return CheckResult::ok(trim($output[0])); + } + + #[FixItem('install-pkg-config')] + public function fix(): bool + { + ApplicationContext::set('elephant', true); + $installer = new PackageInstaller(['dl-prefer-binary' => true]); + $installer->addInstallPackage('pkg-config'); + $installer->run(false, true); + return true; + } +} diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php new file mode 100644 index 000000000..eb9b917fb --- /dev/null +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -0,0 +1,35 @@ += 1.0.3', limit_os: 'Linux', level: 20)] + #[CheckItem('if re2c version >= 1.0.3', limit_os: 'Darwin', level: 20)] + public function checkRe2cVersion(): ?CheckResult + { + $ver = shell(false)->execWithResult('re2c --version', false); + // match version: re2c X.X(.X) + if ($ver[0] !== 0 || !preg_match('/re2c\s+(\d+\.\d+(\.\d+)?)/', $ver[1][0], $matches)) { + return CheckResult::fail('Failed to get re2c version', 'build-re2c'); + } + $version_string = $matches[1]; + if (version_compare($version_string, '1.0.3') < 0) { + return CheckResult::fail('re2c version is too low (' . $version_string . ')', 'build-re2c'); + } + return CheckResult::ok($version_string); + } + + #[FixItem('build-re2c')] + public function buildRe2c(): bool + { + // TODO: implement re2c build process + return false; + } +} diff --git a/src/StaticPHP/Exception/BuildFailureException.php b/src/StaticPHP/Exception/BuildFailureException.php new file mode 100644 index 000000000..11ccf26b4 --- /dev/null +++ b/src/StaticPHP/Exception/BuildFailureException.php @@ -0,0 +1,13 @@ +solution; + } +} diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php new file mode 100644 index 000000000..36ed1a63c --- /dev/null +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -0,0 +1,228 @@ + Build PHP extra info binding */ + private static array $build_php_extra_info = []; + + public static function handleSPCException(SPCException $e): void + { + // XXX error: yyy + $head_msg = match ($class = get_class($e)) { + BuildFailureException::class => "✗ Build failed: {$e->getMessage()}", + DownloaderException::class => "✗ Download failed: {$e->getMessage()}", + EnvironmentException::class => "⚠ Environment check failed: {$e->getMessage()}", + ExecutionException::class => "✗ Command execution failed: {$e->getMessage()}", + FileSystemException::class => "✗ File system error: {$e->getMessage()}", + InterruptException::class => "⚠ Build interrupted by user: {$e->getMessage()}", + PatchException::class => "✗ Patch apply failed: {$e->getMessage()}", + SPCInternalException::class => "✗ SPC internal error: {$e->getMessage()}", + ValidationException::class => "⚠ Validation failed: {$e->getMessage()}", + WrongUsageException::class => $e->getMessage(), + default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}", + }; + self::logError($head_msg); + + // ---------------------------------------- + $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); + + if ($minor_logs) { + return; + } + + self::logError("----------------------------------------\n"); + + // get the SPCException module + if ($lib_info = $e->getLibraryInfo()) { + self::logError('Failed module: ' . ConsoleColor::yellow("library {$lib_info['library_name']} builder for {$lib_info['os']}")); + } elseif ($ext_info = $e->getExtensionInfo()) { + self::logError('Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder")); + } elseif (self::$builder) { + $os = match (get_class(self::$builder)) { + WindowsBuilder::class => 'Windows', + MacOSBuilder::class => 'macOS', + LinuxBuilder::class => 'Linux', + BSDBuilder::class => 'FreeBSD', + default => 'Unknown OS', + }; + self::logError('Failed module: ' . ConsoleColor::yellow("Builder for {$os}")); + } elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) { + self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); + } + + // get command execution info + if ($e instanceof ExecutionException) { + self::logError(''); + self::logError('Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand())); + if ($cd = $e->getCd()) { + self::logError('Command executed in: ' . ConsoleColor::yellow($cd)); + } + if ($env = $e->getEnv()) { + self::logError('Command inline env variables:'); + foreach ($env as $k => $v) { + self::logError(ConsoleColor::yellow("{$k}={$v}"), 4); + } + } + } + + // validation error + if ($e instanceof ValidationException) { + self::logError('Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString())); + } + + // environment error + if ($e instanceof EnvironmentException) { + self::logError('Failed environment check: ' . ConsoleColor::yellow($e->getMessage())); + if (($solution = $e->getSolution()) !== null) { + self::logError('Solution: ' . ConsoleColor::yellow($solution)); + } + } + + // get patch info + if ($e instanceof PatchException) { + self::logError("Failed patch module: {$e->getPatchModule()}"); + } + + // get internal trace + if ($e instanceof SPCInternalException) { + self::logError('Internal trace:'); + self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4); + } + + // get the full build info if possible + if ($info = ExceptionHandler::$build_php_extra_info) { + self::logError('', output_log: ApplicationContext::isDebug()); + self::logError('Build PHP extra info:', output_log: ApplicationContext::isDebug()); + self::printArrayInfo($info); + } + + // get the full builder options if possible + if ($e->getBuildPHPInfo()) { + $info = $e->getBuildPHPInfo(); + self::logError('', output_log: ApplicationContext::isDebug()); + self::logError('Builder function: ' . ConsoleColor::yellow($info['builder_function']), output_log: ApplicationContext::isDebug()); + } + + self::logError("\n----------------------------------------\n"); + + // convert log file path if in docker + $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); + $shell_log_convert = get_display_path(SPC_SHELL_LOG); + $spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR); + + self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::cyan($spc_log_convert)); + if (file_exists(SPC_SHELL_LOG)) { + self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::cyan($shell_log_convert)); + } + if ($e->getExtraLogFiles() !== []) { + foreach ($e->getExtraLogFiles() as $key => $file) { + self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::cyan("{$spc_logs_dir_convert}/{$file}")); + } + } + if (!ApplicationContext::isDebug()) { + self::logError('⚠ If you want to see more details in console, use `--debug` option.'); + } + } + + public static function handleDefaultException(\Throwable $e): void + { + $class = get_class($e); + $file = $e->getFile(); + $line = $e->getLine(); + self::logError("✗ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); + self::logError('Stack trace:'); + self::logError(ConsoleColor::gray($e->getTraceAsString()) . PHP_EOL, 4); + self::logError('⚠ Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues'); + } + + public static function bindBuilder(?BuilderBase $bind_builder): void + { + self::$builder = $bind_builder; + } + + public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void + { + self::$build_php_extra_info = $build_php_extra_info; + } + + private static function logError($message, int $indent_space = 0, bool $output_log = true): void + { + $spc_log = fopen(SPC_OUTPUT_LOG, 'a'); + $msg = explode("\n", (string) $message); + foreach ($msg as $v) { + $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); + fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + if ($output_log) { + echo ConsoleColor::red($line) . PHP_EOL; + } + } + } + + /** + * Print array info to console and log. + */ + private static function printArrayInfo(array $info): void + { + $log_output = ApplicationContext::isDebug(); + $maxlen = 0; + foreach ($info as $k => $v) { + $maxlen = max(strlen($k), $maxlen); + } + foreach ($info as $k => $v) { + if (is_string($v)) { + if ($v === '') { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('""'), 4, $log_output); + } else { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v), 4, $log_output); + } + } elseif (is_array($v) && !is_assoc_array($v)) { + if ($v === []) { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('[]'), 4, $log_output); + continue; + } + $first = array_shift($v); + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first), 4, $log_output); + foreach ($v as $vs) { + self::logError(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs), 4, $log_output); + } + } elseif (is_bool($v) || is_null($v)) { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::cyan($v === true ? 'true' : ($v === false ? 'false' : 'null')), 4, $log_output); + } else { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow(json_encode($v, JSON_PRETTY_PRINT)), 4, $log_output); + } + } + } +} diff --git a/src/StaticPHP/Exception/ExecutionException.php b/src/StaticPHP/Exception/ExecutionException.php new file mode 100644 index 000000000..3fc4c67f4 --- /dev/null +++ b/src/StaticPHP/Exception/ExecutionException.php @@ -0,0 +1,58 @@ +cmd instanceof UnixShell || $this->cmd instanceof WindowsCmd) { + return $this->cmd->getLastCommand(); + } + return $this->cmd; + } + + /** + * Returns the directory in which the command was executed. + */ + public function getCd(): ?string + { + return $this->cd; + } + + /** + * Returns the environment variables that were set during the command execution. + */ + public function getEnv(): array + { + return $this->env; + } +} diff --git a/src/StaticPHP/Exception/FileSystemException.php b/src/StaticPHP/Exception/FileSystemException.php new file mode 100644 index 000000000..b4de97f94 --- /dev/null +++ b/src/StaticPHP/Exception/FileSystemException.php @@ -0,0 +1,7 @@ +patch_module; + } +} diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php new file mode 100644 index 000000000..1859dd1ff --- /dev/null +++ b/src/StaticPHP/Exception/SPCException.php @@ -0,0 +1,147 @@ +loadStackTraceInfo(); + } + + public function bindExtensionInfo(array $extension_info): void + { + $this->extension_info = $extension_info; + } + + public function addExtraLogFile(string $key, string $filename): void + { + $this->extra_log_files[$key] = $filename; + } + + /** + * Returns an array containing information about the SPC module. + * + * This method can be overridden by subclasses to provide specific module information. + * + * @return null|array{ + * library_name: string, + * library_class: string, + * os: string, + * file: null|string, + * line: null|int, + * } an array containing module information + */ + public function getLibraryInfo(): ?array + { + return $this->library_info; + } + + /** + * Returns an array containing information about the PHP build process. + * + * @return null|array{ + * builder_function: string, + * file: null|string, + * line: null|int, + * } an array containing PHP build information + */ + public function getBuildPHPInfo(): ?array + { + return $this->build_php_info; + } + + /** + * Returns an array containing information about the SPC extension. + * + * This method can be overridden by subclasses to provide specific extension information. + * + * @return null|array{ + * extension_name: string, + * extension_class: string, + * file: null|string, + * line: null|int, + * } an array containing extension information + */ + public function getExtensionInfo(): ?array + { + return $this->extension_info; + } + + public function getExtraLogFiles(): array + { + return $this->extra_log_files; + } + + private function loadStackTraceInfo(): void + { + $trace = $this->getTrace(); + foreach ($trace as $frame) { + if (!isset($frame['class'])) { + continue; + } + + // Check if the class is a subclass of LibraryBase + if (!$this->library_info && is_a($frame['class'], LibraryBase::class, true)) { + try { + $reflection = new \ReflectionClass($frame['class']); + if ($reflection->hasConstant('NAME')) { + $name = $reflection->getConstant('NAME'); + if ($name !== 'unknown') { + $this->library_info = [ + 'library_name' => $name, + 'library_class' => $frame['class'], + 'os' => match (true) { + is_a($frame['class'], BSDLibraryBase::class, true) => 'BSD', + is_a($frame['class'], LinuxLibraryBase::class, true) => 'Linux', + is_a($frame['class'], MacOSLibraryBase::class, true) => 'macOS', + is_a($frame['class'], WindowsLibraryBase::class, true) => 'Windows', + default => 'Unknown', + }, + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + ]; + continue; + } + } + } catch (\ReflectionException) { + continue; + } + } + + // Check if the class is a subclass of BuilderBase and the method is buildPHP + if (!$this->build_php_info && is_a($frame['class'], BuilderBase::class, true)) { + $this->build_php_info = [ + 'builder_function' => $frame['function'], + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + ]; + } + } + } +} diff --git a/src/StaticPHP/Exception/SPCInternalException.php b/src/StaticPHP/Exception/SPCInternalException.php new file mode 100644 index 000000000..bb468920a --- /dev/null +++ b/src/StaticPHP/Exception/SPCInternalException.php @@ -0,0 +1,12 @@ +getTrace() as $trace) { + // Extension validate() => "Extension validator" + if (is_a($trace['class'] ?? null, Extension::class, true) && $trace['function'] === 'validate') { + $this->validation_module = 'Extension validator'; + break; + } + + // Other => "ClassName::functionName" + $this->validation_module = [ + 'class' => $trace['class'] ?? null, + 'function' => $trace['function'], + ]; + break; + } + } else { + $this->validation_module = $validation_module; + } + } + + /** + * Returns the validation module string. + */ + public function getValidationModuleString(): string + { + if ($this->validation_module === null) { + return 'Unknown'; + } + if (is_string($this->validation_module)) { + return $this->validation_module; + } + $str = $this->validation_module['class'] ?? null; + if ($str !== null) { + $str .= '::'; + } + return ($str ?? '') . $this->validation_module['function']; + } +} diff --git a/src/StaticPHP/Exception/WrongUsageException.php b/src/StaticPHP/Exception/WrongUsageException.php new file mode 100644 index 000000000..2044a82c0 --- /dev/null +++ b/src/StaticPHP/Exception/WrongUsageException.php @@ -0,0 +1,13 @@ + $build_functions Build functions for different OS binding */ + protected array $build_functions = []; + + /** + * Add a build function for a specific platform. + * + * @param string $platform PHP_OS_FAMILY + * @param callable $func Function to build for the platform + */ + public function addBuildFunction(string $platform, callable $func): void + { + $this->build_functions[$platform] = $func; + if ($platform === PHP_OS_FAMILY) { + $this->addStage('build', $func); + } + } + + public function isInstalled(): bool + { + foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) { + if (!file_exists("{$this->getLibDir()}/{$lib}")) { + return false; + } + } + foreach (PackageConfig::get($this->getName(), 'headers', []) as $header) { + if (!file_exists("{$this->getIncludeDir()}/{$header}")) { + return false; + } + } + foreach (PackageConfig::get($this->getName(), 'pkg-configs', []) as $pc) { + if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}.pc")) { + return false; + } + } + foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { + if (!file_exists("{$this->getBinDir()}/{$bin}")) { + return false; + } + } + return true; + } + + public function patchLaDependencyPrefix(?array $files = null): void + { + logger()->info("Patching library {$this->name} la files"); + $throwOnMissing = true; + if ($files === null) { + $files = PackageConfig::get($this->getName(), 'static-libs', []); + $files = array_map(fn ($name) => str_replace('.a', '.la', $name), $files); + $throwOnMissing = false; + } + foreach ($files as $name) { + $realpath = realpath(BUILD_LIB_PATH . '/' . $name); + if ($realpath === false) { + if ($throwOnMissing) { + throw new PatchException('la dependency patcher', "Cannot find library [{$this->name}] la file [{$name}] !"); + } + logger()->warning(message: 'Cannot find library [' . $this->name . '] la file [' . $name . '] !'); + continue; + } + logger()->debug('Patching ' . $realpath); + // replace prefix + $file = FileSystem::readFile($realpath); + $file = str_replace( + ' /lib/', + ' ' . BUILD_LIB_PATH . '/', + $file + ); + $file = preg_replace('/^libdir=.*$/m', "libdir='" . BUILD_LIB_PATH . "'", $file); + FileSystem::writeFile($realpath, $file); + } + } + + /** + * Get extra CFLAGS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_CFLAGS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_CFLAGS. + */ + public function getLibExtraCFlags(): string + { + // get environment variable + $env = getenv($this->getSnakeCaseName() . '_CFLAGS') ?: ''; + // get default c flags + $arch_c_flags = getenv('SPC_DEFAULT_C_FLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_C_FLAGS')) && !str_contains($env, $arch_c_flags)) { + $env .= ' ' . $arch_c_flags; + } + return trim($env); + } + + /** + * Get extra CXXFLAGS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_CXXFLAGS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_CXXFLAGS. + */ + public function getLibExtraCxxFlags(): string + { + // get environment variable + $env = getenv($this->getSnakeCaseName() . '_CXXFLAGS') ?: ''; + // get default cxx flags + $arch_cxx_flags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_CXX_FLAGS')) && !str_contains($env, $arch_cxx_flags)) { + $env .= ' ' . $arch_cxx_flags; + } + return trim($env); + } + + /** + * Get extra LDFLAGS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_LDFLAGS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_LDFLAGS. + */ + public function getLibExtraLdFlags(): string + { + // get environment variable + $env = getenv($this->getSnakeCaseName() . '_LDFLAGS') ?: ''; + // get default ld flags + $arch_ld_flags = getenv('SPC_DEFAULT_LD_FLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_LD_FLAGS')) && !str_contains($env, $arch_ld_flags)) { + $env .= ' ' . $arch_ld_flags; + } + return trim($env); + } + + /** + * Get extra LIBS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_LIBS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_LIBS. + */ + public function getLibExtraLibs(): string + { + return getenv($this->getSnakeCaseName() . '_LIBS') ?: ''; + } + + /** + * Get the build root path for the package. + * + * TODO: Can be changed to support per-package build root path in the future. + */ + public function getBuildRootPath(): string + { + return BUILD_ROOT_PATH; + } + + /** + * Get the include directory for the package. + * + * TODO: Can be changed to support per-package include directory in the future. + */ + public function getIncludeDir(): string + { + return BUILD_INCLUDE_PATH; + } + + /** + * Get the library directory for the package. + * + * TODO: Can be changed to support per-package library directory in the future. + */ + public function getLibDir(): string + { + return BUILD_LIB_PATH; + } + + public function getBinDir(): string + { + return BUILD_BIN_PATH; + } +} diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php new file mode 100644 index 000000000..263a88821 --- /dev/null +++ b/src/StaticPHP/Package/Package.php @@ -0,0 +1,162 @@ + $stages Defined stages for the package + */ + protected array $stages = []; + + /** + * @param string $name Name of the package + * @param string $type Type of the package + */ + public function __construct(public readonly string $name, public readonly string $type) {} + + /** + * Run a defined stage of the package. + * If the stage is not defined, an exception should be thrown. + * + * @param string $name Name of the stage to run + * @param array $context Additional context to pass to the stage callback + * @return mixed Based on the stage definition, return the result of the stage + */ + public function runStage(string $name, array $context = []): mixed + { + if (!isset($this->stages[$name])) { + throw new SPCInternalException("Stage '{$name}' is not defined for package '{$this->name}'."); + } + + // Merge package context with provided context + /** @noinspection PhpDuplicateArrayKeysInspection */ + $stageContext = array_merge([ + Package::class => $this, + static::class => $this, + ], $context); + + // emit BeforeStage + $this->emitBeforeStage($name, $stageContext); + + $ret = ApplicationContext::invoke($this->stages[$name], $stageContext); + // emit AfterStage + $this->emitAfterStage($name, $stageContext, $ret); + return $ret; + } + + public function isInstalled(): bool + { + // By default, assume package is not installed. + return false; + } + + /** + * Add a stage to the package. + * + * @param string $name Stage name + * @param callable $stage Stage callable + */ + public function addStage(string $name, callable $stage): void + { + $this->stages[$name] = $stage; + } + + /** + * Check if the package has a specific stage defined. + * + * @param string $name Stage name + */ + public function hasStage(string $name): bool + { + return isset($this->stages[$name]); + } + + /** + * Get the name of the package. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the type of the package. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get the artifact associated with the package, or null if none is defined. + * + * @return null|Artifact Artifact instance or null + */ + public function getArtifact(): ?Artifact + { + // find config + $artifact_name = PackageConfig::get($this->name, 'artifact'); + return $artifact_name !== null ? ArtifactLoader::getArtifactInstance($artifact_name) : null; + } + + /** + * Check if the artifact has source available. + */ + public function hasSource(): bool + { + return $this->getArtifact()?->hasSource() ?? false; + } + + /** + * Get source directory of the package. + * If the source artifact is not available, an exception will be thrown. + */ + public function getSourceDir(): string + { + if (($artifact = $this->getArtifact()) && $artifact->hasSource()) { + return $artifact->getSourceDir(); + } + throw new SPCInternalException("Source directory for package {$this->name} is not available because the source artifact is missing."); + } + + /** + * Check if the package has a binary available for current OS and architecture. + */ + public function hasLocalBinary(): bool + { + return $this->getArtifact()?->hasPlatformBinary() ?? false; + } + + /** + * Get the snake_case name of the package. + */ + protected function getSnakeCaseName(): string + { + return str_replace('-', '_', $this->name); + } + + private function emitBeforeStage(string $stage, array $stageContext): void + { + foreach (PackageLoader::getBeforeStageCallbacks($this->getName(), $stage) as $callback) { + ApplicationContext::invoke($callback, $stageContext); + } + } + + private function emitAfterStage(string $stage, array $stageContext, mixed $return_value): void + { + foreach (PackageLoader::getAfterStageCallbacks($this->getName(), $stage) as $callback) { + ApplicationContext::invoke($callback, array_merge($stageContext, ['return_value' => $return_value])); + } + } +} diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php new file mode 100644 index 000000000..b40888722 --- /dev/null +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -0,0 +1,104 @@ +concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1; + } + + public function buildPackage(Package $package, bool $force = false): int + { + // init build dirs + if (!$package instanceof LibraryPackage) { + throw new SPCInternalException('Please, never try to build non-library packages directly.'); + } + FileSystem::createDir($package->getBuildRootPath()); + FileSystem::createDir($package->getIncludeDir()); + FileSystem::createDir($package->getBinDir()); + FileSystem::createDir($package->getLibDir()); + + if (!$package->hasStage('build')) { + throw new WrongUsageException("Package '{$package->name}' does not have a current platform 'build' stage defined."); + } + + // validate package should be built + if (!$force) { + return $package->isInstalled() ? SPC_STATUS_ALREADY_BUILT : $this->buildPackage($package, true); + } + // check source is ready + if ($package->getType() !== 'virtual-target' && !is_dir($package->getSourceDir())) { + throw new WrongUsageException("Source directory for package '{$package->name}' does not exist. Please fetch the source before building."); + } + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + + if ($package->getType() !== 'virtual-target') { + // patch before build + $package->patchBeforeBuild(); + } + + // build + $package->runStage('build'); + + if ($package->getType() !== 'virtual-target') { + // install license + if (($license = PackageConfig::get($package->getName(), 'license')) !== null) { + $this->installLicense($package, $license); + } + } + return SPC_STATUS_BUILT; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getOption(string $key, mixed $default = null): mixed + { + return $this->options[$key] ?? $default; + } + + private function installLicense(Package $package, array $license): void + { + $dir = BUILD_ROOT_PATH . '/source-licenses/' . $package->getName(); + FileSystem::createDir($dir); + if (is_assoc_array($license)) { + $license = [$license]; + } + + foreach ($license as $index => $item) { + if ($item['type'] === 'text') { + FileSystem::writeFile("{$dir}/{$index}.txt", $item['text']); + } elseif ($item['type'] === 'file') { + FileSystem::copy("{$package->getSourceDir()}/{$item['path']}", "{$dir}/{$index}.txt"); + } + } + } +} diff --git a/src/StaticPHP/Package/PackageCallbacksTrait.php b/src/StaticPHP/Package/PackageCallbacksTrait.php new file mode 100644 index 000000000..96ad039ee --- /dev/null +++ b/src/StaticPHP/Package/PackageCallbacksTrait.php @@ -0,0 +1,89 @@ +info_callback = $callback; + } + + /** + * Get package info by invoking the info callback. + * + * @return array Package information + */ + public function getPackageInfo(): array + { + if ($this->info_callback === null) { + return []; + } + + // Use CallbackInvoker with current package as context + $result = ApplicationContext::invoke($this->info_callback, [ + Package::class => $this, + static::class => $this, + ]); + + return is_array($result) ? $result : []; + } + + public function setValidateCallback(callable $callback): void + { + $this->validate_callback = $callback; + } + + public function setPatchBeforeBuildCallback(callable $callback): void + { + $this->patch_before_build_callback = $callback; + } + + public function patchBeforeBuild(): void + { + if (file_exists("{$this->getSourceDir()}/.spc-patched")) { + return; + } + if ($this->patch_before_build_callback === null) { + return; + } + // Use CallbackInvoker with current package as context + $ret = ApplicationContext::invoke($this->patch_before_build_callback, [ + Package::class => $this, + static::class => $this, + ]); + if ($ret === true) { + FileSystem::writeFile("{$this->getSourceDir()}/.spc-patched", 'PATCHED!!!'); + } + } + + /** + * Validate the package by invoking the validate callback. + */ + public function validatePackage(): void + { + if ($this->validate_callback === null) { + return; + } + + // Use CallbackInvoker with current package as context + ApplicationContext::invoke($this->validate_callback, [ + Package::class => $this, + static::class => $this, + ]); + } +} diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php new file mode 100644 index 000000000..15bab0dc9 --- /dev/null +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -0,0 +1,534 @@ + Resolved package list */ + protected array $packages = []; + + /** @var array Packages to be built from source */ + protected array $build_packages = []; + + /** @var array Packages to be installed */ + protected array $install_packages = []; + + /** @var array> Unresolved target additional dependencies defined in #[ResolveBuild] */ + protected array $target_additional_dependencies = []; + + /** @var bool Whether to download missing sources automatically */ + protected bool $download = true; + + public function __construct(protected array $options = []) + { + ApplicationContext::set(PackageInstaller::class, $this); + $builder = new PackageBuilder($options); + ApplicationContext::set(PackageBuilder::class, $builder); + ApplicationContext::set('patch_point', ''); + + // Check for no-download option + if (!empty($options['no-download'])) { + $this->download = false; + } + } + + /** + * Add a package to the build list. + * This means the package will be built from source. + */ + public function addBuildPackage(LibraryPackage|string|TargetPackage $package): static + { + if (is_string($package)) { + $package = PackageLoader::getPackage($package); + } + // special check for php target packages + if (in_array($package->getName(), ['php', 'php-cli', 'php-fpm', 'php-micro', 'php-cgi', 'php-embed', 'frankenphp'], true)) { + $this->handlePhpTargetPackage($package); + return $this; + } + if (!$package->hasStage('build')) { + throw new WrongUsageException("Target package '{$package->getName()}' does not define build process for current OS: " . PHP_OS_FAMILY . '.'); + } + $this->build_packages[$package->getName()] = $package; + return $this; + } + + /** + * @param string $name Package name + * @return null|Package The build package instance or null if not found + */ + public function getBuildPackage(string $name): ?Package + { + return $this->build_packages[$name] ?? null; + } + + /** + * Add a package to the installation list. + * This means the package will try to install binary artifacts first. + * If no artifacts found, it will fallback to build from source. + */ + public function addInstallPackage(LibraryPackage|string $package): static + { + if (is_string($package)) { + $package = PackageLoader::getPackage($package); + } + $this->install_packages[$package->getName()] = $package; + return $this; + } + + /** + * Set whether to download packages before installation. + */ + public function setDownload(bool $download = true): static + { + $this->download = $download; + return $this; + } + + /** + * Run the package installation process. + */ + public function run(bool $interactive = true, bool $disable_delay_msg = false): void + { + // resolve input, make dependency graph + $this->resolvePackages(); + + if ($interactive && !$disable_delay_msg) { + // show install or build options in terminal with beautiful output + $this->printInstallerInfo(); + + InteractiveTerm::notice('Build process will start after 2s ...'); + sleep(2); + echo PHP_EOL; + } + + // check download + if ($this->download) { + $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); + $downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]); + $downloader->addArtifacts($this->getArtifacts())->download($interactive); + } else { + logger()->notice('Skipping download (--no-download option enabled)'); + } + + // extract sources + $this->extractSourceArtifacts(interactive: $interactive); + + // validate packages + foreach ($this->packages as $package) { + // 1. call validate package + $package->validatePackage(); + } + + // build/install packages + if ($interactive) { + InteractiveTerm::notice('Building/Installing packages ...'); + keyboard_interrupt_register(function () { + InteractiveTerm::finish('Build/Install process interrupted by user!', false); + exit(130); + }); + } + $builder = ApplicationContext::get(PackageBuilder::class); + foreach ($this->packages as $package) { + if ($this->isBuildPackage($package) || $package instanceof LibraryPackage && $package->hasStage('build')) { + if ($interactive) { + InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); + } + try { + /** @var LibraryPackage $package */ + $status = $builder->buildPackage($package, $this->isBuildPackage($package)); + } catch (\Throwable $e) { + if ($interactive) { + InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); + echo PHP_EOL; + } + throw $e; + } + if ($interactive) { + InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); + } + } elseif ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) { + // install binary + if ($interactive) { + InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); + } + try { + $status = $this->installBinary($package); + } catch (\Throwable $e) { + if ($interactive) { + InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); + echo PHP_EOL; + } + throw $e; + } + if ($interactive) { + InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); + } + } elseif ($package instanceof LibraryPackage) { + throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS."); + } + } + } + + public function isBuildPackage(Package|string $package): bool + { + return isset($this->build_packages[is_string($package) ? $package : $package->getName()]); + } + + /** + * Get all resolved packages. + * + * @return array + */ + public function getResolvedPackages(): array + { + return $this->packages; + } + + public function isPackageBeingResolved(string $package_name): bool + { + return isset($this->packages[$package_name]); + } + + /** + * Returns the download status of all artifacts for the resolved packages. + * + * @return array artifact name => [source downloaded, binary downloaded] + */ + public function getArtifactDownloadStatus(): array + { + $download_status = []; + foreach ($this->getResolvedPackages() as $package) { + if (($artifact = $package->getArtifact()) !== null && !isset($download_status[$artifact->getName()])) { + // [0: source, 1: binary for current OS] + $download_status[$artifact->getName()] = [ + 'source-downloaded' => $artifact->isSourceDownloaded(), + 'binary-downloaded' => $artifact->isBinaryDownloaded(), + 'has-source' => $artifact->hasSource(), + 'has-binary' => $artifact->hasPlatformBinary(), + ]; + $download_status[$artifact->getName()] = [$artifact->isSourceDownloaded(), $artifact->isBinaryDownloaded()]; + } + } + return $download_status; + } + + /** + * Get all artifacts from resolved and build packages. + * + * @return Artifact[] + */ + public function getArtifacts(): array + { + $artifacts = []; + foreach ($this->getResolvedPackages() as $package) { + // Validate package artifacts + $this->validatePackageArtifact($package); + if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) { + $artifacts[] = $artifact; + } + } + // add target artifacts + foreach ($this->build_packages as $package) { + // Validate package artifacts + $this->validatePackageArtifact($package); + if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) { + $artifacts[] = $artifact; + } + } + return $artifacts; + } + + /** + * Extract all artifacts for resolved packages. + */ + public function extractSourceArtifacts(bool $interactive = true): void + { + $packages = array_values($this->packages); + + $cache = ApplicationContext::get(ArtifactCache::class); + $extractor = new ArtifactExtractor($cache); + + // Collect all unique artifacts + $artifacts = []; + $pkg_artifact_map = []; + foreach ($packages as $package) { + $artifact = $package->getArtifact(); + if ($artifact !== null && !isset($artifacts[$artifact->getName()]) && (!$artifact->shouldUseBinary() || $this->isBuildPackage($package))) { + $pkg_artifact_map[$package->getName()] = $artifact->getName(); + $artifacts[$artifact->getName()] = $artifact; + } + } + + // Sort: php-src should be extracted first (extensions depend on it) + uksort($artifacts, function (string $a, string $b): int { + if ($a === 'php-src') { + return -1; + } + if ($b === 'php-src') { + return 1; + } + return 0; + }); + + if (count($artifacts) === 0) { + return; + } + + // Extract each artifact + if ($interactive) { + InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...'); + InteractiveTerm::indicateProgress('Extracting artifacts'); + } + + try { + V2CompatLayer::beforeExtsExtractHook(); + foreach ($artifacts as $artifact) { + if ($interactive) { + InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName())); + } + if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { + V2CompatLayer::beforeLibExtractHook($pkg); + } + $extractor->extract($artifact, true); + if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { + V2CompatLayer::afterLibExtractHook($pkg); + } + } + V2CompatLayer::afterExtsExtractHook(); + if ($interactive) { + InteractiveTerm::finish('Extracted all sources successfully.'); + echo PHP_EOL; + } + } catch (\Throwable $e) { + if ($interactive) { + InteractiveTerm::finish('Artifact extraction failed!', false); + echo PHP_EOL; + } + throw $e; + } + } + + public function installBinary(Package $package): int + { + $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); + $artifact = $package->getArtifact(); + if ($artifact === null || !$artifact->shouldUseBinary()) { + throw new WrongUsageException("Package '{$package->getName()}' does not have a binary artifact to install."); + } + + $status = $extractor->extract($artifact); + if ($status === SPC_STATUS_ALREADY_EXTRACTED) { + return SPC_STATUS_ALREADY_INSTALLED; + } + + // perform package after-install actions + $this->performAfterInstallActions($package); + return SPC_STATUS_INSTALLED; + } + + public function getPackage(string $package_name): ?Package + { + return $this->packages[$package_name] ?? null; + } + + /** + * Validate that a package has required artifacts. + * + * @throws WrongUsageException if target/library package has no source or platform binary + */ + private function validatePackageArtifact(Package $package): void + { + // target and library must have at least source or platform binary + if (in_array($package->getType(), ['library', 'target']) && !$package->getArtifact()?->hasSource() && !$package->getArtifact()?->hasPlatformBinary()) { + throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or platform binary defined."); + } + } + + private function resolvePackages(): void + { + $pkgs = []; + + foreach ($this->build_packages as $package) { + // call #[ResolveBuild] annotation methods if defined + if ($package instanceof TargetPackage && is_array($deps = $package->_emitResolveBuild($this))) { + $this->target_additional_dependencies[$package->getName()] = $deps; + } + $pkgs[] = $package->getName(); + } + + // gather install packages + foreach ($this->install_packages as $package) { + $pkgs[] = $package->getName(); + } + + // resolve dependencies string + $resolved_packages = DependencyResolver::resolve( + $pkgs, + $this->target_additional_dependencies, + $this->options['with-suggests'] ?? false + ); + + foreach ($resolved_packages as $pkg_name) { + $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name); + } + } + + private function handlePhpTargetPackage(TargetPackage $package): void + { + // process 'php' target + if ($package->getName() === 'php') { + logger()->warning("Building 'php' target is deprecated, please use specific targets like 'build:php-cli' instead."); + + $added = false; + + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) { + $cli = PackageLoader::getPackage('php-cli'); + $this->build_packages[$cli->getName()] = $cli; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) { + $fpm = PackageLoader::getPackage('php-fpm'); + $this->build_packages[$fpm->getName()] = $fpm; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) { + $micro = PackageLoader::getPackage('php-micro'); + $this->build_packages[$micro->getName()] = $micro; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) { + $cgi = PackageLoader::getPackage('php-cgi'); + $this->build_packages[$cgi->getName()] = $cgi; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) { + $embed = PackageLoader::getPackage('php-embed'); + $this->build_packages[$embed->getName()] = $embed; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { + $frankenphp = PackageLoader::getPackage('frankenphp'); + $this->build_packages[$frankenphp->getName()] = $frankenphp; + $added = true; + } + $this->build_packages[$package->getName()] = $package; + + if (!$added) { + throw new WrongUsageException( + "No SAPI target specified to build. Please use '--build-cli', '--build-fpm', '--build-micro', " . + "'--build-cgi', '--build-embed', '--build-frankenphp' or '--build-all' options." + ); + } + } else { + // process specific php sapi targets + $this->build_packages['php'] = PackageLoader::getPackage('php'); + $this->build_packages[$package->getName()] = $package; + } + } + + private function printInstallerInfo(): void + { + InteractiveTerm::notice('Installation summary:'); + $summary['Packages to be built'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->build_packages))); + $summary['Packages to be installed'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->packages))); + $summary['Artifacts to be downloaded'] = implode(',', array_map(fn ($x) => $x->getName(), $this->getArtifacts())); + $this->printArrayInfo(array_filter($summary)); + echo PHP_EOL; + + foreach ($this->build_packages as $package) { + $info = $package->getPackageInfo(); + if ($info === []) { + continue; + } + InteractiveTerm::notice("{$package->getName()} build options:"); + // calculate space count for every line + $this->printArrayInfo($info); + echo PHP_EOL; + } + } + + private function printArrayInfo(array $info): void + { + $maxlen = 0; + foreach ($info as $k => $v) { + $maxlen = max(strlen($k), $maxlen); + } + foreach ($info as $k => $v) { + if (is_string($v)) { + InteractiveTerm::plain(" {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v)); + } elseif (is_array($v) && !is_assoc_array($v)) { + $first = array_shift($v); + InteractiveTerm::plain(" {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first)); + foreach ($v as $vs) { + InteractiveTerm::plain(str_pad('', $maxlen + 4) . ConsoleColor::yellow($vs)); + } + } + } + } + + private function performAfterInstallActions(Package $package): void + { + // ----------- perform post-install actions from extracted .package.{pkg_name}.postinstall.json ----------- + $root_dir = ($package->getArtifact()?->getBinaryDir() ?? '') !== '' ? $package->getArtifact()?->getBinaryDir() : null; + if ($root_dir !== null) { + $action_json = "{$root_dir}/.package.{$package->getName()}.postinstall.json"; + if (is_file($action_json)) { + $action_json = json_decode(file_get_contents($action_json), true); + if (!is_array($action_json)) { + throw new WrongUsageException("Invalid post-install action JSON format for package '{$package->getName()}'."); + } + $placeholders = get_pack_replace(); + foreach ($action_json as $action) { + $action_name = $action['action'] ?? ''; + switch ($action_name) { + // replace-path: => files: [relative_path1, relative_path2] + case 'replace-path': + $files = $action['files'] ?? []; + foreach ($files as $file) { + $filepath = $root_dir . "/{$file}"; + FileSystem::replaceFileStr($filepath, array_values($placeholders), array_keys($placeholders)); + } + break; + // replace-to-env: => file: "relative_path", search: "SEARCH_STR", replace-env: "ENV_VAR_NAME" + case 'replace-to-env': + $file = $action['file'] ?? ''; + $search = $action['search'] ?? ''; + $env_var = $action['replace-env'] ?? ''; + $replace = getenv($env_var) ?: ''; + $filepath = $root_dir . "/{$file}"; + FileSystem::replaceFileStr($filepath, $search, $replace); + break; + default: + throw new WrongUsageException("Unknown post-install action '{$action_name}' for package '{$package->getName()}'."); + } + } + // remove the action file after processing + unlink($root_dir . "/.package.{$package->getName()}.postinstall.json"); + } + } + } +} diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Package/PackageLoader.php new file mode 100644 index 000000000..cdbb21962 --- /dev/null +++ b/src/StaticPHP/Package/PackageLoader.php @@ -0,0 +1,280 @@ + */ + private static ?array $packages = null; + + private static array $before_stages = []; + + private static array $after_stage = []; + + private static array $patch_before_builds = []; + + /** @var array Track loaded classes to prevent duplicates */ + private static array $loaded_classes = []; + + public static function initPackageInstances(): void + { + if (self::$packages !== null) { + return; + } + // init packages instance from config + foreach (PackageConfig::getAll() as $name => $item) { + $pkg = match ($item['type']) { + 'target', 'virtual-target' => new TargetPackage($name, $item['type']), + 'library' => new LibraryPackage($name, $item['type']), + 'php-extension' => new PhpExtensionPackage($name, $item['type']), + default => null, + }; + if ($pkg !== null) { + self::$packages[$name] = $pkg; + } else { + throw new WrongUsageException("Package [{$name}] has unknown type [{$item['type']}]"); + } + } + } + + /** + * Load package definitions from PSR-4 directory. + * + * @param string $dir Directory path + * @param string $base_namespace Base namespace for dir's PSR-4 mapping + * @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload) + */ + public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void + { + self::initPackageInstances(); + $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require); + foreach ($classes as $class) { + self::loadFromClass($class); + } + } + + public static function hasPackage(string $name): bool + { + return isset(self::$packages[$name]); + } + + /** + * Get a Package instance by its name. + * + * @param string $name The name of the package + * @return Package Returns the Package instance if found, otherwise null + */ + public static function getPackage(string $name): Package + { + if (!isset(self::$packages[$name])) { + throw new WrongUsageException("Package [{$name}] not found."); + } + return self::$packages[$name]; + } + + public static function getTargetPackage(string $name): TargetPackage + { + $pkg = self::getPackage($name); + if ($pkg instanceof TargetPackage) { + return $pkg; + } + throw new WrongUsageException("Package [{$name}] is not a TargetPackage."); + } + + public static function getLibraryPackage(string $name): LibraryPackage + { + $pkg = self::getPackage($name); + if ($pkg instanceof LibraryPackage) { + return $pkg; + } + throw new WrongUsageException("Package [{$name}] is not a LibraryPackage."); + } + + /** + * Get all loaded Package instances. + */ + public static function getPackages(array|string|null $type_filter = null): iterable + { + foreach (self::$packages as $name => $package) { + if ($type_filter === null) { + yield $name => $package; + } elseif ($package->getType() === $type_filter) { + yield $name => $package; + } elseif (is_array($type_filter) && in_array($package->getType(), $type_filter, true)) { + yield $name => $package; + } + } + } + + /** + * Init package instance from defined classes and attributes. + * + * @internal + */ + public static function loadFromClass(mixed $class): void + { + $refClass = new \ReflectionClass($class); + $class_name = $refClass->getName(); + + // Skip if already loaded to prevent duplicate registrations + if (isset(self::$loaded_classes[$class_name])) { + return; + } + self::$loaded_classes[$class_name] = true; + + $instance_class = $refClass->newInstance(); + + $attributes = $refClass->getAttributes(); + foreach ($attributes as $attribute) { + $pkg = null; + + $attribute_instance = $attribute->newInstance(); + if ($attribute_instance instanceof Target === false && + $attribute_instance instanceof Library === false && + $attribute_instance instanceof Extension === false) { + // not a package attribute + continue; + } + $package_type = PackageConfig::get($attribute_instance->name, 'type'); + if ($package_type === null) { + throw new WrongUsageException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); + } + $pkg = self::$packages[$attribute_instance->name]; + + // validate package type matches + $pkg_type_attr = match ($attribute->getName()) { + Target::class => ['target', 'virtual-target'], + Library::class => ['library'], + Extension::class => ['php-extension'], + default => null, + }; + if (!in_array($package_type, $pkg_type_attr, true)) { + throw new ValidationException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].'); + } + if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) { + throw new ValidationException("Package [{$pkg->getName()}] config not found for class {$class}"); + } + + // init method attributes + $methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC); + foreach ($methods as $method) { + $method_attributes = $method->getAttributes(); + foreach ($method_attributes as $method_attribute) { + $method_instance = $method_attribute->newInstance(); + match ($method_attribute->getName()) { + // #[BuildFor(PHP_OS_FAMILY)] + BuildFor::class => self::addBuildFunction($pkg, $method_instance, [$instance_class, $method->getName()]), + // #[CustomPhpConfigureArg(PHP_OS_FAMILY)] + CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]), + // #[Stage('stage_name')] + Stage::class => $pkg->addStage($method_attribute->newInstance()->name, [$instance_class, $method->getName()]), + // #[InitPackage] (run now with package context) + InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [ + Package::class => $pkg, + $pkg::class => $pkg, + ]), + // #[InitBuild] + ResolveBuild::class => $pkg instanceof TargetPackage ? $pkg->setResolveBuildCallback([$instance_class, $method->getName()]) : null, + // #[Info] + Info::class => $pkg->setInfoCallback([$instance_class, $method->getName()]), + // #[Validate] + Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]), + // #[PatchBeforeBuild] + PatchBeforeBuild::class => $pkg->setPatchBeforeBuildCallback([$instance_class, $method->getName()]), + default => null, + }; + } + } + // register package + self::$packages[$pkg->getName()] = $pkg; + } + + // parse non-package available attributes + foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + $method_attributes = $method->getAttributes(); + foreach ($method_attributes as $method_attribute) { + $method_instance = $method_attribute->newInstance(); + match ($method_attribute->getName()) { + // #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')] + BeforeStage::class => self::$before_stages[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved], + AfterStage::class => self::$after_stage[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved], + // #[PatchBeforeBuild() + default => null, + }; + } + } + } + + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable + { + // match condition + $installer = ApplicationContext::get(PackageInstaller::class); + $stages = self::$before_stages[$package_name][$stage] ?? []; + foreach ($stages as [$callback, $only_when_package_resolved]) { + if ($only_when_package_resolved !== null && !$installer->isPackageBeingResolved($only_when_package_resolved)) { + continue; + } + yield $callback; + } + } + + public static function getAfterStageCallbacks(string $package_name, string $stage): array + { + // match condition + $installer = ApplicationContext::get(PackageInstaller::class); + $stages = self::$after_stage[$package_name][$stage] ?? []; + $result = []; + foreach ($stages as [$callback, $only_when_package_resolved]) { + if ($only_when_package_resolved !== null && !$installer->isPackageBeingResolved($only_when_package_resolved)) { + continue; + } + $result[] = $callback; + } + return $result; + } + + public static function getPatchBeforeBuildCallbacks(string $package_name): array + { + return self::$patch_before_builds[$package_name] ?? []; + } + + /** + * Bind a custom PHP configure argument callback to a php-extension package. + */ + private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void + { + if (!$pkg instanceof PhpExtensionPackage) { + throw new ValidationException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute."); + } + $pkg->addCustomPhpConfigureArgCallback($attr->os, $fn); + } + + private static function addBuildFunction(Package $pkg, object $attr, callable $fn): void + { + if (!$pkg instanceof LibraryPackage) { + throw new ValidationException("Class [{$pkg->getName()}] must implement LibraryPackage for BuildFor attribute."); + } + $pkg->addBuildFunction($attr->os, $fn); + } +} diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php new file mode 100644 index 000000000..673d1c82f --- /dev/null +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -0,0 +1,110 @@ + Callbacks for custom PHP configure arguments per OS + */ + protected array $custom_php_configure_arg_callbacks = []; + + protected bool $build_shared = false; + + protected bool $build_static = false; + + protected bool $build_with_php = false; + + /** + * @param string $name Name of the php extension + * @param string $type Type of the package, defaults to 'php-extension' + */ + public function __construct(string $name, string $type = 'php-extension', protected array $extension_config = []) + { + // Ensure the package name starts with 'ext-' + if (!str_starts_with($name, 'ext-')) { + $name = "ext-{$name}"; + } + if ($this->extension_config === []) { + $this->extension_config = PackageConfig::get($name, 'php-extension', []); + } + parent::__construct($name, $type); + } + + public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void + { + if ($os === '') { + foreach (['Linux', 'Windows', 'Darwin'] as $supported_os) { + $this->custom_php_configure_arg_callbacks[$supported_os] = $fn; + } + } else { + $this->custom_php_configure_arg_callbacks[$os] = $fn; + } + } + + public function getPhpConfigureArg(string $os, bool $shared): string + { + if (isset($this->custom_php_configure_arg_callbacks[$os])) { + $callback = $this->custom_php_configure_arg_callbacks[$os]; + return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); + } + $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; + $name = str_replace('_', '-', substr($this->getName(), 4)); + $ext_config = PackageConfig::get($name, 'php-extension', []); + + $arg_type = match (SystemTarget::getTargetOS()) { + 'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable', + 'Darwin' => $ext_config['arg-type@macos'] ?? $ext_config['arg-type@unix'] ?? $ext_config['arg-type'] ?? 'enable', + 'Linux' => $ext_config['arg-type@linux'] ?? $ext_config['arg-type@unix'] ?? $ext_config['arg-type'] ?? 'enable', + default => $ext_config['arg-type'] ?? 'enable', + }; + + return match ($arg_type) { + 'enable' => $shared ? "--enable-{$name}=shared" : "--enable-{$name}", + 'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}", + 'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}", + 'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}", + default => throw new WrongUsageException("Unknown argument type '{$arg_type}' for PHP extension '{$name}'"), + }; + } + + public function setBuildShared(bool $build_shared = true): void + { + $this->build_shared = $build_shared; + } + + public function setBuildStatic(bool $build_static = true): void + { + $this->build_static = $build_static; + } + + public function setBuildWithPhp(bool $build_with_php = true): void + { + $this->build_with_php = $build_with_php; + } + + public function isBuildShared(): bool + { + return $this->build_shared; + } + + public function isBuildStatic(): bool + { + return $this->build_static; + } + + public function isBuildWithPhp(): bool + { + return $this->build_with_php; + } +} diff --git a/src/StaticPHP/Package/TargetPackage.php b/src/StaticPHP/Package/TargetPackage.php new file mode 100644 index 000000000..d29cf9238 --- /dev/null +++ b/src/StaticPHP/Package/TargetPackage.php @@ -0,0 +1,142 @@ + $build_options Build options for the target package + */ + protected array $build_options = []; + + protected array $build_arguments = []; + + protected mixed $resolve_build_callback = null; + + /** + * Checks if the target is virtual. + */ + public function isVirtualTarget(): bool + { + return $this->type === 'virtual-target'; + } + + /** + * Adds a build option to the target package. + * + * @param string $name The name of the build option + * @param null|string $shortcut The shortcut for the build option + * @param null|int $mode The mode of the build option + * @param string $description The description of the build option + * @param null|mixed $default The default value of the build option + */ + public function addBuildOption(string $name, ?string $shortcut = null, ?int $mode = InputOption::VALUE_NONE, string $description = '', mixed $default = null): void + { + $this->build_options[$name] = new InputOption($name, $shortcut, $mode, $description, $default); + } + + /** + * Adds a build argument to the target package. + * + * @param string $name The name of the build argument + * @param null|int $mode The mode of the build argument + * @param string $description The description of the build argument + * @param null|mixed $default The default value of the build argument + */ + public function addBuildArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null): void + { + $this->build_arguments[$name] = new InputArgument($name, $mode, $description, $default); + } + + public function setResolveBuildCallback(callable $callback): static + { + $this->resolve_build_callback = $callback; + return $this; + } + + /** + * Get a build option value for the target package. + * + * @param string $key The build option key + * @param null|mixed $default The default value if the option is not set + * @return mixed The value of the build option + */ + public function getBuildOption(string $key, mixed $default = null): mixed + { + $input = ApplicationContext::has(InputInterface::class) + ? ApplicationContext::get(InputInterface::class) + : null; + + if ($input !== null && $input->hasOption($key)) { + return $input->getOption($key); + } + return $default; + } + + /** + * Get a build argument value for the target package. + * + * @param string $key The build argument key + * @return mixed The value of the build argument + */ + public function getBuildArgument(string $key): mixed + { + $input = ApplicationContext::has(InputInterface::class) + ? ApplicationContext::get(InputInterface::class) + : null; + + if ($input !== null && $input->hasArgument($key)) { + return $input->getArgument($key); + } + return null; + } + + /** + * Gets all build options for the target package. + * + * @internal + * @return InputOption[] Get all build options for the target package + */ + public function _exportBuildOptions(): array + { + return $this->build_options; + } + + /** + * Gets all build arguments for the target package. + * + * @internal + * @return InputArgument[] Get all build arguments for the target package + */ + public function _exportBuildArguments(): array + { + return $this->build_arguments; + } + + /** + * Run the init build callback to prepare its dependencies. + * + * @internal + */ + public function _emitResolveBuild(PackageInstaller $installer): mixed + { + if (!is_callable($this->resolve_build_callback)) { + return null; + } + + return ApplicationContext::invoke($this->resolve_build_callback, [ + TargetPackage::class => $this, + PackageInstaller::class => $installer, + ]); + } +} diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php new file mode 100644 index 000000000..8e598863f --- /dev/null +++ b/src/StaticPHP/Registry/Registry.php @@ -0,0 +1,265 @@ + json_decode($yaml, true), + 'yaml', 'yml' => Yaml::parse($yaml), + default => throw new EnvironmentException("Unsupported registry file format: {$registry_file}"), + }; + if (!is_array($data)) { + throw new EnvironmentException("Invalid registry format in file: {$registry_file}"); + } + $registry_name = $data['name'] ?? null; + if (!is_string($registry_name) || empty($registry_name)) { + throw new EnvironmentException("Registry 'name' is missing or invalid in file: {$registry_file}"); + } + + // Prevent loading the same registry twice + if (in_array($registry_name, self::$loaded_registries, true)) { + logger()->debug("Registry '{$registry_name}' already loaded, skipping."); + return; + } + self::$loaded_registries[] = $registry_name; + + logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}"); + + // Load composer autoload if specified (for external registries with their own dependencies) + if (isset($data['autoload']) && is_string($data['autoload'])) { + $autoload_path = self::fullpath($data['autoload'], dirname($registry_file)); + if (file_exists($autoload_path)) { + logger()->debug("Loading external autoload from: {$autoload_path}"); + require_once $autoload_path; + } else { + logger()->warning("Autoload file not found: {$autoload_path}"); + } + } + + // load doctor items from PSR-4 directories + if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { + foreach ($data['doctor']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load doctor items from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) { + foreach ($data['doctor']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + DoctorLoader::loadFromClass($class); + } + } + + // load package configs + if (isset($data['package']['config']) && is_array($data['package']['config'])) { + foreach ($data['package']['config'] as $path) { + $path = self::fullpath($path, dirname($registry_file)); + PackageConfig::loadFromFile($path); + } + } + + // load artifact configs + if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) { + foreach ($data['artifact']['config'] as $path) { + $path = self::fullpath($path, dirname($registry_file)); + ArtifactConfig::loadFromFile($path); + } + } + + // load packages from PSR-4 directories + if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) { + foreach ($data['package']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load packages from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['package']['classes']) && is_array($data['package']['classes'])) { + foreach ($data['package']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + PackageLoader::loadFromClass($class); + } + } + + // load artifacts from PSR-4 directories + if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) { + foreach ($data['artifact']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load artifacts from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['artifact']['classes']) && is_array($data['artifact']['classes'])) { + foreach ($data['artifact']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + ArtifactLoader::loadFromClass($class); + } + } + + // load additional commands from PSR-4 directories + if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) { + foreach ($data['command']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + $classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require); + $instances = array_map(fn ($x) => new $x(), $classes); + ConsoleApplication::_addAdditionalCommands($instances); + } + } + + // load additional commands from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['command']['classes']) && is_array($data['command']['classes'])) { + $instances = []; + foreach ($data['command']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + $instances[] = new $class(); + } + ConsoleApplication::_addAdditionalCommands($instances); + } + } + + /** + * Load registries from environment variable or CLI option. + * Supports comma-separated list of registry file paths. + * + * @param null|string $registries Comma-separated registry paths, or null to read from SPC_REGISTRIES env + */ + public static function loadFromEnvOrOption(?string $registries = null): void + { + $registries ??= getenv('SPC_REGISTRIES') ?: null; + + if ($registries === null) { + return; + } + + $paths = array_filter(array_map('trim', explode(':', $registries))); + foreach ($paths as $path) { + if (!file_exists($path)) { + logger()->warning("Registry file not found: {$path}"); + continue; + } + self::loadRegistry($path); + } + } + + /** + * Get list of loaded registry names. + * + * @return string[] + */ + public static function getLoadedRegistries(): array + { + return self::$loaded_registries; + } + + /** + * Reset loaded registries (for testing). + * + * @internal + */ + public static function reset(): void + { + self::$loaded_registries = []; + } + + /** + * Parse a class entry from the classes array. + * Supports two formats: + * - Array format: ["ClassName"] where key is numeric and value is class name + * - Map format: {"ClassName": "path/to/file.php"} where key is class name and value is file path + * + * @param int|string $key Array key (numeric for array format, class name for map format) + * @param string $value Array value (class name for array format, file path for map format) + * @return array{string, ?string} [class_name, file_path or null] + */ + private static function parseClassEntry(int|string $key, string $value): array + { + if (is_int($key)) { + // Array format: ["ClassName"] - value is the class name, no file path + return [$value, null]; + } + // Map format: {"ClassName": "path/to/file.php"} - key is class name, value is file path + return [$key, $value]; + } + + /** + * Require a class file if the class doesn't exist and auto_require is enabled. + * + * @param string $class Full class name + * @param null|string $file_path File path (relative or absolute), null if not provided + * @param string $base_path Base path for relative paths + * @param bool $auto_require Whether to auto-require + */ + private static function requireClassFile(string $class, ?string $file_path, string $base_path, bool $auto_require): void + { + if (!$auto_require || class_exists($class, true)) { + return; + } + + // If file path is provided, require it + if ($file_path !== null) { + $full_path = self::fullpath($file_path, $base_path); + require_once $full_path; + return; + } + + // Class not found and no file path provided + throw new EnvironmentException( + "Class '{$class}' not found. For external registries, either:\n" . + " 1. Add an 'autoload' entry pointing to your composer autoload file\n" . + " 2. Use 'psr-4' instead of 'classes' for auto-discovery\n" . + " 3. Provide file path in classes map: \"{$class}\": \"path/to/file.php\"" + ); + } + + private static function fullpath(string $path, string $relative_path_base): string + { + if (FileSystem::isRelativePath($path)) { + $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; + } + if (!file_exists($path)) { + throw new EnvironmentException("Path does not exist: {$path}"); + } + return FileSystem::convertPath($path); + } +} diff --git a/src/StaticPHP/Runtime/Executor/Executor.php b/src/StaticPHP/Runtime/Executor/Executor.php new file mode 100644 index 000000000..a57e11e0d --- /dev/null +++ b/src/StaticPHP/Runtime/Executor/Executor.php @@ -0,0 +1,17 @@ +installer = $installer; + } elseif (ApplicationContext::has(PackageInstaller::class)) { + $this->installer = ApplicationContext::get(PackageInstaller::class); + } else { + throw new SPCInternalException('PackageInstaller not found in container'); + } + $this->initShell(); + + // 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."); + } + } + + /** + * Run ./configure + */ + public function configure(...$args): static + { + // remove all the ignored args + $args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args); + $args = array_diff($args, $this->ignore_args); + $configure_args = implode(' ', $args); + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (./configure)'); + return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}")); + } + + public function getConfigureArgsString(): string + { + return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args)); + } + + /** + * Run make + * + * @param string $target Build target + * @param false|string $with_install Run `make install` after building, or false to skip + * @param bool $with_clean Whether to clean before building + * @param array $after_env_vars Environment variables postfix + */ + public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static + { + return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) { + $shell = $this->shell; + if ($dir) { + $shell = $shell->cd($dir); + } + if ($with_clean) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make clean)'); + $shell->exec('make clean'); + } + $after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : ''; + $concurrency = ApplicationContext::get(PackageBuilder::class)->concurrency; + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make)'); + $shell->exec("make -j{$concurrency} {$target} {$after_env_vars_str}"); + if ($with_install !== false) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make ' . $with_install . ')'); + $shell->exec("make {$with_install}"); + } + return $shell; + }); + } + + public function exec(string $cmd): static + { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName())); + $this->shell->exec($cmd); + return $this; + } + + /** + * Add optional library configuration. + * This method checks if a library is available and adds the corresponding arguments to the CMake configuration. + * + * @param string $name library name to check + * @param \Closure|string $true_args arguments to use if the library is available (allow closure, returns string) + * @param string $false_args arguments to use if the library 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->shell->setEnv($env); + return $this; + } + + public function appendEnv(array $env): static + { + $this->shell->appendEnv($env); + return $this; + } + + /** + * Returns the default autoconf ./configure arguments + */ + private function getDefaultConfigureArgs(): array + { + return [ + '--disable-shared', + '--enable-static', + "--prefix={$this->package->getBuildRootPath()}", + '--with-pic', + '--enable-pic', + ]; + } + + /** + * Initialize UnixShell class. + */ + private function initShell(): void + { + $this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([ + 'CFLAGS' => "-I{$this->package->getIncludeDir()}", + 'CXXFLAGS' => "-I{$this->package->getIncludeDir()}", + 'LDFLAGS' => "-L{$this->package->getLibDir()}", + ]); + } + + /** + * When an exception occurs, this method will check if the config log file exists. + */ + private function seekLogFileOnException(mixed $callable): static + { + try { + $callable(); + return $this; + } catch (SPCException $e) { + if (file_exists("{$this->package->getSourceDir()}/config.log")) { + logger()->debug("Config log file found: {$this->package->getSourceDir()}/config.log"); + $log_file = "lib.{$this->package->getName()}.console.log"; + logger()->debug('Saved config log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library config.log", $log_file); + copy("{$this->package->getSourceDir()}/config.log", SPC_LOGS_DIR . "/{$log_file}"); + } + throw $e; + } + } +} diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php new file mode 100644 index 000000000..107aac116 --- /dev/null +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -0,0 +1,333 @@ +installer = $installer; + } elseif (ApplicationContext::has(PackageInstaller::class)) { + $this->installer = ApplicationContext::get(PackageInstaller::class); + } else { + throw new SPCInternalException('PackageInstaller not found in container'); + } + $this->initShell(); + + // 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."); + } + } + + /** + * Run cmake configure, build and install. + * + * @param string $build_pos Build position relative to build directory + */ + public function build(string $build_pos = '..'): static + { + return $this->seekLogFileOnException(function () use ($build_pos) { + // set cmake dir + $this->initBuildDir(); + + if ($this->reset) { + FileSystem::resetDir($this->build_dir); + } + + $this->shell = $this->shell->cd($this->build_dir); + + // config + 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->shell->exec("cmake {$configure_args} {$build_pos}"); + } + + // make + if ($this->steps >= 2) { + $concurrency = ApplicationContext::get(PackageBuilder::class)->concurrency; + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake build)'); + $this->shell->exec("cmake --build . -j {$concurrency}"); + } + + // install + if ($this->steps >= 3) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake install)'); + $this->shell->exec('make install'); + } + + return $this; + }); + } + + /** + * Execute a custom command. + */ + public function exec(string $cmd): static + { + $this->shell->exec($cmd); + 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->shell->setEnv($env); + return $this; + } + + public function appendEnv(array $env): static + { + $this->shell->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 ?? [ + '-DCMAKE_BUILD_TYPE=Release', + "-DCMAKE_INSTALL_PREFIX={$this->package->getBuildRootPath()}", + '-DCMAKE_INSTALL_BINDIR=bin', + '-DCMAKE_INSTALL_LIBDIR=lib', + '-DCMAKE_INSTALL_INCLUDEDIR=include', + '-DPOSITION_INDEPENDENT_CODE=ON', + '-DBUILD_SHARED_LIBS=OFF', + "-DCMAKE_TOOLCHAIN_FILE={$this->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"; + } + } + + /** + * Generate cmake toolchain file for current spc instance, and return the file path. + * + * @return string CMake toolchain file path + */ + private function makeCmakeToolchainFile(): string + { + static $created; + if (isset($created)) { + return $created; + } + $os = PHP_OS_FAMILY; + $target_arch = arch2gnu(php_uname('m')); + $cflags = getenv('SPC_DEFAULT_C_FLAGS'); + $cc = getenv('CC'); + $cxx = getenv('CXX'); + logger()->debug("making cmake tool chain file for {$os} {$target_arch} with CFLAGS='{$cflags}'"); + $root = BUILD_ROOT_PATH; + $pkgConfigExecutable = PkgConfigUtil::findPkgConfig(); + $ccLine = ''; + if ($cc) { + $ccLine = 'SET(CMAKE_C_COMPILER ' . $cc . ')'; + } + $cxxLine = ''; + if ($cxx) { + $cxxLine = 'SET(CMAKE_CXX_COMPILER ' . $cxx . ')'; + } + $toolchain = <<shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([ + 'CFLAGS' => "-I{$this->package->getIncludeDir()}", + 'CXXFLAGS' => "-I{$this->package->getIncludeDir()}", + 'LDFLAGS' => "-L{$this->package->getLibDir()}", + ]); + } + + /** + * When an exception occurs, this method will check if the cmake log file exists. + */ + private function seekLogFileOnException(mixed $callable): static + { + try { + $callable(); + return $this; + } catch (SPCException $e) { + $cmake_log = "{$this->build_dir}/CMakeFiles/CMakeError.log"; + if (file_exists($cmake_log)) { + logger()->debug("CMake error log file found: {$cmake_log}"); + $log_file = "lib.{$this->package->getName()}.cmake-error.log"; + logger()->debug('Saved CMake error log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library CMakeError.log", $log_file); + copy($cmake_log, SPC_LOGS_DIR . "/{$log_file}"); + } + $cmake_output = "{$this->build_dir}/CMakeFiles/CMakeOutput.log"; + if (file_exists($cmake_output)) { + logger()->debug("CMake output log file found: {$cmake_output}"); + $log_file = "lib.{$this->package->getName()}.cmake-output.log"; + logger()->debug('Saved CMake output log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library CMakeOutput.log", $log_file); + copy($cmake_output, SPC_LOGS_DIR . "/{$log_file}"); + } + throw $e; + } + } +} diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php new file mode 100644 index 000000000..ee6f790c1 --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -0,0 +1,191 @@ + '', + 'HEAD' => '-I', + default => "-X {$method}", + }; + $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); + $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; + $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); + $ret = $result['code']; + $output = $result['output']; + if ($ret !== 0) { + logger()->debug("[CURL ERROR] Command exited with code: {$ret}"); + } + if ($ret === 2 || $ret === -1073741510) { + throw new InterruptException(sprintf('Canceled fetching "%s"', $url)); + } + if ($ret !== 0) { + return false; + } + + return trim($output); + } + + /** + * Execute a cURL command to download a file from a URL. + */ + public function executeCurlDownload(string $url, string $path, array $headers = [], array $hooks = [], int $retries = 0): void + { + foreach ($hooks as $hook) { + $hook('GET', $url, $headers); + } + $url_arg = escapeshellarg($url); + $path_arg = escapeshellarg($path); + + $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); + $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; + $check = $this->console_putput ? '#' : 's'; + $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); + $this->logCommandInfo($cmd); + logger()->debug('[CURL DOWNLOAD] ' . $cmd); + $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + } + + /** + * Execute a Git clone command to clone a repository. + */ + public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void + { + $path = FileSystem::convertPath($path); + if (file_exists($path)) { + FileSystem::removeDir($path); + } + $git = SPC_GIT_EXEC; + $url_arg = escapeshellarg($url); + $branch_arg = escapeshellarg($branch); + $path_arg = escapeshellarg($path); + $shallow_arg = $shallow ? '--depth 1 --single-branch' : ''; + $submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : ''); + $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); + 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}"); + $this->logCommandInfo($submodule_cmd); + logger()->debug("[GIT SUBMODULE] {$submodule_cmd}"); + $this->passthru($submodule_cmd, $this->console_putput, capture_output: false, throw_on_error: true); + } + } + } + + /** + * Execute a tar command to extract an archive. + * + * @param string $archive_path Path to the archive file + * @param string $target_path Path to extract to + * @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 + { + $archive_arg = escapeshellarg(FileSystem::convertPath($archive_path)); + $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); + + $compression_flag = match ($compression) { + 'gz' => '-z', + 'bz2' => '-j', + 'xz' => '-J', + 'none' => '', + default => throw new SPCInternalException("Unknown compression type: {$compression}"), + }; + + $mute = $this->console_putput ? '' : ' 2>/dev/null'; + $cmd = "tar {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; + + $this->logCommandInfo($cmd); + logger()->debug("[TAR EXTRACT] {$cmd}"); + $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + } + + /** + * Execute an unzip command to extract a zip archive. + * + * @param string $zip_path Path to the zip file + * @param string $target_path Path to extract to + */ + public function executeUnzip(string $zip_path, string $target_path): void + { + $zip_arg = escapeshellarg(FileSystem::convertPath($zip_path)); + $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); + + $mute = $this->console_putput ? '' : ' > /dev/null'; + $cmd = "unzip {$zip_arg} -d {$target_arg}{$mute}"; + + $this->logCommandInfo($cmd); + logger()->debug("[UNZIP] {$cmd}"); + $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + } + + /** + * Execute a 7za command to extract an archive (Windows). + * + * @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 + { + $sdk_path = getenv('PHP_SDK_PATH'); + if ($sdk_path === false) { + throw new SPCInternalException('PHP_SDK_PATH environment variable is not set'); + } + + $_7z = escapeshellarg(FileSystem::convertPath($sdk_path . '/bin/7za.exe')); + $archive_arg = escapeshellarg(FileSystem::convertPath($archive_path)); + $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); + + $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}"; + } + + $this->logCommandInfo($cmd); + logger()->debug("[7Z EXTRACT] {$cmd}"); + $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + } +} diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php new file mode 100644 index 000000000..e465f66e8 --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -0,0 +1,259 @@ +console_putput = $console_output ?? ApplicationContext::isDebug(); + $this->enable_log_file = $enable_log_file; + } + + public static function passthruCallback(?callable $callback): void + { + static::$passthru_callback = $callback; + } + + /** + * Equivalent to `cd` command in shell. + * + * @param string $dir Directory to change to + */ + public function cd(string $dir): static + { + logger()->debug('Entering dir: ' . $dir); + $c = clone $this; + $c->cd = $dir; + return $c; + } + + /** + * Set temporarily defined environment variables for current shell commands. + * + * @param array $env Environment variables sets + */ + public function setEnv(array $env): static + { + foreach ($env as $k => $v) { + if (trim($v) === '') { + continue; + } + $this->env[$k] = trim($v); + } + return $this; + } + + /** + * Append temporarily defined environment variables for current shell commands. + * + * @param array $env Environment variables sets + */ + public function appendEnv(array $env): static + { + foreach ($env as $k => $v) { + if ($v === '') { + continue; + } + if (!isset($this->env[$k])) { + $this->env[$k] = $v; + } else { + $this->env[$k] = "{$v} {$this->env[$k]}"; + } + } + return $this; + } + + /** + * Executes a command in the shell. + */ + abstract public function exec(string $cmd): static; + + /** + * Returns the last executed command. + */ + public function getLastCommand(): string + { + return $this->last_cmd; + } + + /** + * Returns unix-style environment variable string. + */ + public function getEnvString(): string + { + $str = ''; + foreach ($this->env as $k => $v) { + $str .= ' ' . $k . '="' . $v . '"'; + } + return trim($str); + } + + /** + * Logs the command information to a log file. + */ + protected function logCommandInfo(string $cmd): void + { + if (!$this->enable_log_file) { + return; + } + // write executed command to log file using fwrite + $log_file = fopen(SPC_SHELL_LOG, 'a'); + fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + fwrite($log_file, "> Executing command: {$cmd}\n"); + // get the backtrace to find the file and line number + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { + $file = $backtrace[1]['file']; + $line = $backtrace[1]['line']; + fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + } + fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + if ($this->cd !== null) { + fwrite($log_file, "> Working dir: {$this->cd}\n"); + } + fwrite($log_file, "\n"); + } + + /** + * 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) { + // write executed command to the log file using fwrite + $file_res = fopen(SPC_SHELL_LOG, 'a'); + } + if ($console_output) { + $console_res = STDOUT; + } + $descriptors = [ + 0 => ['file', 'php://stdin', 'r'], // stdin + 1 => ['pipe', 'w'], // stdout + 2 => ['pipe', 'w'], // stderr + ]; + $process = proc_open($cmd, $descriptors, $pipes); + + $output_value = ''; + try { + if (!is_resource($process)) { + throw new ExecutionException( + cmd: $original_command ?? $cmd, + message: 'Failed to open process for command, proc_open() failed.', + code: -1, + cd: $this->cd, + env: $this->env + ); + } + // fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + while (true) { + $status = proc_get_status($process); + if (!$status['running']) { + foreach ([$pipes[1], $pipes[2]] as $pipe) { + while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { + if ($console_output) { + fwrite($console_res, $chunk); + } + if ($file_res !== null) { + fwrite($file_res, $chunk); + } + if ($capture_output) { + $output_value .= $chunk; + } + } + } + // check exit code + if ($throw_on_error && $status['exitcode'] !== 0) { + if ($file_res !== null) { + fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + } + throw new ExecutionException( + cmd: $original_command ?? $cmd, + message: "Command exited with non-zero code: {$status['exitcode']}", + code: $status['exitcode'], + cd: $this->cd, + env: $this->env, + ); + } + break; + } + + if (static::$passthru_callback !== null) { + $callback = static::$passthru_callback; + $callback(); + } + $read = [$pipes[1], $pipes[2]]; + $write = null; + $except = null; + + $ready = stream_select($read, $write, $except, 0, 100000); + + if ($ready === false) { + continue; + } + + if ($ready > 0) { + foreach ($read as $pipe) { + while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { + if ($console_output) { + fwrite($console_res, $chunk); + } + if ($file_res !== null) { + fwrite($file_res, $chunk); + } + if ($capture_output) { + $output_value .= $chunk; + } + } + } + } + } + + return [ + 'code' => $status['exitcode'], + 'output' => $output_value, + ]; + } finally { + fclose($pipes[1]); + fclose($pipes[2]); + if ($file_res !== null) { + fclose($file_res); + } + proc_close($process); + } + } +} diff --git a/src/StaticPHP/Runtime/Shell/UnixShell.php b/src/StaticPHP/Runtime/Shell/UnixShell.php new file mode 100644 index 000000000..7690f247a --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/UnixShell.php @@ -0,0 +1,91 @@ +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + $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); + return $this; + } + + /** + * Init the environment variable that common build will be used. + * + * @param LibraryPackage $library Library package + */ + public function initializeEnv(LibraryPackage $library): UnixShell + { + $this->setEnv([ + 'CFLAGS' => $library->getLibExtraCFlags(), + 'CXXFLAGS' => $library->getLibExtraCXXFlags(), + 'LDFLAGS' => $library->getLibExtraLdFlags(), + 'LIBS' => $library->getLibExtraLibs() . SystemTarget::getRuntimeLibs(), + ]); + return $this; + } + + /** + * Execute a command and return the result. + * + * @param string $cmd Command to execute + * @param bool $with_log Whether to log the command + * @return array{0: int, 1: string[]} Return code and output lines + */ + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + /* @phpstan-ignore-next-line */ + logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd)); + } + $cmd = $this->getExecString($cmd); + $this->logCommandInfo($cmd); + $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false); + $out = explode("\n", $result['output']); + return [$result['code'], $out]; + } + + private function getExecString(string $cmd): string + { + // logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); + $env_str = $this->getEnvString(); + 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 new file mode 100644 index 000000000..5a7511a9c --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -0,0 +1,156 @@ +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + + $original_command = $cmd; + $this->logCommandInfo($original_command); + $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); + return $this; + } + + public function execWithWrapper(string $wrapper, string $args): WindowsCmd + { + return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); + } + + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + 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); + $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; + } + + /** + * 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/Runtime/SystemTarget.php b/src/StaticPHP/Runtime/SystemTarget.php new file mode 100644 index 000000000..584fa49a6 --- /dev/null +++ b/src/StaticPHP/Runtime/SystemTarget.php @@ -0,0 +1,132 @@ + 'Linux', + str_contains($target, '-macos') => 'Darwin', + str_contains($target, '-windows') => 'Windows', + str_contains($target, '-native') => PHP_OS_FAMILY, + default => PHP_OS_FAMILY, + }; + } + + /** + * Returns the target architecture, e.g. x86_64, aarch64. + * Currently, we only support 'x86_64' and 'aarch64' and both can only be built natively. + * + * @return 'aarch64'|'x86_64' + */ + public static function getTargetArch(): string + { + $target = (string) getenv('SPC_TARGET'); + return match (true) { + str_contains($target, 'x86_64') || str_contains($target, 'amd64') => 'x86_64', + str_contains($target, 'aarch64') || str_contains($target, 'arm64') => 'aarch64', + // str_contains($target, 'armv7') || str_contains($target, 'armhf') => 'armv7', + // str_contains($target, 'armv6') || str_contains($target, 'armel') => 'armv6', + // str_contains($target, 'i386') || str_contains($target, 'i686') => 'i386', + default => GNU_ARCH, + }; + } + + /** + * Get the current platform string in the format of {os}-{arch}, e.g. linux-x86_64. + */ + public static function getCurrentPlatformString(): string + { + $os = match (self::getTargetOS()) { + 'Darwin' => 'macos', + 'Linux' => 'linux', + 'Windows' => 'windows', + default => 'unknown', + }; + $arch = self::getTargetArch(); + if (getenv('EMULATE_PLATFORM') !== false) { + return getenv('EMULATE_PLATFORM'); + } + return "{$os}-{$arch}"; + } + + /** + * Check if the target OS is a Unix-like system. + */ + public static function isUnix(): bool + { + return in_array(self::getTargetOS(), ['Linux', 'Darwin', 'BSD']); + } +} diff --git a/src/StaticPHP/Toolchain/ClangNativeToolchain.php b/src/StaticPHP/Toolchain/ClangNativeToolchain.php new file mode 100644 index 000000000..c34e619cd --- /dev/null +++ b/src/StaticPHP/Toolchain/ClangNativeToolchain.php @@ -0,0 +1,57 @@ + LinuxUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + 'Darwin' => MacOSUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + default => throw new EnvironmentException(__CLASS__ . ' is not supported on ' . PHP_OS_FAMILY . '.'), + }; + } + } + + public function getCompilerInfo(): ?string + { + $compiler = getenv('CC') ?: 'clang'; + $version = shell(false)->execWithResult("{$compiler} --version", false); + $head = pathinfo($compiler, PATHINFO_BASENAME); + if ($version[0] === 0 && preg_match('/clang version (\d+\.\d+\.\d+)/', $version[1][0], $match)) { + return "{$head} {$match[1]}"; + } + return $head; + } + + public function isStatic(): bool + { + return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist(); + } +} diff --git a/src/StaticPHP/Toolchain/GccNativeToolchain.php b/src/StaticPHP/Toolchain/GccNativeToolchain.php new file mode 100644 index 000000000..92b82892e --- /dev/null +++ b/src/StaticPHP/Toolchain/GccNativeToolchain.php @@ -0,0 +1,54 @@ + LinuxUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + 'Darwin' => MacOSUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + default => throw new EnvironmentException(__CLASS__ . ' is not supported on ' . PHP_OS_FAMILY . '.'), + }; + } + } + + public function getCompilerInfo(): ?string + { + $compiler = getenv('CC') ?: 'gcc'; + $version = shell(false)->execWithResult("{$compiler} --version", false); + $head = pathinfo($compiler, PATHINFO_BASENAME); + if ($version[0] === 0 && preg_match('/gcc.*?(\d+\.\d+\.\d+)/', $version[1][0], $match)) { + return "{$head} {$match[1]}"; + } + return $head; + } + + public function isStatic(): bool + { + return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist(); + } +} diff --git a/src/StaticPHP/Toolchain/Interface/ToolchainInterface.php b/src/StaticPHP/Toolchain/Interface/ToolchainInterface.php new file mode 100644 index 000000000..34e879cf6 --- /dev/null +++ b/src/StaticPHP/Toolchain/Interface/ToolchainInterface.php @@ -0,0 +1,43 @@ +execWithResult("{$compiler} --version", false); + $head = pathinfo($compiler, PATHINFO_BASENAME); + if ($version[0] === 0 && preg_match('/linux-musl-cc.*(\d+.\d+.\d+)/', $version[1][0], $match)) { + return "{$head} {$match[1]}"; + } + return $head; + } + + public function isStatic(): bool + { + return true; + } +} diff --git a/src/StaticPHP/Toolchain/ToolchainManager.php b/src/StaticPHP/Toolchain/ToolchainManager.php new file mode 100644 index 000000000..688cd33a1 --- /dev/null +++ b/src/StaticPHP/Toolchain/ToolchainManager.php @@ -0,0 +1,92 @@ + LinuxUtil::isMuslDist() ? GccNativeToolchain::class : MuslToolchain::class, + 'glibc' => !LinuxUtil::isMuslDist() ? GccNativeToolchain::class : throw new WrongUsageException('SPC_LIBC must be musl for musl dist.'), + default => throw new WrongUsageException('Unsupported SPC_LIBC value: ' . $libc), + }; + } + + return match (PHP_OS_FAMILY) { + 'Linux' => ZigToolchain::class, + 'Windows' => MSVCToolchain::class, + 'Darwin' => ClangNativeToolchain::class, + default => throw new WrongUsageException('Unsupported OS family: ' . PHP_OS_FAMILY), + }; + } + + /** + * Init the toolchain and set it in the container. + */ + public static function initToolchain(): void + { + $toolchainClass = self::getToolchainClass(); + $toolchain = new $toolchainClass(); + ApplicationContext::set(ToolchainInterface::class, $toolchain); + /* @var ToolchainInterface $toolchainClass */ + $toolchain->initEnv(); + GlobalEnvManager::putenv("SPC_TOOLCHAIN={$toolchainClass}"); + } + + /** + * Perform post-initialization checks and setups for the toolchain. + */ + public static function afterInitToolchain(): void + { + if (!getenv('SPC_TOOLCHAIN')) { + throw new WrongUsageException('SPC_TOOLCHAIN was not properly set. Please contact the developers.'); + } + $musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m')); + if (SystemTarget::getLibc() === 'musl' && !ApplicationContext::get(ToolchainInterface::class)->isStatic() && !file_exists($musl_wrapper_lib)) { + throw new WrongUsageException('You are linking against musl libc dynamically, but musl libc is not installed. Please use `bin/spc doctor` to install it.'); + } + if (SystemTarget::getLibc() === 'glibc' && LinuxUtil::isMuslDist()) { + throw new WrongUsageException('You are linking against glibc dynamically, which is only supported on glibc distros.'); + } + + // init pkg-config for unix + if (SystemTarget::isUnix()) { + if (($found = PkgConfigUtil::findPkgConfig()) !== null) { + GlobalEnvManager::putenv("PKG_CONFIG={$found}"); + } elseif (!ApplicationContext::has('elephant')) { // skip pkg-config check in elephant mode :P (elephant mode is only for building pkg-config itself) + throw new WrongUsageException('Cannot find pkg-config executable. Please run `doctor` to fix this.'); + } + } + + /* @var ToolchainInterface $toolchain */ + $instance = ApplicationContext::get(ToolchainInterface::class); + $instance->afterInit(); + if (getenv('PHP_BUILD_COMPILER') === false && ($compiler_info = $instance->getCompilerInfo())) { + GlobalEnvManager::putenv("PHP_BUILD_COMPILER={$compiler_info}"); + } + } +} diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php new file mode 100644 index 000000000..2d7c71b0c --- /dev/null +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -0,0 +1,108 @@ +/dev/null | grep -v '/32/' | head -n 1"); + $line = trim((string) $output); + if ($line !== '') { + $located = $line; + break; + } + } + if ($located) { + $found[] = $located; + } + } + GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found)); + } + + public function afterInit(): void + { + GlobalEnvManager::addPathIfNotExists($this->getPath()); + f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: ''; + $cxxflags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: ''; + $extraCflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + $cflags = trim($cflags . ' -Wno-date-time'); + $cxxflags = trim($cxxflags . ' -Wno-date-time'); + $extraCflags = trim($extraCflags . ' -Wno-date-time'); + GlobalEnvManager::putenv("SPC_DEFAULT_C_FLAGS={$cflags}"); + GlobalEnvManager::putenv("SPC_DEFAULT_CXX_FLAGS={$cxxflags}"); + GlobalEnvManager::putenv("SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS={$extraCflags}"); + GlobalEnvManager::putenv('RANLIB=zig-ranlib'); + GlobalEnvManager::putenv('OBJCOPY=zig-objcopy'); + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + if (!str_contains($extra_libs, '-lunwind')) { + // Add unwind library if not already present + $extra_libs = trim($extra_libs . ' -lunwind'); + GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); + } + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); + if (!$has_avx512) { + GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no'); + } + } + + public function getCompilerInfo(): ?string + { + $version = shell(false)->execWithResult('zig version', false)[1][0] ?? ''; + return trim("zig {$version}"); + } + + public function isStatic(): bool + { + // if SPC_LIBC is set, it means the target is static, remove it when 3.0 is released + if ($target = getenv('SPC_TARGET')) { + if (str_contains($target, '-macos') || str_contains($target, '-native') && PHP_OS_FAMILY === 'Darwin') { + return false; + } + if (str_contains($target, '-gnu')) { + return false; + } + if (str_contains($target, '-dynamic')) { + return false; + } + if (str_contains($target, '-musl')) { + return true; + } + if (PHP_OS_FAMILY === 'Linux') { + return LinuxUtil::isMuslDist(); + } + return true; + } + if (getenv('SPC_LIBC') === 'musl') { + return true; + } + return false; + } + + private function getPath(): string + { + return PKG_ROOT_PATH . '/zig'; + } +} diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php new file mode 100644 index 000000000..defb00ba2 --- /dev/null +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -0,0 +1,160 @@ + $dependency_overrides Override dependencies (e.g. ['php' => ['ext-gd', 'ext-curl']]) + * @param bool $include_suggests Include suggested packages in the resolution + * @param null|array &$why If provided, will be filled with a reverse dependency map + * @return array Resolved package names in order + */ + public static function resolve(array $packages, array $dependency_overrides = [], bool $include_suggests = false, ?array &$why = null): array + { + $dep_list = PackageConfig::getAll(); + $dep_list_clean = []; + // clear array for further step + foreach ($dep_list as $k => $v) { + $dep_list_clean[$k] = [ + 'depends' => PackageConfig::get($k, 'depends', []), + 'suggests' => PackageConfig::get($k, 'suggests', []), + ]; + } + + // apply dependency overrides + foreach ($dependency_overrides as $target_name => $deps) { + $dep_list_clean[$target_name]['depends'] = array_merge($dep_list_clean[$target_name]['depends'] ?? [], $deps); + } + + // mark suggests as depends + if ($include_suggests) { + foreach ($dep_list_clean as $pkg_name => $pkg_item) { + $dep_list_clean[$pkg_name]['depends'] = array_merge($pkg_item['depends'], $pkg_item['suggests']); + $dep_list_clean[$pkg_name]['suggests'] = []; + } + } + + $resolved = self::doVisitPlat($packages, $dep_list_clean); + + // Build reverse dependency map if $why is requested + if ($why !== null) { + $why = self::buildReverseDependencyMap($resolved, $dep_list_clean, $include_suggests); + } + + return $resolved; + } + + /** + * Build a reverse dependency map for the resolved packages. + * For each package that is depended upon, list which packages depend on it. + * + * @param array $resolved Resolved package names + * @param array $dep_list Dependency declaration list + * @param bool $include_suggests Whether suggests are treated as depends + * @return array Reverse dependency map [depended_pkg => [dependant1, dependant2, ...]] + */ + private static function buildReverseDependencyMap(array $resolved, array $dep_list, bool $include_suggests): array + { + $why = []; + $resolved_set = array_flip($resolved); + + foreach ($resolved as $pkg_name) { + $deps = $dep_list[$pkg_name]['depends'] ?? []; + if ($include_suggests) { + $deps = array_merge($deps, $dep_list[$pkg_name]['suggests'] ?? []); + } + + foreach ($deps as $dep) { + // Only include dependencies that are in the resolved set + if (isset($resolved_set[$dep])) { + if (!isset($why[$dep])) { + $why[$dep] = []; + } + $why[$dep][] = $pkg_name; + } + } + } + + return $why; + } + + /** + * Visitor pattern implementation for dependency resolution. + * + * @param Package[]|string[] $packages Packages list (input) + * @param array $dep_list Dependency declaration list + * @return array Resolved packages array + */ + private static function doVisitPlat(array $packages, array $dep_list): array + { + $sorted = []; + $visited = []; + foreach ($packages as $pkg) { + $pkg_name = is_string($pkg) ? $pkg : $pkg->getName(); + if (!isset($dep_list[$pkg_name])) { + throw new WrongUsageException("Package '{$pkg_name}' does not exist in config, please check your package name !"); + } + if (!isset($visited[$pkg_name])) { + self::visitPlatDeps($pkg_name, $dep_list, $visited, $sorted); + } + } + + $sorted_suggests = []; + $visited_suggests = []; + $final = []; + foreach ($packages as $pkg) { + $pkg_name = is_string($pkg) ? $pkg : $pkg->getName(); + if (!isset($visited_suggests[$pkg_name])) { + self::visitPlatAllDeps($pkg_name, $dep_list, $visited_suggests, $sorted_suggests); + } + } + foreach ($sorted_suggests as $suggest) { + if (in_array($suggest, $sorted)) { + $final[] = $suggest; + } + } + return $final; + } + + private static function visitPlatAllDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void + { + // 如果已经识别到了,那就不管 + if (isset($visited[$pkg_name])) { + return; + } + $visited[$pkg_name] = true; + // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) + foreach (array_merge($dep_list[$pkg_name]['depends'], $dep_list[$pkg_name]['suggests']) as $dep) { + self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted); + } + $sorted[] = $pkg_name; + } + + private static function visitPlatDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void + { + // 如果已经识别到了,那就不管 + if (isset($visited[$pkg_name])) { + return; + } + $visited[$pkg_name] = true; + // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) + if (!isset($dep_list[$pkg_name])) { + throw new WrongUsageException("{$pkg_name} not exist !"); + } + foreach ($dep_list[$pkg_name]['depends'] as $dep) { + self::visitPlatDeps($dep, $dep_list, $visited, $sorted); + } + $sorted[] = $pkg_name; + } +} diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php new file mode 100644 index 000000000..d1e43de23 --- /dev/null +++ b/src/StaticPHP/Util/FileSystem.php @@ -0,0 +1,495 @@ +debug('Reading file: ' . $filename); + $r = file_get_contents(self::convertPath($filename)); + if ($r === false) { + throw new FileSystemException('Reading file ' . $filename . ' failed'); + } + return $r; + } + + /** + * Replace string content in file + * + * @param string $filename The file path + * @param mixed $search The search string + * @param mixed $replace The replacement string + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileStr(string $filename, mixed $search = null, mixed $replace = null): false|int + { + return self::replaceFile($filename, REPLACE_FILE_STR, $search, $replace); + } + + /** + * Replace content in file using regex + * + * @param string $filename The file path + * @param mixed $search The regex pattern + * @param mixed $replace The replacement string + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileRegex(string $filename, mixed $search = null, mixed $replace = null): false|int + { + return self::replaceFile($filename, REPLACE_FILE_PREG, $search, $replace); + } + + /** + * Replace content in file using custom callback + * + * @param string $filename The file path + * @param mixed $callback The callback function + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileUser(string $filename, mixed $callback = null): false|int + { + return self::replaceFile($filename, REPLACE_FILE_USER, $callback); + } + + /** + * Get file extension from filename + * + * @param string $fn The filename + * @return string The file extension (without dot) + */ + public static function extname(string $fn): string + { + $parts = explode('.', basename($fn)); + if (count($parts) < 2) { + return ''; + } + return array_pop($parts); + } + + /** + * Find command path in system PATH (similar to which command) + * + * @param string $name The command name + * @param array $paths Optional array of paths to search + * @return null|string The full path to the command or null if not found + */ + public static function findCommandPath(string $name, array $paths = []): ?string + { + if (!$paths) { + $paths = explode(PATH_SEPARATOR, getenv('PATH')); + } + if (PHP_OS_FAMILY === 'Windows') { + foreach ($paths as $path) { + foreach (['.exe', '.bat', '.cmd'] as $suffix) { + if (file_exists($path . DIRECTORY_SEPARATOR . $name . $suffix)) { + return $path . DIRECTORY_SEPARATOR . $name . $suffix; + } + } + } + return null; + } + foreach ($paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { + return $path . DIRECTORY_SEPARATOR . $name; + } + } + return null; + } + + /** + * Copy directory recursively + * + * @param string $from Source directory path + * @param string $to Destination directory path + */ + public static function copyDir(string $from, string $to): void + { + logger()->debug("Copying directory from {$from} to {$to}"); + $dst_path = FileSystem::convertPath($to); + $src_path = FileSystem::convertPath($from); + switch (PHP_OS_FAMILY) { + case 'Windows': + f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i'); + break; + case 'Linux': + case 'Darwin': + case 'BSD': + f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"'); + break; + } + } + + /** + * Copy file from one location to another. + * This method will throw an exception if the copy operation fails. + * + * @param string $from Source file path + * @param string $to Destination file path + */ + public static function copy(string $from, string $to): void + { + logger()->debug("Copying file from {$from} to {$to}"); + $dst_path = FileSystem::convertPath($to); + $src_path = FileSystem::convertPath($from); + if (!copy($src_path, $dst_path)) { + throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path); + } + } + + /** + * Convert path to system-specific format + * + * @param string $path The path to convert + * @return string The converted path + */ + public static function convertPath(string $path): string + { + if (str_starts_with($path, 'phar://')) { + return $path; + } + return str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + /** + * Convert Windows path to MinGW format + * + * @param string $path The Windows path + * @return string The MinGW format path + */ + public static function convertWinPathToMinGW(string $path): string + { + if (preg_match('/^[A-Za-z]:/', $path)) { + $path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2)); + } + return $path; + } + + /** + * Scan directory files recursively + * + * @param string $dir Directory to scan + * @param bool $recursive Whether to scan recursively + * @param bool|string $relative Whether to return relative paths + * @param bool $include_dir Whether to include directories in result + * @return array|false Array of files or false on failure + */ + public static function scanDirFiles(string $dir, bool $recursive = true, bool|string $relative = false, bool $include_dir = false): array|false + { + $dir = self::convertPath($dir); + if (!is_dir($dir)) { + return false; + } + logger()->debug('scanning directory ' . $dir); + $scan_list = scandir($dir); + if ($scan_list === false) { + logger()->warning('Scan dir failed, cannot scan directory: ' . $dir); + return false; + } + $list = []; + // 将 relative 置为相对目录的前缀 + if ($relative === true) { + $relative = $dir; + } + // 遍历目录 + foreach ($scan_list as $v) { + // Unix 系统排除这俩目录 + if ($v == '.' || $v == '..') { + continue; + } + $sub_file = self::convertPath($dir . '/' . $v); + if (is_dir($sub_file) && $recursive) { + # 如果是 目录 且 递推 , 则递推添加下级文件 + $sub_list = self::scanDirFiles($sub_file, $recursive, $relative); + if (is_array($sub_list)) { + foreach ($sub_list as $item) { + $list[] = $item; + } + } + } elseif (is_file($sub_file) || (is_dir($sub_file) && !$recursive && $include_dir)) { + # 如果是 文件 或 (是 目录 且 不递推 且 包含目录) + if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) { + $list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\'); + } elseif ($relative === false) { + $list[] = $sub_file; + } + } + } + return $list; + } + + /** + * Get PSR-4 classes from directory + * + * @param string $dir Directory to scan + * @param string $base_namespace Base namespace + * @param mixed $rule Optional filtering rule + * @param bool|string $return_path_value Whether to return path as value + * @param bool $auto_require Whether to auto-require files (useful for external plugins) + * @return array Array of class names or class=>path pairs + */ + public static function getClassesPsr4(string $dir, string $base_namespace, mixed $rule = null, bool|string $return_path_value = false, bool $auto_require = false): array + { + $classes = []; + $files = FileSystem::scanDirFiles($dir, true, true); + if ($files === false) { + throw new FileSystemException('Cannot scan dir files during get classes psr-4 from dir: ' . $dir); + } + foreach ($files as $v) { + $pathinfo = pathinfo($v); + if (($pathinfo['extension'] ?? '') == 'php') { + if ($rule === null) { + if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) { + continue; + } + if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') { + continue; + } + } elseif (is_callable($rule) && !$rule($dir, $pathinfo)) { + continue; + } + $dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\'); + $class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename']; + $file_path = self::convertPath($dir . '/' . $v); + + // Auto-require file if class is not loaded (for external plugins not in composer autoload) + if ($auto_require && !class_exists($class_name, false)) { + require_once $file_path; + } + + if (is_string($return_path_value)) { + $classes[$class_name] = $return_path_value . '/' . $v; + } else { + $classes[] = $class_name; + } + } + } + return $classes; + } + + /** + * Remove directory recursively + * + * @param string $dir Directory to remove + * @return bool Success status + */ + public static function removeDir(string $dir): bool + { + $dir = self::convertPath($dir); + logger()->debug('Removing path recursively: "' . $dir . '"'); + if (!file_exists($dir)) { + logger()->debug('Scan dir failed, no such file or directory.'); + return false; + } + if (!is_dir($dir)) { + logger()->warning('Scan dir failed, not directory.'); + return false; + } + logger()->debug('scanning directory ' . $dir); + // 套上 zm_dir + $scan_list = scandir($dir); + if ($scan_list === false) { + logger()->warning('Scan dir failed, cannot scan directory: ' . $dir); + return false; + } + // 遍历目录 + foreach ($scan_list as $v) { + InteractiveTerm::advance(); + // Unix 系统排除这俩目录 + if ($v == '.' || $v == '..') { + continue; + } + $sub_file = self::convertPath($dir . '/' . $v); + if (is_dir($sub_file)) { + # 如果是 目录 且 递推 , 则递推添加下级文件 + if (!self::removeDir($sub_file)) { + return false; + } + } elseif (is_link($sub_file) || is_file($sub_file)) { + if (!unlink($sub_file)) { + return false; + } + } + } + if (is_link($dir)) { + return @unlink($dir); + } + return @rmdir($dir); + } + + /** + * Create directory recursively + * + * @param string $path Directory path to create + */ + public static function createDir(string $path): void + { + if (!is_dir($path) && !f_mkdir($path, 0755, true) && !is_dir($path)) { + throw new FileSystemException(sprintf('Unable to create dir: %s', $path)); + } + } + + /** + * Write content to file + * + * @param string $path File path + * @param mixed $content Content to write + * @param mixed ...$args Additional arguments passed to file_put_contents + * @return bool|int|string Result of file writing operation + */ + public static function writeFile(string $path, mixed $content, ...$args): bool|int|string + { + $dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME); + if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) { + throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir); + } + return file_put_contents($path, $content, ...$args); + } + + /** + * Reset directory by removing and recreating it + * + * @param string $dir_name Directory name + */ + public static function resetDir(string $dir_name): void + { + $dir_name = self::convertPath($dir_name); + if (is_dir($dir_name)) { + self::removeDir($dir_name); + } + self::createDir($dir_name); + } + + /** + * Add source extraction hook + * + * @param string $name Source name + * @param callable $callback Callback function + */ + public static function addSourceExtractHook(string $name, callable $callback): void + { + self::$_extract_hook[$name][] = $callback; + } + + /** + * Check if path is relative + * + * @param string $path Path to check + * @return bool True if path is relative + */ + public static function isRelativePath(string $path): bool + { + if (DIRECTORY_SEPARATOR === '\\') { + return !(strlen($path) > 2 && ctype_alpha($path[0]) && $path[1] === ':'); + } + return strlen($path) > 0 && $path[0] !== '/'; + } + + /** + * Replace path variables with actual values + * + * @param string $path Path with variables + * @return string Path with replaced variables + */ + public static function replacePathVariable(string $path): string + { + $replacement = [ + '{pkg_root_path}' => PKG_ROOT_PATH, + '{php_sdk_path}' => getenv('PHP_SDK_PATH') ? getenv('PHP_SDK_PATH') : WORKING_DIR . '/php-sdk-binary-tools', + '{working_dir}' => WORKING_DIR, + '{download_path}' => DOWNLOAD_PATH, + '{source_path}' => SOURCE_PATH, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $path); + } + + /** + * Create backup of file + * + * @param string $path File path + * @return string Backup file path + */ + public static function backupFile(string $path): string + { + copy($path, $path . '.bak'); + return $path . '.bak'; + } + + /** + * Restore file from backup + * + * @param string $path Original file path + */ + public static function restoreBackupFile(string $path): void + { + if (!file_exists($path . '.bak')) { + throw new FileSystemException("Backup restore failed: Cannot find bak file for {$path}"); + } + copy($path . '.bak', $path); + unlink($path . '.bak'); + } + + /** + * Remove file if it exists + * + * @param string $string File path + */ + public static function removeFileIfExists(string $string): void + { + $string = self::convertPath($string); + if (file_exists($string)) { + unlink($string); + } + } + + /** + * Replace line in file that contains specific string + * + * @param string $file File path + * @param string $find String to find in line + * @param string $line New line content + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileLineContainsString(string $file, string $find, string $line): false|int + { + $lines = file($file); + if ($lines === false) { + throw new FileSystemException('Cannot read file: ' . $file); + } + foreach ($lines as $key => $value) { + if (str_contains($value, $find)) { + $lines[$key] = $line . PHP_EOL; + } + } + return file_put_contents($file, implode('', $lines)); + } + + private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int + { + logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename); + $file = self::readFile($filename); + switch ($replace_type) { + case REPLACE_FILE_STR: + default: + $file = str_replace($callback_or_search, $to_replace, $file); + break; + case REPLACE_FILE_PREG: + $file = preg_replace($callback_or_search, $to_replace, $file); + break; + case REPLACE_FILE_USER: + $file = $callback_or_search($file); + break; + } + return file_put_contents($filename, $file); + } +} diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php new file mode 100644 index 000000000..cc21b480c --- /dev/null +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -0,0 +1,178 @@ + $v) { + if (getenv($k) === false) { + $default_put_list[$k] = $v; + self::putenv("{$k}={$v}"); + } + } + $os_ini = match (PHP_OS_FAMILY) { + 'Windows' => $ini['windows'] ?? [], + 'Darwin' => $ini['macos'] ?? [], + 'Linux' => $ini['linux'] ?? [], + 'BSD' => $ini['freebsd'] ?? [], + default => [], + }; + foreach ($os_ini as $k => $v) { + if (getenv($k) === false) { + $default_put_list[$k] = $v; + self::putenv("{$k}={$v}"); + } + } + + ToolchainManager::initToolchain(); + + // apply second time + $ini2 = self::readIniFile(); + + foreach ($ini2['global'] as $k => $v) { + if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) { + self::putenv("{$k}={$v}"); + } + } + $os_ini2 = match (PHP_OS_FAMILY) { + 'Windows' => $ini2['windows'] ?? [], + 'Darwin' => $ini2['macos'] ?? [], + 'Linux' => $ini2['linux'] ?? [], + 'BSD' => $ini2['freebsd'] ?? [], + default => [], + }; + foreach ($os_ini2 as $k => $v) { + if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) { + self::putenv("{$k}={$v}"); + } + } + self::$initialized = true; + } + + public static function putenv(string $val): void + { + f_putenv($val); + self::$env_cache[] = $val; + } + + public static function addPathIfNotExists(string $path): void + { + if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) { + self::putenv("PATH={$path}:" . getenv('PATH')); + } + } + + /** + * Initialize the toolchain after the environment variables are set. + * The toolchain or environment availability check is done here. + */ + public static function afterInit(): void + { + if (!filter_var(getenv('SPC_SKIP_TOOLCHAIN_CHECK'), FILTER_VALIDATE_BOOL)) { + ToolchainManager::afterInitToolchain(); + } + // test bison + if (PHP_OS_FAMILY === 'Darwin') { + if ($bison = MacOSUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) { + self::putenv("BISON={$bison}"); + } + if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) { + self::putenv("YACC={$yacc}"); + } + } + } + + private static function readIniFile(): array + { + // Init env.ini file, read order: + // WORKING_DIR/config/env.ini + // ROOT_DIR/config/env.ini + $ini_files = [ + WORKING_DIR . '/config/env.ini', + ROOT_DIR . '/config/env.ini', + ]; + $ini_custom = [ + WORKING_DIR . '/config/env.custom.ini', + ROOT_DIR . '/config/env.custom.ini', + ]; + $ini = null; + foreach ($ini_files as $ini_file) { + if (file_exists($ini_file)) { + $ini = parse_ini_file($ini_file, true); + break; + } + } + if ($ini === null) { + throw new WrongUsageException('env.ini not found'); + } + if ($ini === false || !isset($ini['global'])) { + throw new WrongUsageException('Failed to parse ' . $ini_file); + } + // apply custom env + foreach ($ini_custom as $ini_file) { + if (file_exists($ini_file)) { + $ini_custom = parse_ini_file($ini_file, true); + if ($ini_custom !== false) { + $ini['global'] = array_merge($ini['global'], $ini_custom['global'] ?? []); + match (PHP_OS_FAMILY) { + 'Windows' => $ini['windows'] = array_merge($ini['windows'], $ini_custom['windows'] ?? []), + 'Darwin' => $ini['macos'] = array_merge($ini['macos'], $ini_custom['macos'] ?? []), + 'Linux' => $ini['linux'] = array_merge($ini['linux'], $ini_custom['linux'] ?? []), + 'BSD' => $ini['freebsd'] = array_merge($ini['freebsd'], $ini_custom['freebsd'] ?? []), + default => null, + }; + } + break; + } + } + return $ini; + } +} diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php new file mode 100644 index 000000000..01e4bdc92 --- /dev/null +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -0,0 +1,112 @@ +isVerbose()) { + logger()->notice(strip_ansi_colors($message)); + } else { + $output->writeln(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message); + } + } + + public static function success(string $message, bool $indent = false): void + { + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + logger()->info(strip_ansi_colors($message)); + } else { + $output->writeln(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message); + } + } + + public static function plain(string $message): void + { + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + logger()->info(strip_ansi_colors($message)); + } else { + $output->writeln($message); + } + } + + public static function info(string $message): void + { + $output = ApplicationContext::get(OutputInterface::class); + if (!$output->isVerbose()) { + $output->writeln(ConsoleColor::green('▶ ') . $message); + } + logger()->info(strip_ansi_colors($message)); + } + + public static function error(string $message, bool $indent = true): void + { + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + logger()->error(strip_ansi_colors($message)); + } else { + $output->writeln('' . ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message)); + } + } + + public static function advance(): void + { + self::$indicator?->advance(); + } + + public static function setMessage(string $message): void + { + self::$indicator?->setMessage($message); + } + + public static function finish(string $message, bool $status = true): void + { + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + if ($status) { + logger()->info($message); + } else { + logger()->error($message); + } + return; + } + if (self::$indicator !== null) { + if (!$status) { + self::$indicator->finish($message, '' . ConsoleColor::red(' ✘')); + } else { + self::$indicator->finish($message, '' . ConsoleColor::green(' ✔')); + } + self::$indicator = null; + } + } + + public static function indicateProgress(string $message): void + { + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + logger()->info(strip_ansi_colors($message)); + return; + } + if (self::$indicator !== null) { + // just reuse existing indicator, change + self::setMessage($message); + self::$indicator->advance(); + return; + } + self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' ⠏', ' ⠛', ' ⠹', ' ⢸', ' ⣰', ' ⣤', ' ⣆', ' ⡇']); + self::$indicator->start($message); + } +} diff --git a/src/StaticPHP/Util/PkgConfigUtil.php b/src/StaticPHP/Util/PkgConfigUtil.php new file mode 100644 index 000000000..5efd33de9 --- /dev/null +++ b/src/StaticPHP/Util/PkgConfigUtil.php @@ -0,0 +1,124 @@ +no_php = $options['no_php'] ?? false; + $this->libs_only_deps = $options['libs_only_deps'] ?? false; + $this->absolute_libs = $options['absolute_libs'] ?? false; + } + + public function config(array $packages = [], bool $include_suggests = false): array + { + // if have php, make php as all extension's dependency + if (!$this->no_php) { + $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; + } else { + $dep_override = []; + } + $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); + + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved); + $libs = $this->getLibsString($resolved, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + // embed + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved)}"; + } + // C++ + if ($this->hasCpp($resolved)) { + $libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + $libs = "-lphp {$libs} -lc"; + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + + /** + * [Helper function] + * Get configuration for a specific extension(s) dependencies. + * + * @param Extension|Extension[] $extension Extension instance or list + * @param bool $include_suggest_ext Whether to include suggested extensions + * @param bool $include_suggest_lib Whether to include suggested libraries + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array + { + if (!is_array($extension)) { + $extension = [$extension]; + } + $libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension))); + return $this->config( + extensions: array_map(fn ($x) => $x->getName(), $extension), + libraries: $libs, + include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false, + include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + ); + } + + /** + * [Helper function] + * Get configuration for a specific library(s) dependencies. + * + * @param LibraryBase|LibraryBase[] $lib Library instance or list + * @param bool $include_suggest_lib Whether to include suggested libraries + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array + { + if (!is_array($lib)) { + $lib = [$lib]; + } + $save_no_php = $this->no_php; + $this->no_php = true; + $save_libs_only_deps = $this->libs_only_deps; + $this->libs_only_deps = true; + $ret = $this->config( + libraries: array_map(fn ($x) => $x->getName(), $lib), + include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + ); + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + return $ret; + } + + private function hasCpp(array $packages): bool + { + foreach ($packages as $package) { + $lang = PackageConfig::get($package, 'lang', 'c'); + if ($lang === 'cpp') { + return true; + } + } + return false; + } + + private function getIncludesString(array $packages): string + { + $base = BUILD_INCLUDE_PATH; + $includes = ["-I{$base}"]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "-I{$base}/php", + "-I{$base}/php/main", + "-I{$base}/php/TSRM", + "-I{$base}/php/Zend", + "-I{$base}/php/ext", + ]; + } + + // parse pkg-configs + foreach ($packages as $package) { + $pc = PackageConfig::get($package, 'pkg-configs', []); + foreach ($pc as $file) { + if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$file}.pc")) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + } + } + $pc_cflags = implode(' ', $pc); + if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { + $arr = explode(' ', $pc_cflags); + $arr = array_unique($arr); + $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); + $pc_cflags = implode(' ', $arr); + $includes[] = $pc_cflags; + } + } + $includes = array_unique($includes); + return implode(' ', $includes); + } + + private function getLdflagsString(): string + { + return '-L' . BUILD_LIB_PATH; + } + + private function getLibsString(array $packages, bool $use_short_libs = true): string + { + $lib_names = []; + $frameworks = []; + + foreach ($packages as $package) { + // add pkg-configs libs + $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + foreach ($pkg_configs as $pkg_config) { + if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + } + } + $pkg_configs = implode(' ', $pkg_configs); + if ($pkg_configs !== '') { + // static libs with dependencies come in reverse order, so reverse this too + $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); + $lib_names = [...$lib_names, ...$pc_libs]; + } + // convert all static-libs to short names + $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); + foreach ($libs as $lib) { + // check file existence + if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { + throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); + } + $lib_names[] = $this->getShortLibName($lib); + } + // add frameworks for macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); + } + } + + // post-process + $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); + $lib_names = array_reverse(array_unique($lib_names)); + $frameworks = array_unique($frameworks); + + // process frameworks to short_name + if (SystemTarget::getTargetOS() === 'Darwin') { + foreach ($frameworks as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $lib_names)) { + $lib_names[] = $ks; + } + } + } + + if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { + $lib_names[] = '-lcrypt'; + } + if (!$use_short_libs) { + $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); + } + return implode(' ', $lib_names); + } + + private function getShortLibName(string $lib): string + { + if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { + return BUILD_LIB_PATH . '/' . $lib; + } + // get short name + return '-l' . substr($lib, 3, -2); + } + + private function getFullLibName(string $lib): string + { + if (!str_starts_with($lib, '-l')) { + return $lib; + } + $libname = substr($lib, 2); + $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; + if (file_exists($staticLib)) { + return $staticLib; + } + return $lib; + } + + private function getFrameworksString(array $extensions): string + { + $list = []; + foreach ($extensions as $extension) { + foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $list)) { + $list[] = $ks; + } + } + } + return implode(' ', $list); + } +} diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php new file mode 100644 index 000000000..6f57cb00a --- /dev/null +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -0,0 +1,162 @@ +debug('Applying ' . ($reverse ? 'reverse ' : '') . "patch [{$patch_name}] at [{$cwd}]"); + if (FileSystem::isRelativePath($patch_name)) { + $patch_file = ROOT_DIR . "/src/globals/patch/{$patch_name}"; + } else { + $patch_file = $patch_name; + } + + if (!file_exists($patch_file)) { + throw new PatchException($patch_name, "Patch file [{$patch_file}] does not exist"); + } + + $patch_str = FileSystem::convertPath($patch_file); + if (!file_exists($patch_str)) { + throw new PatchException($patch_name, "Patch file [{$patch_str}] does not exist"); + } + + // Copy patch from phar + if (str_starts_with($patch_str, 'phar://')) { + $filename = pathinfo($patch_file, PATHINFO_BASENAME); + file_put_contents(SOURCE_PATH . "/{$filename}", file_get_contents($patch_file)); + $patch_str = FileSystem::convertPath(SOURCE_PATH . "/{$filename}"); + } + + // Detect if patch is already applied (reverse detection) + $detect_reverse = !$reverse; + $detect_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' + . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) + . ' | patch --dry-run -p1 -s -f ' . ($detect_reverse ? '-R' : '') + . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; + exec($detect_cmd, $output, $detect_status); + + if ($detect_status === 0) { + // Patch already applied + return true; + } + + // Apply patch + $apply_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' + . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) + . ' | patch -p1 ' . ($reverse ? '-R' : ''); + + exec($apply_cmd, $apply_output, $apply_status); + if ($apply_status !== 0) { + throw new PatchException($patch_name, "Patch file [{$patch_name}] failed to apply"); + } + + return true; + } + + /** + * Patch hardcoded INI values into PHP SAPI files. + * + * @param string $php_source_dir PHP source directory path + * @param array $ini Associative array of INI key-value pairs + * @return bool True if patch was applied successfully + */ + #[PatchDescription('Patch hardcoded INI values into PHP SAPI files')] + public static function patchHardcodedINI(string $php_source_dir, array $ini = []): bool + { + $sapi_files = [ + 'cli' => "{$php_source_dir}/sapi/cli/php_cli.c", + 'micro' => "{$php_source_dir}/sapi/micro/php_micro.c", + 'embed' => "{$php_source_dir}/sapi/embed/php_embed.c", + ]; + + // Build patch string + $find_str = 'const char HARDCODED_INI[] ='; + $patch_str = ''; + foreach ($ini as $key => $value) { + $patch_str .= "\"{$key}={$value}\\n\"\n"; + } + $patch_str = "const char HARDCODED_INI[] =\n{$patch_str}"; + + // Detect and restore from backup if exists + $has_backup = false; + foreach ($sapi_files as $file) { + if (file_exists("{$file}.bak")) { + $has_backup = true; + break; + } + } + if ($has_backup) { + self::unpatchHardcodedINI($php_source_dir); + } + + // Backup and patch each SAPI file + $result = true; + foreach ($sapi_files as $file) { + if (!file_exists($file)) { + continue; + } + // Backup + $result = $result && file_put_contents("{$file}.bak", file_get_contents($file)) !== false; + // Patch + FileSystem::replaceFileStr($file, $find_str, $patch_str); + } + + return $result; + } + + /** + * Restore PHP SAPI files from backup (unpatch hardcoded INI). + * + * @param string $php_source_dir PHP source directory path + * @return bool True if backup was restored successfully + */ + public static function unpatchHardcodedINI(string $php_source_dir): bool + { + $sapi_files = [ + 'cli' => "{$php_source_dir}/sapi/cli/php_cli.c", + 'micro' => "{$php_source_dir}/sapi/micro/php_micro.c", + 'embed' => "{$php_source_dir}/sapi/embed/php_embed.c", + ]; + + $has_backup = false; + foreach ($sapi_files as $file) { + if (file_exists("{$file}.bak")) { + $has_backup = true; + break; + } + } + + if (!$has_backup) { + return false; + } + + $result = true; + foreach ($sapi_files as $file) { + $backup = "{$file}.bak"; + if (file_exists($backup)) { + $result = $result && file_put_contents($file, file_get_contents($backup)) !== false; + @unlink($backup); + } + } + + return $result; + } +} diff --git a/src/StaticPHP/Util/System/LinuxUtil.php b/src/StaticPHP/Util/System/LinuxUtil.php new file mode 100644 index 000000000..2b1d44f43 --- /dev/null +++ b/src/StaticPHP/Util/System/LinuxUtil.php @@ -0,0 +1,148 @@ + 'unknown', + 'ver' => 'unknown', + ]; + switch (true) { + case file_exists('/etc/centos-release'): + $lines = file('/etc/centos-release'); + $centos = true; + goto rh; + case file_exists('/etc/redhat-release'): + $lines = file('/etc/redhat-release'); + $centos = false; + rh: + foreach ($lines as $line) { + if (preg_match('/release\s+(\d*(\.\d+)*)/', $line, $matches)) { + /* @phpstan-ignore-next-line */ + $ret['dist'] = $centos ? 'centos' : 'redhat'; + $ret['ver'] = $matches[1]; + } + } + break; + case file_exists('/etc/os-release'): + $lines = file('/etc/os-release'); + foreach ($lines as $line) { + if (preg_match('/^ID=(.*)$/', $line, $matches)) { + $ret['dist'] = $matches[1]; + } + if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) { + $ret['ver'] = $matches[1]; + } + } + $ret['dist'] = trim($ret['dist'], '"\''); + $ret['ver'] = trim($ret['ver'], '"\''); + if (strcasecmp($ret['dist'], 'centos') === 0) { + $ret['dist'] = 'redhat'; + } + break; + } + return $ret; + } + + /** + * Check if current linux distro is musl-based (alpine). + */ + public static function isMuslDist(): bool + { + return static::getOSRelease()['dist'] === 'alpine'; + } + + /** + * Get CPU core count. + */ + public static function getCpuCount(): int + { + $ncpu = 1; + + if (is_file('/proc/cpuinfo')) { + $cpuinfo = file_get_contents('/proc/cpuinfo'); + preg_match_all('/^processor/m', $cpuinfo, $matches); + $ncpu = count($matches[0]); + } + + return $ncpu; + } + + /** + * Get fully-supported linux distros. + * + * @return string[] List of supported Linux distro name for doctor + */ + public static function getSupportedDistros(): array + { + return [ + // debian-like + 'debian', 'ubuntu', 'Deepin', + // rhel-like + 'redhat', + // centos + 'centos', + // alpine + 'alpine', + // arch + 'arch', 'manjaro', + ]; + } + + /** + * Get libc version string from ldd. + */ + public static function getLibcVersionIfExists(?string $libc = null): ?string + { + if (self::$libc_version !== null) { + return self::$libc_version; + } + if ($libc === 'glibc') { + $result = shell()->execWithResult('ldd --version', false); + if ($result[0] !== 0) { + return null; + } + // get first line + $first_line = $result[1][0]; + // match ldd version: "ldd (some useless text) 2.17" match 2.17 + $pattern = '/ldd\s+\(.*?\)\s+(\d+\.\d+)/'; + if (preg_match($pattern, $first_line, $matches)) { + self::$libc_version = $matches[1]; + return self::$libc_version; + } + return null; + } + if ($libc === 'musl') { + if (self::isMuslDist()) { + $result = shell()->execWithResult('ldd 2>&1', false); + } elseif (is_file('/usr/local/musl/lib/libc.so')) { + $result = shell()->execWithResult('/usr/local/musl/lib/libc.so 2>&1', false); + } else { + $arch = php_uname('m'); + $result = shell()->execWithResult("/lib/ld-musl-{$arch}.so.1 2>&1", false); + } + // Match Version * line + // match ldd version: "Version 1.2.3" match 1.2.3 + $pattern = '/Version\s+(\d+\.\d+\.\d+)/'; + if (preg_match($pattern, $result[1][1] ?? '', $matches)) { + self::$libc_version = $matches[1]; + return self::$libc_version; + } + } + return null; + } +} diff --git a/src/StaticPHP/Util/System/MacOSUtil.php b/src/StaticPHP/Util/System/MacOSUtil.php new file mode 100644 index 000000000..5acd712f7 --- /dev/null +++ b/src/StaticPHP/Util/System/MacOSUtil.php @@ -0,0 +1,42 @@ + '--target=x86_64-apple-darwin', + 'arm64','aarch64' => '--target=arm64-apple-darwin', + default => throw new WrongUsageException('unsupported arch: ' . $arch), + }; + } +} diff --git a/src/StaticPHP/Util/System/UnixUtil.php b/src/StaticPHP/Util/System/UnixUtil.php new file mode 100644 index 000000000..8dd606188 --- /dev/null +++ b/src/StaticPHP/Util/System/UnixUtil.php @@ -0,0 +1,128 @@ +execWithResult($cmd); + if ($result[0] !== 0) { + throw new ExecutionException($cmd, 'Failed to get defined symbols from ' . $lib_file); + } + // parse shell output and filter + $defined = []; + foreach ($result[1] as $line) { + $line = trim($line); + if ($line === '' || str_ends_with($line, '.o:') || str_ends_with($line, '.o]:')) { + continue; + } + $name = strtok($line, " \t"); + if (!$name) { + continue; + } + $name = preg_replace('/@.*$/', '', $name); + if ($name !== '' && $name !== false) { + $defined[] = $name; + } + } + $defined = array_unique($defined); + sort($defined); + // export + if (SystemTarget::getTargetOS() === 'Linux') { + file_put_contents("{$lib_file}.dynsym", "{\n" . implode("\n", array_map(fn ($x) => " {$x};", $defined)) . "};\n"); + } else { + file_put_contents("{$lib_file}.dynsym", implode("\n", $defined) . "\n"); + } + } + + /** + * Get linker flag to export dynamic symbols from a static library. + * + * @param string $lib_file Static library file path (e.g. /path/to/libxxx.a) + * @return null|string Linker flag to export dynamic symbols, null if no .dynsym file found + */ + public static function getDynamicExportedSymbols(string $lib_file): ?string + { + $symbol_file = "{$lib_file}.dynsym"; + if (!is_file($symbol_file)) { + self::exportDynamicSymbols($lib_file); + } + if (!is_file($symbol_file)) { + throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); + } + // https://github.com/ziglang/zig/issues/24662 + if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + return '-Wl,--export-dynamic'; + } + // macOS + if (SystemTarget::getTargetOS() !== 'Linux') { + return "-Wl,-exported_symbols_list,{$symbol_file}"; + } + return "-Wl,--dynamic-list={$symbol_file}"; + } + + /** + * Find a command in given paths or system PATH. + * If $name is an absolute path, check if it exists. + * + * @param string $name Command name or absolute path + * @param array $paths Paths to search, if empty, use system PATH + * @return null|string Absolute path of the command if found, null otherwise + */ + public static function findCommand(string $name, array $paths = []): ?string + { + if (!$paths) { + $paths = explode(PATH_SEPARATOR, getenv('PATH')); + } + if (str_starts_with($name, '/')) { + return file_exists($name) ? $name : null; + } + foreach ($paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { + return $path . DIRECTORY_SEPARATOR . $name; + } + } + return null; + } + + /** + * Make environment variable string for shell command. + * + * @param array $vars Variables, like: ["CFLAGS" => "-Ixxx"] + * @return string like: CFLAGS="-Ixxx" + */ + public static function makeEnvVarString(array $vars): string + { + $str = ''; + foreach ($vars as $key => $value) { + if ($str !== '') { + $str .= ' '; + } + $str .= $key . '=' . escapeshellarg($value); + } + return $str; + } +} diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php new file mode 100644 index 000000000..1d9111617 --- /dev/null +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -0,0 +1,106 @@ +|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', + ]; + 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, + ]; + } + } + return false; + } + + /** + * Get CPU count for concurrency. + */ + public static function getCpuCount(): int + { + $result = f_exec('echo %NUMBER_OF_PROCESSORS%', $out, $code); + if ($code !== 0 || !$result) { + return 1; + } + return intval($result); + } + + /** + * Create CMake toolchain file. + * + * @param null|string $cflags CFLAGS for cmake, default use '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP=' + * @param null|string $ldflags LDFLAGS for cmake, default use '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt' + */ + public static function makeCmakeToolchainFile(?string $cflags = null, ?string $ldflags = null): string + { + if ($cflags === null) { + $cflags = '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP='; + } + if ($ldflags === null) { + $ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'; + } + $buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH); + $toolchain = <<getOption('with-suggested-libs')) { + $input->setOption('with-suggests', true); + } + if ($input->getOption('with-suggested-exts')) { + $input->setOption('with-suggests', true); + } + if ($input->getOption('with-libs')) { + $existing = $input->getOption('with-packages'); + $additional = $input->getOption('with-libs'); + if (!empty($existing)) { + $input->setOption('with-packages', $existing . ',' . $additional); + } else { + $input->setOption('with-packages', $additional); + } + } + } + + public static function getLegacyBuildOptions(): array + { + return [ + new InputOption('with-suggested-libs', null, InputOption::VALUE_NONE, 'Resolve and install suggested libraries as well (legacy)'), + new InputOption('with-suggested-exts', null, InputOption::VALUE_NONE, 'Resolve and install suggested extensions as well (legacy)'), + new InputOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries to install/build, comma separated (legacy)', ''), + ]; + } + + /** + * Add legacy build options for the 'php' target package. + */ + public static function addLegacyBuildOptionsForPhp(TargetPackage $package): void + { + if ($package->getName() === 'php') { + $package->addBuildOption('build-micro', null, null, 'Build micro SAPI'); + $package->addBuildOption('build-cli', null, null, 'Build cli SAPI'); + $package->addBuildOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); + $package->addBuildOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); + $package->addBuildOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); + $package->addBuildOption('build-cgi', null, null, 'Build cgi SAPI'); + $package->addBuildOption('build-all', null, null, 'Build all SAPI'); + } + } + + public static function beforeExtractHook(Artifact $artifact): void + { + self::emitPatchPoint(match ($artifact->getName()) { + 'php-src' => 'before-php-extract', + 'micro' => 'before-micro-extract', + default => '', + }); + } + + public static function afterExtractHook(Artifact $artifact): void + { + self::emitPatchPoint(match ($artifact->getName()) { + 'php-src' => 'after-php-extract', + 'micro' => 'after-micro-extract', + default => '', + }); + } + + public static function beforeExtsExtractHook(): void + { + self::emitPatchPoint('before-exts-extract'); + } + + public static function afterExtsExtractHook(): void + { + self::emitPatchPoint('after-exts-extract'); + } + + public static function beforeLibExtractHook(string $pkg): void + { + self::emitPatchPoint("before-library[{$pkg}]-extract"); + } + + public static function afterLibExtractHook(string $pkg): void + { + self::emitPatchPoint("after-library[{$pkg}]-extract"); + } + + public static function emitPatchPoint(string $point_name): void + { + if ($point_name === '') { + return; + } + if (!ApplicationContext::has(PackageInstaller::class)) { + return; + } + $builder = ApplicationContext::get(PackageBuilder::class); + $patch_points = $builder->getOption('with-added-patch', []); + ApplicationContext::set('patch_point', $point_name); + foreach ($patch_points as $patch_point) { + if (!file_exists($patch_point)) { + throw new WrongUsageException("Additional patch script {$patch_point} does not exist!"); + } + logger()->debug("Applying additional patch script {$patch_point}"); + + try { + require $patch_point; + } catch (InterruptException $e) { + if ($e->getCode() === 0) { + logger()->notice('Patch script ' . $patch_point . ' interrupted' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.')); + } else { + logger()->error('Patch script ' . $patch_point . ' interrupted with error code [' . $e->getCode() . ']' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.')); + } + } + } + ApplicationContext::set('patch_point', ''); + } +} diff --git a/src/bootstrap.php b/src/bootstrap.php new file mode 100644 index 000000000..6af814e8f --- /dev/null +++ b/src/bootstrap.php @@ -0,0 +1,64 @@ +addLogCallback(function ($level, $output) use ($log_file_fd) { + if ($log_file_fd) { + fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); + } + return true; + }); +} + +// load internal registry +Registry::loadRegistry(ROOT_DIR . '/spc.registry.json'); +// load registries from environment variable SPC_REGISTRIES +Registry::loadFromEnvOrOption(); diff --git a/src/globals/defines.php b/src/globals/defines.php index 36ffba798..3e6d23605 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -2,8 +2,6 @@ declare(strict_types=1); -use ZM\Logger\ConsoleLogger; - define('WORKING_DIR', getcwd()); define('ROOT_DIR', dirname(__DIR__, 2)); putenv('WORKING_DIR=' . WORKING_DIR); @@ -79,22 +77,32 @@ const PKGCONF_PATCH_CUSTOM = 16; const PKGCONF_PATCH_ALL = 31; -// autoconf flags -const AUTOCONF_LIBS = 1; -const AUTOCONF_CFLAGS = 2; -const AUTOCONF_CPPFLAGS = 4; -const AUTOCONF_LDFLAGS = 8; -const AUTOCONF_ALL = 15; +// spc download status +const SPC_DOWNLOAD_STATUS_SKIPPED = 1; +const SPC_DOWNLOAD_STATUS_SUCCESS = 2; +const SPC_DOWNLOAD_STATUS_FAILED = 3; // spc download source type const SPC_SOURCE_ARCHIVE = 'archive'; // download as archive const SPC_SOURCE_GIT = 'git'; // download as git repository const SPC_SOURCE_LOCAL = 'local'; // download as local directory -// spc logs dir -const SPC_LOGS_DIR = WORKING_DIR . DIRECTORY_SEPARATOR . 'log'; -const SPC_OUTPUT_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.output.log'; -const SPC_SHELL_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.shell.log'; - -ConsoleLogger::$date_format = 'H:i:s'; -ConsoleLogger::$format = '[%date%] [%level_short%] %body%'; +const SPC_STATUS_EXTRACTED = 0; +const SPC_STATUS_INSTALLED = 0; +const SPC_STATUS_BUILT = 0; +const SPC_STATUS_ALREADY_EXTRACTED = 1; +const SPC_STATUS_ALREADY_INSTALLED = 1; +const SPC_STATUS_ALREADY_BUILT = 1; + +const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [ + 'bitbuckettag' => 'BitBucket', + 'filelist' => 'website', + 'git' => 'git', + 'ghrel' => 'GitHub release', + 'ghtar', 'ghtagtar' => 'GitHub tarball', + 'local' => 'local dir', + 'pie' => 'PHP Installer for Extensions', + 'url' => 'url', + 'php-release' => 'php.net', + 'custom' => 'custom downloader', +]; diff --git a/src/globals/functions.php b/src/globals/functions.php index c8c6a8d0f..8621e7adc 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -2,14 +2,12 @@ declare(strict_types=1); -use Psr\Log\LoggerInterface; -use SPC\builder\BuilderBase; -use SPC\builder\BuilderProvider; -use SPC\exception\ExecutionException; -use SPC\exception\InterruptException; -use SPC\exception\WrongUsageException; -use SPC\util\shell\UnixShell; -use SPC\util\shell\WindowsCmd; +use StaticPHP\Exception\ExecutionException; +use StaticPHP\Exception\InterruptException; +use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Runtime\Shell\DefaultShell; +use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\Shell\WindowsCmd; use ZM\Logger\ConsoleLogger; /** @@ -31,7 +29,7 @@ function is_list_array(mixed $array): bool /** * Return a logger instance */ -function logger(): LoggerInterface +function logger(): ConsoleLogger { global $ob_logger; if ($ob_logger === null) { @@ -40,11 +38,6 @@ function logger(): LoggerInterface return $ob_logger; } -function is_unix(): bool -{ - return in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'BSD']); -} - /** * Transfer architecture name to gnu triplet */ @@ -84,50 +77,22 @@ function quote(string $str, string $quote = '"'): string return $quote . $str . $quote; } -/** - * Get Family name of current OS. - */ -function osfamily2dir(): string -{ - return match (PHP_OS_FAMILY) { - /* @phpstan-ignore-next-line */ - 'Windows', 'WINNT', 'Cygwin' => 'windows', - 'Darwin' => 'macos', - 'Linux' => 'linux', - 'BSD' => 'freebsd', - default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY), - }; -} - -function osfamily2shortname(): string -{ - return match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'Linux' => 'linux', - 'BSD' => 'bsd', - default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY), - }; -} - function shell(?bool $debug = null): UnixShell { /* @noinspection PhpUnhandledExceptionInspection */ return new UnixShell($debug); } -function cmd(?bool $debug = null): WindowsCmd +function default_shell(): DefaultShell { /* @noinspection PhpUnhandledExceptionInspection */ - return new WindowsCmd($debug); + return new DefaultShell(); } -/** - * Get current builder. - */ -function builder(): BuilderBase +function cmd(?bool $debug = null): WindowsCmd { - return BuilderProvider::getBuilder(); + /* @noinspection PhpUnhandledExceptionInspection */ + return new WindowsCmd($debug); } /** @@ -135,7 +100,11 @@ function builder(): BuilderBase */ function patch_point(): string { - return BuilderProvider::getBuilder()->getPatchPoint(); + if (StaticPHP\DI\ApplicationContext::has('patch_point')) { + /* @phpstan-ignore-next-line */ + return StaticPHP\DI\ApplicationContext::get('patch_point'); + } + return ''; } function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException @@ -272,6 +241,8 @@ function keyboard_interrupt_register(callable $callback): void if (PHP_OS_FAMILY === 'Windows') { sapi_windows_set_ctrl_handler($callback); } elseif (extension_loaded('pcntl')) { + global $_previous_sigint_handler; + $_previous_sigint_handler = pcntl_signal_get_handler(SIGINT); pcntl_signal(SIGINT, $callback); } } @@ -287,6 +258,12 @@ function keyboard_interrupt_unregister(): void if (PHP_OS_FAMILY === 'Windows') { sapi_windows_set_ctrl_handler(null); } elseif (extension_loaded('pcntl')) { + global $_previous_sigint_handler; + if ($_previous_sigint_handler !== null) { + pcntl_signal(SIGINT, $_previous_sigint_handler); + $_previous_sigint_handler = null; + return; + } pcntl_signal(SIGINT, SIG_IGN); } } @@ -317,3 +294,61 @@ function get_display_path(string $path): string } throw new WrongUsageException("Cannot convert path: {$path}"); } + +/** + * Get the global DI container instance. + * + * @deprecated Use ApplicationContext::getContainer() or dependency injection instead. + * This function is kept for backward compatibility during the migration period. + */ +function spc_container(): DI\Container +{ + return \StaticPHP\DI\ApplicationContext::getContainer(); +} + +/** + * Parse extension list from string, replace alias and filter internal extensions. + * + * @param null|array|string $ext_list Extension list, can be array or comma-separated string + * @return string[] List of extension names + */ +function parse_extension_list(array|string|null $ext_list): array +{ + // standardize and trim + $ext_list = parse_comma_list($ext_list); + // replace alias + $ls = array_map(function ($x) { + $lower = strtolower(trim($x)); + if (isset(SPC_EXTENSION_ALIAS[$lower])) { + logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.'); + return SPC_EXTENSION_ALIAS[$lower]; + } + return $lower; + }, $ext_list); + // filter internals + return array_values(array_filter($ls, function ($x) { + if (in_array($x, SPC_INTERNAL_EXTENSIONS)) { + logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored."); + return false; + } + return true; + })); +} + +/** + * Parse comma list from string. + * + * @param null|array|string $package_list Comma list, can be array or comma-separated string + * @return string[] List of items + */ +function parse_comma_list(array|string|null $package_list): array +{ + if (is_string($package_list)) { + $package_list = array_map('trim', array_filter(explode(',', $package_list))); + } + if (is_array($package_list)) { + // remove duplicates + return array_values(array_unique($package_list)); + } + return []; +} diff --git a/src/globals/internal-env.php b/src/globals/internal-env.php index 9f893df24..97df88417 100644 --- a/src/globals/internal-env.php +++ b/src/globals/internal-env.php @@ -2,14 +2,24 @@ declare(strict_types=1); -use SPC\builder\freebsd\SystemUtil as BSDSystemUtil; -use SPC\builder\linux\SystemUtil as LinuxSystemUtil; -use SPC\builder\macos\SystemUtil as MacOSSystemUtil; -use SPC\builder\windows\SystemUtil as WindowsSystemUtil; -use SPC\ConsoleApplication; -use SPC\store\FileSystem; - // static-php-cli version string +use Laravel\Prompts\ConfirmPrompt; +use Laravel\Prompts\Prompt; +use Laravel\Prompts\TextPrompt; +use StaticPHP\ConsoleApplication; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Util\FileSystem; +use StaticPHP\Util\System\LinuxUtil; +use StaticPHP\Util\System\MacOSUtil; +use StaticPHP\Util\System\WindowsUtil; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; + const SPC_VERSION = ConsoleApplication::VERSION; // output path for everything, other paths are defined relative to this by default define('BUILD_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot'))); @@ -23,16 +33,15 @@ define('BUILD_MODULES_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_MODULES_PATH')) ? $a : (BUILD_ROOT_PATH . '/modules'))); // pkg arch name -$_pkg_arch_name = arch2gnu(php_uname('m')) . '-' . strtolower(PHP_OS_FAMILY); +$_pkg_arch_name = getenv('EMULATE_PLATFORM') ?: (arch2gnu(php_uname('m')) . '-' . strtolower(PHP_OS_FAMILY)); define('PKG_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('PKG_ROOT_PATH')) ? $a : (WORKING_DIR . "/pkgroot/{$_pkg_arch_name}"))); define('SOURCE_PATH', FileSystem::convertPath(is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source'))); define('DOWNLOAD_PATH', FileSystem::convertPath(is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads'))); define('CPU_COUNT', match (PHP_OS_FAMILY) { - 'Windows' => (string) WindowsSystemUtil::getCpuCount(), - 'Darwin' => (string) MacOSSystemUtil::getCpuCount(), - 'Linux' => (string) LinuxSystemUtil::getCpuCount(), - 'BSD' => (string) BSDSystemUtil::getCpuCount(), + 'Windows' => (string) WindowsUtil::getCpuCount(), + 'Darwin' => (string) MacOSUtil::getCpuCount(), + 'Linux' => (string) LinuxUtil::getCpuCount(), default => 1, }); define('GNU_ARCH', arch2gnu(php_uname('m'))); @@ -41,6 +50,11 @@ default => $_im8a }); +// logs dir +define('SPC_LOGS_DIR', FileSystem::convertPath(is_string($a = getenv('SPC_LOGS_DIR')) ? $a : (WORKING_DIR . '/log'))); +const SPC_OUTPUT_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.output.log'; +const SPC_SHELL_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.shell.log'; + // deprecated variables define('SEPARATED_PATH', [ '/' . pathinfo(BUILD_LIB_PATH)['basename'], // lib @@ -61,3 +75,31 @@ putenv('SPC_ARCH=' . php_uname('m')); putenv('GNU_ARCH=' . GNU_ARCH); putenv('MAC_ARCH=' . MAC_ARCH); + +// initialize windows prompt fallback for laravel-prompts +Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows'); +ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) { + $helper = new QuestionHelper(); + $case = $prompt->default ? ' [Y/n] ' : ' [y/N] '; + $question = new ConfirmationQuestion($prompt->label . $case, $prompt->default); + if (ApplicationContext::has(InputInterface::class) && ApplicationContext::has(OutputInterface::class)) { + $input = ApplicationContext::get(InputInterface::class); + $output = ApplicationContext::get(OutputInterface::class); + } else { + $input = new ArrayInput([]); + $output = new ConsoleOutput(); + } + return $helper->ask($input, $output, $question); +}); +TextPrompt::fallbackUsing(function (TextPrompt $prompt) { + $helper = new QuestionHelper(); + $question = new Question($prompt->label . ' ', $prompt->default); + if (ApplicationContext::has(InputInterface::class) && ApplicationContext::has(OutputInterface::class)) { + $input = ApplicationContext::get(InputInterface::class); + $output = ApplicationContext::get(OutputInterface::class); + } else { + $input = new ArrayInput([]); + $output = new ConsoleOutput(); + } + return $helper->ask($input, $output, $question); +}); diff --git a/src/globals/patch/php-src-patches/Readme.md b/src/globals/patch/php-src-patches/Readme.md new file mode 100644 index 000000000..152cf6860 --- /dev/null +++ b/src/globals/patch/php-src-patches/Readme.md @@ -0,0 +1,95 @@ + +# 补丁 / Patches + +名称 Name | 平台 Platform | 可选? Optional? | 用途 Usage +--- | --- | --- | --- +phar | * | 可选 Optional | 允许micro使用压缩phar Allow micro use compressed phar +static_opcache | * | 可选 Optional | 支持静态构建opcache Support build opcache statically +macos_iconv | macOS | 可选 Optional | 支持链接到系统的iconv Support link against system iconv +static_extensions_win32 | Windows | 可选 Optional | 支持静态构建Windows其他扩展 Support build other extensions for windows +cli_checks | * | 可选 Optional | 修改PHP内核中硬编码的SAPI检查 Modify hardcoden SAPI name checks in PHP core +disable_huge_page | Linux | 可选 Optional | 禁用linux构建的max-page-size选项,缩减sfx体积(典型的, 10M+ -> 5M) Disable max-page-size for linux build,shrink sfx size (10M+ -> 5M typ.) +vcruntime140 | Windows | 必须 Nessesary | 禁用sfx启动时GetModuleHandle(vcruntime140(d).dll) Disable GetModuleHandle(vcruntime140(d).dll) at sfx start +win32 | Windows | 必须 Nessesary | 修改构建系统以静态构建 Modify build system for build sfx file +zend_stream | Windows | 必须 Nessesary | 修改构建系统以静态构建 Modify build system for build sfx file +comctl32 | Windows | 可选 Optional | 添加comctl32.dll manifest以启用[visual style](https://learn.microsoft.com/en-us/windows/win32/controls/visual-styles-overview) (会让窗口控件好看一些) Add manifest dependency for comctl32 to enable [visual style](https://learn.microsoft.com/en-us/windows/win32/controls/visual-styles-overview) (makes window control looks modern) +win32_api | Windows | 必须 Necessary | 修复一些win32 api的声明 Fix declarations of some win32 apis + +## Usage + +目前补丁不需要特定顺序,使用 + +```bash +# 在PHP源码目录 +patch -p1 < sapi/micro/patches/some_patch.patch +``` + +来打patch + +Currently, patches do not require a specific order. Use + +```bash +# at PHP source root +patch -p1 < sapi/micro/patches/some_patch.patch +``` + +to apply the patch. + +### version choose + +patch文件名为\<名称\>.patch或者\<名称\>_\<版本\>.patch,如果没有版本号,说明这个补丁支持所有目前micro支持的PHP版本 + +Patch file name is \.patch or \_\.patch. If there is no version number, it means that the patch supports all PHP versions that micro supports. + +选择等于或者低于要打补丁的PHP版本的最新版本的patch,例如要给php 8.2打patch,有 80 81 84 三个patch, 则选择81 + +Choose the latest patch that is equal to or lower than the PHP version you want to patch. For example, if you want to patch PHP 8.2, and there are patches 80 81 84, choose 81. + +所有的补丁都是给最新的修正版本使用的 + +All patches are applied to the latest patch version of its minor version. + +## Something special + +### phar.patch + +这个patch绕过PHAR对micro的文件名中包含".phar"的限制(并不会允许micro本身以外的其他文件),这使得micro文件名中不含".phar"时依然可以使用压缩过的phar + +This patch bypasses the restriction that a PHAR file must contain '.phar' in its filename when invoked with micro (it will not allow files other than the sfx to be regarded as phar). This allows micro to handle compressed phar files without a custom stub. + +有特别的stub的PHAR不需要这个补丁也可以使用 + +phar with a stub (may be a special one) do not need this patch. + +这个补丁只能在micro中使用,会导致其他SAPI编译不过 + +This patch can only be used with micro, as it causes other SAPIs to fail to build. + +### static_opcache + +静态链接opcache到PHP里,可以在其他的SAPI上用 + +This makes opcache statically linked into PHP, and it can be used for other SAPIs. + +PHP 8.3.11, 8.2.23中,opcache的config.m4发生了[变动](https://github.com/php/php-src/commit/d20d11375fa602236e1fb828f6a2236b19b43cdc),这个patch对应变动后的版本 + +The opcache's config.m4 has [changed](https://github.com/php/php-src/commit/d20d11375fa602236e1fb828f6a2236b19b43cdc) in PHP 8.3.11 and 8.2.23, and this patch corresponds to the updated version. + +### cli_checks + +绕过许多硬编码的“是不是cli”的检查 + +This bypasses many hard-coded cli SAPI name checks. + +### cli_static + +允许Windows的cli静态构建,不是给micro用的 + +This allows the Windows cli SAPI to be built fully statically. It is not a patch for micro. + +### win32_api + +修复一些win32 api的声明,避免编译警告。这些修改已经在新版本 PHP (>=8.4)中合并,但保证旧版本也能用,这些补丁仍然需要 + +This fixes declarations of some win32 apis to avoid compilation warnings. These changes have been merged into newer versions of PHP (>=8.4), but to ensure that older versions can still be used, these patches are still needed. + diff --git a/src/globals/patch/php-src-patches/cli_checks_80.patch b/src/globals/patch/php-src-patches/cli_checks_80.patch new file mode 100644 index 000000000..ff7fd4364 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_80.patch @@ -0,0 +1,174 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index bc5a6b2e23..710515b6c1 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -531,7 +531,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index fc8bb9a1b0..2fd083d912 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -4935,7 +4935,7 @@ ZEND_MINIT_FUNCTION(ffi) + + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + + INIT_NS_CLASS_ENTRY(ce, "FFI", "Exception", NULL); + zend_ffi_exception_ce = zend_register_internal_class_ex(&ce, zend_ce_error); +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index c195ad7d2c..eef18fd10a 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2622,7 +2622,7 @@ static inline int accel_find_sapi(void) + } + } + if (ZCG(accel_directives).enable_cli && ( +- strcmp(sapi_module.name, "cli") == 0 ++ strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0)) { + return SUCCESS; + } +@@ -2916,7 +2916,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -2928,7 +2928,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = 0; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI and LiteSpeed SAPIs", NULL, accelerator_remove_cb); +diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c +index d9b9d94c6f..744c715b38 100644 +--- a/ext/pcre/php_pcre.c ++++ b/ext/pcre/php_pcre.c +@@ -291,7 +291,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */ + + /* If we're on the CLI SAPI, there will only be one request, so we don't need the + * cache to survive after RSHUTDOWN. */ +- pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0; ++ pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + if (!pcre_globals->per_request_cache) { + zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1); + } +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 2930796ae7..20ad2706c7 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -721,7 +721,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if (0 == strncmp("cli", sapi_module.name, 3) || 0 == strncmp("micro", sapi_module.name, 5)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index cd91e68fd3..f270eb5a15 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -400,7 +400,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index c5743c3361..b2dd79f5c4 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index 03b55c3eac..5bb0472f76 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1136,7 +1136,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 7bd5400760..f0a71d7915 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -480,7 +480,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(tmp_value, tmp_value_length); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +diff --git a/win32/console.c b/win32/console.c +index 7833dd97d3..1fa8e4cea9 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return !strncmp(sapi_module.name, "cli", sizeof("cli") - 1) || !strncmp(sapi_module.name, "micro", sizeof("micro") - 1); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_81.patch b/src/globals/patch/php-src-patches/cli_checks_81.patch new file mode 100644 index 000000000..92d6cce5a --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_81.patch @@ -0,0 +1,183 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index cfe344e377..7e1a5ca54f 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -531,7 +531,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index 8f05686367..c155028233 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5247,7 +5247,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 5f6b854d47..2b8362c412 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2830,7 +2830,7 @@ static inline int accel_find_sapi(void) + } + if (ZCG(accel_directives).enable_cli && ( + strcmp(sapi_module.name, "cli") == 0 +- || strcmp(sapi_module.name, "phpdbg") == 0)) { ++ || strcmp(sapi_module.name, "phpdbg") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + return SUCCESS; + } + } +@@ -3128,7 +3128,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -3140,7 +3140,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = 0; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI and LiteSpeed SAPIs", NULL, accelerator_remove_cb); +diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c +index a8d3559ef5..1b40f94643 100644 +--- a/ext/pcre/php_pcre.c ++++ b/ext/pcre/php_pcre.c +@@ -291,7 +291,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */ + + /* If we're on the CLI SAPI, there will only be one request, so we don't need the + * cache to survive after RSHUTDOWN. */ +- pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0; ++ pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + if (!pcre_globals->per_request_cache) { + zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1); + } +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 8bf5d23df7..9af99ada0b 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -735,7 +735,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if ((strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 007eef7a74..86d75103c8 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -399,7 +399,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index 4287045511..eab0311d07 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index a57e66bd97..e9044cd34e 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1135,7 +1135,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index dc705fcdbd..a206aa11e4 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -475,7 +475,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1340,7 +1340,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: %s in %s on line %" PRIu32 "\n", error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno); +diff --git a/win32/console.c b/win32/console.c +index 9b48561088..a2b764cdb5 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_83.patch b/src/globals/patch/php-src-patches/cli_checks_83.patch new file mode 100644 index 000000000..e94250958 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_83.patch @@ -0,0 +1,192 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index dc8f9fefa3..057d76229e 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -530,7 +530,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index bbfe07576e..398373d577 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5402,7 +5402,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index a21c640d91..3af0e89b21 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2822,7 +2822,7 @@ static inline zend_result accel_find_sapi(void) + } + if (ZCG(accel_directives).enable_cli && ( + strcmp(sapi_module.name, "cli") == 0 +- || strcmp(sapi_module.name, "phpdbg") == 0)) { ++ || strcmp(sapi_module.name, "phpdbg") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + return SUCCESS; + } + } +@@ -3127,7 +3127,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -3139,7 +3139,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = false; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI, FrankenPHP, LiteSpeed and uWSGI SAPIs", NULL, accelerator_remove_cb); +@@ -4681,7 +4681,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + if (!ZCG(accel_directives).preload_user + || !*ZCG(accel_directives).preload_user) { + +- bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c +index 6ad0b6eb76..7c9861678f 100644 +--- a/ext/pcre/php_pcre.c ++++ b/ext/pcre/php_pcre.c +@@ -300,7 +300,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */ + + /* If we're on the CLI SAPI, there will only be one request, so we don't need the + * cache to survive after RSHUTDOWN. */ +- pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0; ++ pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + if (!pcre_globals->per_request_cache) { + zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1); + } +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 8fbe93d648..3c14946e58 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -736,7 +736,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if ((strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index badcfcc29b..70d4d5423e 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -402,7 +402,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index 8926485025..6740163bc5 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index 2d4cb42b7a..726d995dc0 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1280,7 +1280,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 3c9c55129e..cb8fb42eea 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -486,7 +486,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1367,7 +1367,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 9b48561088..a2b764cdb5 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_84.patch b/src/globals/patch/php-src-patches/cli_checks_84.patch new file mode 100644 index 000000000..6b8ac74e1 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_84.patch @@ -0,0 +1,191 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index dc8f9fefa3..057d76229e 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -530,7 +530,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index d797f5f93f..27cb05e3e4 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5403,7 +5403,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 8d45b2ae41..35e9403a31 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2822,7 +2822,7 @@ static inline zend_result accel_find_sapi(void) + } + if (ZCG(accel_directives).enable_cli && ( + strcmp(sapi_module.name, "cli") == 0 +- || strcmp(sapi_module.name, "phpdbg") == 0)) { ++ || strcmp(sapi_module.name, "phpdbg") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + return SUCCESS; + } + } +@@ -3134,7 +3134,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -3146,7 +3146,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = false; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI, FrankenPHP, LiteSpeed and uWSGI SAPIs", NULL, accelerator_remove_cb); +@@ -4685,7 +4685,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + if (!ZCG(accel_directives).preload_user + || !*ZCG(accel_directives).preload_user) { + +- bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c +index 49a477998b..18fe71cce4 100644 +--- a/ext/pdo_sqlite/pdo_sqlite.c ++++ b/ext/pdo_sqlite/pdo_sqlite.c +@@ -94,6 +94,7 @@ PHP_METHOD(Pdo_Sqlite, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { + zend_throw_exception_ex(php_pdo_get_exception(), 0, "Not supported in multithreaded Web servers"); +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 80ddd88f7d..7b37aaff0b 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -730,7 +730,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if ((strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 0f26593b85..5f253f0061 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -408,7 +408,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index a5581d9ccc..98455f7b52 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index d935f9d216..bac478144d 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1274,7 +1274,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index a3acaf94b7..7ac56f9919 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -486,7 +486,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1371,7 +1371,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 9b48561088..a2b764cdb5 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_85.patch b/src/globals/patch/php-src-patches/cli_checks_85.patch new file mode 100644 index 000000000..cfd72550c --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_85.patch @@ -0,0 +1,178 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index 4c8fc9d1..8284ac2f 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -535,7 +535,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index 10fc11f5..eb4d4175 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5478,7 +5478,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index f597df36..ec617af7 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2847,6 +2847,7 @@ static void zps_startup_failure(const char *reason, const char *api_reason, int + static inline bool accel_sapi_is_cli(void) + { + return strcmp(sapi_module.name, "cli") == 0 ++ || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0; + } + +@@ -3163,6 +3164,7 @@ static int accel_startup(zend_extension *extension) + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && + (strcmp(sapi_module.name, "cli") == 0 || ++ strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -4958,6 +4960,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + || !*ZCG(accel_directives).preload_user) { + + bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c +index 023e35a2..6f00159a 100644 +--- a/ext/pdo_sqlite/pdo_sqlite.c ++++ b/ext/pdo_sqlite/pdo_sqlite.c +@@ -94,6 +94,7 @@ PHP_METHOD(Pdo_Sqlite, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { + zend_throw_exception_ex(php_pdo_get_exception(), 0, "Not supported in multithreaded Web servers"); +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 31212999..d7705d59 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -730,7 +730,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 21b6840a..05a7aa8e 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -413,6 +413,7 @@ PHP_METHOD(SQLite3, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index ea33ba49..083184b8 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index 690e23e0..80ab3d32 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1333,7 +1333,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 8465b6c0..cf8f9ef0 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -580,7 +580,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1470,7 +1470,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 9b485610..a2b764cd 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_static_80.patch b/src/globals/patch/php-src-patches/cli_static_80.patch new file mode 100644 index 000000000..338873ee6 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_static_80.patch @@ -0,0 +1,24 @@ +diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c +index 0ad53e813c..a8cc1bee29 100644 +--- a/sapi/cli/php_cli.c ++++ b/sapi/cli/php_cli.c +@@ -97,7 +97,7 @@ PHPAPI extern char *php_ini_scanned_files; + + #if defined(PHP_WIN32) + #if defined(ZTS) +-ZEND_TSRMLS_CACHE_DEFINE() ++//ZEND_TSRMLS_CACHE_DEFINE() + #endif + static DWORD orig_cp = 0; + #endif +@@ -1160,6 +1160,10 @@ int main(int argc, char *argv[]) + #endif + { + #if defined(PHP_WIN32) ++ if (!php_win32_ioutil_init()) { ++ fprintf(stderr, "ioutil initialization failed"); ++ return 1; ++ } + # ifdef PHP_CLI_WIN32_NO_CONSOLE + int argc = __argc; + char **argv = __argv; diff --git a/src/globals/patch/php-src-patches/cli_static_84.patch b/src/globals/patch/php-src-patches/cli_static_84.patch new file mode 100644 index 000000000..90d8b310e --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_static_84.patch @@ -0,0 +1,23 @@ +diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c +--- a/sapi/cli/php_cli.c (revision d3bf67d44102869f340a7be0e12f4f09de0edbcf) ++++ b/sapi/cli/php_cli.c (date 1735128770216) +@@ -98,7 +98,7 @@ + + #if defined(PHP_WIN32) + #if defined(ZTS) +-ZEND_TSRMLS_CACHE_DEFINE() ++//ZEND_TSRMLS_CACHE_DEFINE() + #endif + static DWORD orig_cp = 0; + #endif +@@ -1137,6 +1137,10 @@ + #endif + { + #if defined(PHP_WIN32) ++ if (!php_win32_ioutil_init()) { ++ fprintf(stderr, "ioutil initialization failed"); ++ return 1; ++ } + # ifdef PHP_CLI_WIN32_NO_CONSOLE + int argc = __argc; + char **argv = __argv; diff --git a/src/globals/patch/php-src-patches/cli_static_85.patch b/src/globals/patch/php-src-patches/cli_static_85.patch new file mode 100644 index 000000000..83f701d00 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_static_85.patch @@ -0,0 +1,13 @@ +diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c +index e212a0f7..f16e8ea9 100644 +--- a/sapi/cli/php_cli.c ++++ b/sapi/cli/php_cli.c +@@ -98,7 +98,7 @@ PHPAPI extern char *php_ini_scanned_files; + + #if defined(PHP_WIN32) + #if defined(ZTS) +-ZEND_TSRMLS_CACHE_DEFINE() ++// ZEND_TSRMLS_CACHE_DEFINE() + #endif + static DWORD orig_cp = 0; + #endif diff --git a/src/globals/patch/php-src-patches/comctl32.patch b/src/globals/patch/php-src-patches/comctl32.patch new file mode 100644 index 000000000..45b2cf1c8 --- /dev/null +++ b/src/globals/patch/php-src-patches/comctl32.patch @@ -0,0 +1,21 @@ +diff --git a/win32/build/default.manifest b/win32/build/default.manifest +index a73c2fb53d..52351251e1 100644 +--- a/win32/build/default.manifest ++++ b/win32/build/default.manifest +@@ -24,4 +24,16 @@ + true + + ++ ++ ++ ++ ++ + diff --git a/src/globals/patch/php-src-patches/disable_huge_page_80.patch b/src/globals/patch/php-src-patches/disable_huge_page_80.patch new file mode 100644 index 000000000..e671f3fb3 --- /dev/null +++ b/src/globals/patch/php-src-patches/disable_huge_page_80.patch @@ -0,0 +1,11 @@ +--- php-8.0.0/configure.ac 2020-11-25 01:04:03.000000000 +0800 ++++ php-8.0.0-micro/configure.ac 2020-11-29 20:00:13.256181206 +0800 +@@ -1005,7 +1005,7 @@ dnl Extensions post-config. + dnl ---------------------------------------------------------------------------- + + dnl Align segments on huge page boundary +-case $host_alias in ++case nope in + i[[3456]]86-*-linux-* | x86_64-*-linux-*) + AC_MSG_CHECKING(linker support for -zcommon-page-size=2097152) + save_LDFLAGS=$LDFLAGS diff --git a/src/globals/patch/php-src-patches/disable_huge_page_84.patch b/src/globals/patch/php-src-patches/disable_huge_page_84.patch new file mode 100644 index 000000000..b49dbbcdb --- /dev/null +++ b/src/globals/patch/php-src-patches/disable_huge_page_84.patch @@ -0,0 +1,13 @@ +diff --git a/configure.ac b/configure.ac +index 48778c7bd2..760c4f2670 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -1127,7 +1127,7 @@ dnl Extensions post-config. + dnl ---------------------------------------------------------------------------- + + dnl Align segments on huge page boundary +-AS_CASE([$host_alias], [[i[3456]86-*-linux-* | x86_64-*-linux-*]], ++AS_CASE([nope], [[i[3456]86-*-linux-* | x86_64-*-linux-*]], + [AC_CACHE_CHECK([linker support for -zcommon-page-size=2097152], + [php_cv_have_common_page_size], [ + save_LDFLAGS=$LDFLAGS diff --git a/src/globals/patch/php-src-patches/macos_iconv_80.patch b/src/globals/patch/php-src-patches/macos_iconv_80.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_80.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_81.patch b/src/globals/patch/php-src-patches/macos_iconv_81.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_81.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_82.patch b/src/globals/patch/php-src-patches/macos_iconv_82.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_82.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_83.patch b/src/globals/patch/php-src-patches/macos_iconv_83.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_83.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_84.patch b/src/globals/patch/php-src-patches/macos_iconv_84.patch new file mode 100644 index 000000000..c362e3987 --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_84.patch @@ -0,0 +1,21 @@ +diff --git a/build/php.m4 b/build/php.m4 +index e45b22b766..506be904f1 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1821,15 +1821,12 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + AS_VAR_IF([PHP_ICONV], [yes], [ +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save=$LIBS +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC([iconv], [found_iconv=yes], + [AC_CHECK_FUNC([libiconv], [ + AC_DEFINE([HAVE_LIBICONV], [1]) + found_iconv=yes + ])]) +- LIBS=$LIBS_save + ]) + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/phar_80.patch b/src/globals/patch/php-src-patches/phar_80.patch new file mode 100644 index 000000000..c33a24f5a --- /dev/null +++ b/src/globals/patch/php-src-patches/phar_80.patch @@ -0,0 +1,22 @@ +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +index 2403d77a..c908a1b4 100644 +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -3309,6 +3309,8 @@ static zend_string *phar_resolve_path(const char *filename, size_t filename_len) + return phar_find_in_include_path((char *) filename, filename_len, NULL); + } + ++char *micro_get_filename(void); ++ + static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */ + { + zend_op_array *res; +@@ -3319,7 +3321,7 @@ static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) + if (!file_handle || !file_handle->filename) { + return phar_orig_compile_file(file_handle, type); + } +- if (strstr(file_handle->filename, ".phar") && !strstr(file_handle->filename, "://")) { ++ if ((strstr(file_handle->filename, micro_get_filename()) || strstr(file_handle->filename, ".phar")) && !strstr(file_handle->filename, "://")) { + if (SUCCESS == phar_open_from_filename((char*)file_handle->filename, strlen(file_handle->filename), NULL, 0, 0, &phar, NULL)) { + if (phar->is_zip || phar->is_tar) { + zend_file_handle f = *file_handle; diff --git a/src/globals/patch/php-src-patches/phar_81.patch b/src/globals/patch/php-src-patches/phar_81.patch new file mode 100644 index 000000000..2bbdbf012 --- /dev/null +++ b/src/globals/patch/php-src-patches/phar_81.patch @@ -0,0 +1,22 @@ +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +index 3c0f3eb50b..455b303a8d 100644 +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -3295,6 +3295,8 @@ static zend_string *phar_resolve_path(zend_string *filename) + return ret; + } + ++char *micro_get_filename(void); ++ + static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */ + { + zend_op_array *res; +@@ -3305,7 +3307,7 @@ static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) + if (!file_handle || !file_handle->filename) { + return phar_orig_compile_file(file_handle, type); + } +- if (strstr(ZSTR_VAL(file_handle->filename), ".phar") && !strstr(ZSTR_VAL(file_handle->filename), "://")) { ++ if ((strstr(ZSTR_VAL(file_handle->filename), micro_get_filename()) || strstr(ZSTR_VAL(file_handle->filename), ".phar")) && !strstr(ZSTR_VAL(file_handle->filename), "://")) { + if (SUCCESS == phar_open_from_filename(ZSTR_VAL(file_handle->filename), ZSTR_LEN(file_handle->filename), NULL, 0, 0, &phar, NULL)) { + if (phar->is_zip || phar->is_tar) { + zend_file_handle f; diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_80.patch b/src/globals/patch/php-src-patches/static_extensions_win32_80.patch new file mode 100644 index 000000000..4b47d325f --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_80.patch @@ -0,0 +1,31 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index eefccb3d72..b231f67b23 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,6 +10,6 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 9187d6bfc2..f1acc2e8b5 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,12 +1,12 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret > 0) { +- EXTENSION("openssl", "openssl.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", PHP_OPENSSL_SHARED ? 0 : 1, "Have openssl"); + AC_DEFINE("HAVE_OPENSSL", 1); + } diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_83.patch b/src/globals/patch/php-src-patches/static_extensions_win32_83.patch new file mode 100644 index 000000000..007edd9cf --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_83.patch @@ -0,0 +1,31 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index e42f1ce3f7..db28a68676 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,6 +10,6 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 9187d6bfc2..f1acc2e8b5 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,12 +1,12 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret > 0) { +- EXTENSION("openssl", "openssl.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", PHP_OPENSSL_SHARED ? 0 : 1, "Have openssl"); + AC_DEFINE("HAVE_OPENSSL", 1); + } diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_84.patch b/src/globals/patch/php-src-patches/static_extensions_win32_84.patch new file mode 100644 index 000000000..04246ccd4 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_84.patch @@ -0,0 +1,34 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index 2a42dc45..c207694f 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,7 +10,7 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_EXTENSION_DEP('fileinfo', 'pcre'); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 24064ec2..87ff3161 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,6 +1,6 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + ARG_WITH("openssl-legacy-provider", "OPENSSL: Load legacy algorithm provider in addition to default provider", "no"); + +@@ -10,7 +10,7 @@ if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret >= 2) { +- EXTENSION("openssl", "openssl.c openssl_pwhash.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c openssl_pwhash.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", 1, "Define to 1 if the PHP extension 'openssl' is available."); + if (PHP_OPENSSL_LEGACY_PROVIDER != "no") { + AC_DEFINE("LOAD_OPENSSL_LEGACY_PROVIDER", 1, "Define to 1 to load the OpenSSL legacy algorithm provider in addition to the default provider."); diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_85.patch b/src/globals/patch/php-src-patches/static_extensions_win32_85.patch new file mode 100644 index 000000000..9784edbc4 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_85.patch @@ -0,0 +1,34 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index 2a42dc45..c207694f 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,7 +10,7 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_EXTENSION_DEP('fileinfo', 'pcre'); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 714f93a0..0ab6efff 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,6 +1,6 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + ARG_WITH("openssl-legacy-provider", "OPENSSL: Load legacy algorithm provider in addition to default provider", "no"); + +@@ -10,7 +10,7 @@ if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret >= 2) { +- EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", 1, "Define to 1 if the PHP extension 'openssl' is available."); + if (PHP_OPENSSL_LEGACY_PROVIDER != "no") { + AC_DEFINE("LOAD_OPENSSL_LEGACY_PROVIDER", 1, "Define to 1 to load the OpenSSL legacy algorithm provider in addition to the default provider."); diff --git a/src/globals/patch/php-src-patches/static_opcache_80.patch b/src/globals/patch/php-src-patches/static_opcache_80.patch new file mode 100644 index 000000000..f83ad046a --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_80.patch @@ -0,0 +1,129 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index ad3781101b..b7133fc0c8 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index c195ad7d2c..8bb8dd78fc 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -91,7 +91,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4991,7 +4994,11 @@ static int accel_finish_startup(void) + return SUCCESS; + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 5492fd920c..6fdb475e49 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -21,7 +21,8 @@ PHP_ARG_ENABLE([opcache-jit], + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -334,7 +335,9 @@ int main() { + Optimizer/compact_vars.c \ + Optimizer/zend_dump.c \ + $ZEND_JIT_SRC, +- shared,,-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1,,yes) ++ $ext_shared,,-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1,,yes) ++ ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + + PHP_ADD_BUILD_DIR([$ext_builddir/Optimizer], 1) + PHP_ADD_EXTENSION_DEP(opcache, pcre) +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index fb921c73da..41de817bda 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index a40a4c8c37..ae93c8ae6c 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2016,6 +2016,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint32_t num_additional_modules) + { +@@ -2255,6 +2267,9 @@ int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_mod + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index bf88cdae44..f8f6547e39 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1535,6 +1535,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_81.patch b/src/globals/patch/php-src-patches/static_opcache_81.patch new file mode 100644 index 000000000..cde02c12d --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_81.patch @@ -0,0 +1,129 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea2069..3da32d8830 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 5f6b854d47..ea15c0d5bc 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -91,7 +91,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4808,7 +4811,11 @@ static int accel_finish_startup(void) + return SUCCESS; + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 2a83fa2455..7b3b37182e 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -21,7 +21,8 @@ PHP_ARG_ENABLE([opcache-jit], + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -327,7 +328,9 @@ int main() { + shared_alloc_mmap.c \ + shared_alloc_posix.c \ + $ZEND_JIT_SRC, +- shared,,"-Wno-implicit-fallthrough -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ $ext_shared,,"-Wno-implicit-fallthrough -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + + PHP_ADD_EXTENSION_DEP(opcache, pcre) + +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index 764a2edaab..95427090ce 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index 8c16f01b11..0560348a06 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2011,6 +2011,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint32_t num_additional_modules) + { +@@ -2253,6 +2265,9 @@ int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_mod + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 1a2dfe43b4..ae405f035a 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1535,6 +1535,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_82.patch b/src/globals/patch/php-src-patches/static_opcache_82.patch new file mode 100644 index 000000000..03e04dd2c --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_82.patch @@ -0,0 +1,129 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea20..77895167 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 9bcd035c..7bc01614 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -93,7 +93,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4792,7 +4795,11 @@ static int accel_finish_startup(void) + return SUCCESS; + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index b3929382..8607ff25 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -21,7 +21,8 @@ PHP_ARG_ENABLE([opcache-jit], + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -336,7 +337,9 @@ int main(void) { + shared_alloc_mmap.c \ + shared_alloc_posix.c \ + $ZEND_JIT_SRC, +- shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ $ext_shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + + PHP_ADD_EXTENSION_DEP(opcache, pcre) + +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index 764a2eda..95427090 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index 0adecd10..ee89ebfb 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2048,6 +2048,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module) + { +@@ -2293,6 +2305,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 4eece379..59b7bd5c 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1534,6 +1534,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); \ No newline at end of file diff --git a/src/globals/patch/php-src-patches/static_opcache_83.patch b/src/globals/patch/php-src-patches/static_opcache_83.patch new file mode 100644 index 000000000..ae4541ef8 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_83.patch @@ -0,0 +1,130 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea2069..3da32d8830 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index ec33c69eb2..b8ce7e3eca 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -93,7 +93,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4814,7 +4817,11 @@ static int accel_finish_startup(void) + #endif /* ZEND_WIN32 */ + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 2a83fa2455..7b3b37182e 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -27,7 +27,8 @@ + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -319,8 +320,10 @@ + shared_alloc_mmap.c \ + shared_alloc_posix.c \ + $ZEND_JIT_SRC, +- shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ $ext_shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) + ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) ++ + PHP_ADD_EXTENSION_DEP(opcache, pcre) + + if test "$have_shm_ipc" != "yes" && test "$have_shm_mmap_posix" != "yes" && test "$have_shm_mmap_anon" != "yes"; then +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index 764a2edaab..95427090ce 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index 6fdfbce13e..bcccfad6e3 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2012,6 +2012,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module) + { +@@ -2196,6 +2208,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 359c751b7b..01068efcf6 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1534,6 +1534,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_84.patch b/src/globals/patch/php-src-patches/static_opcache_84.patch new file mode 100644 index 000000000..65841eda7 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_84.patch @@ -0,0 +1,146 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea20..3da32d88 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 3e8bdea9..4a784945 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -97,7 +97,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// micro: avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4828,7 +4831,11 @@ static zend_result accel_finish_startup(void) + #endif /* ZEND_WIN32 */ + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 8f6d5ab7..19530321 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -26,8 +26,8 @@ PHP_ARG_WITH([capstone], + [no]) + + if test "$PHP_OPCACHE" != "no"; then +- dnl Always build as shared extension. +- ext_shared=yes ++ dnl Always build as shared extension. (micro patches: no, we need static) ++ dnl ext_shared=yes + + AS_VAR_IF([PHP_HUGE_CODE_PAGES], [yes], + [AC_DEFINE([HAVE_HUGE_CODE_PAGES], [1], +@@ -343,6 +343,7 @@ int main(void) { + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 $JIT_CFLAGS],, + [yes]) + ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + PHP_ADD_EXTENSION_DEP(opcache, date) + PHP_ADD_EXTENSION_DEP(opcache, pcre) + +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index d0af7258..a054e6c8 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,8 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + ADD_EXTENSION_DEP('opcache', 'date'); + ADD_EXTENSION_DEP('opcache', 'hash'); + ADD_EXTENSION_DEP('opcache', 'pcre'); +diff --git a/ext/opcache/jit/ir/ir_gdb.c b/ext/opcache/jit/ir/ir_gdb.c +index ecaf8803..a8275466 100644 +--- a/ext/opcache/jit/ir/ir_gdb.c ++++ b/ext/opcache/jit/ir/ir_gdb.c +@@ -504,11 +504,11 @@ typedef struct _ir_gdbjit_descriptor { + extern ir_gdbjit_descriptor __jit_debug_descriptor; + void __jit_debug_register_code(void); + #else +-ir_gdbjit_descriptor __jit_debug_descriptor = { ++static ir_gdbjit_descriptor __jit_debug_descriptor = { + 1, IR_GDBJIT_NOACTION, NULL, NULL + }; + +-IR_NEVER_INLINE void __jit_debug_register_code(void) ++static IR_NEVER_INLINE void __jit_debug_register_code(void) + { + __asm__ __volatile__(""); + } +diff --git a/main/main.c b/main/main.c +index 0b38f303..b2cb9d4a 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2099,6 +2099,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module) + { +@@ -2283,6 +2295,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 1a4ddbff..f47090b7 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1531,7 +1531,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + } +- ++ // micro: skip zend opcache ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_85.patch b/src/globals/patch/php-src-patches/static_opcache_85.patch new file mode 100644 index 000000000..e69de29bb diff --git a/src/globals/patch/php-src-patches/vcruntime140_74.patch b/src/globals/patch/php-src-patches/vcruntime140_74.patch new file mode 100644 index 000000000..842ffa8b3 --- /dev/null +++ b/src/globals/patch/php-src-patches/vcruntime140_74.patch @@ -0,0 +1,12 @@ +--- a/main/main.c 2020-09-29 10:17:07.000000000 +0000 ++++ b/main/main.c 2020-11-19 07:57:40.769785000 +0000 +@@ -2172,7 +2172,8 @@ int php_module_startup(sapi_module_struc + #endif + + #ifdef PHP_WIN32 +-# if PHP_LINKER_MAJOR == 14 ++// fucked here ++# if false && PHP_LINKER_MAJOR == 14 + /* Extend for other CRT if needed. */ + # if PHP_DEBUG + # define PHP_VCRUNTIME "vcruntime140d.dll" diff --git a/src/globals/patch/php-src-patches/vcruntime140_80.patch b/src/globals/patch/php-src-patches/vcruntime140_80.patch new file mode 100644 index 000000000..1430d63d9 --- /dev/null +++ b/src/globals/patch/php-src-patches/vcruntime140_80.patch @@ -0,0 +1,11 @@ +--- php-8.0.0-src/win32/winutil.c 2020-11-24 17:04:03.000000000 +0000 ++++ php-8.0.0-micro/win32/winutil.c 2020-12-03 07:59:22.177745800 +0000 +@@ -484,7 +484,7 @@ + /* Expect a CRT module handle */ + PHP_WINUTIL_API BOOL php_win32_crt_compatible(char **err) + {/*{{{*/ +-#if PHP_LINKER_MAJOR == 14 ++#if false && PHP_LINKER_MAJOR == 14 + /* Extend for other CRT if needed. */ + # if PHP_DEBUG + const char *crt_name = "vcruntime140d.dll"; diff --git a/src/globals/patch/php-src-patches/win32_74.patch b/src/globals/patch/php-src-patches/win32_74.patch new file mode 100644 index 000000000..c4a9d37e3 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_74.patch @@ -0,0 +1,21 @@ +diff -pru /mnt/c/Users/dixyes/Desktop/phiwrapaper/phibatsh/build/php-7.4.11-src/win32/build/confutils.js win32/build/confutils.js +--- a/win32/build/confutils.js 2020-09-29 10:17:06.000000000 +0000 ++++ b/win32/build/confutils.js 2020-11-20 10:07:21.642064000 +0000 +@@ -3413,7 +3413,7 @@ function toolset_setup_common_libs() + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/LDd /MDd /W3 /Od /D _DEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/LDd /MTd /W3 /Od /D _DEBUG /D ZEND_DEBUG=1 " + + (X64?"/Zi":"/ZI")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3425,7 +3425,7 @@ function toolset_setup_build_mode() + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/LD /MD /W3"); ++ ADD_FLAG("CFLAGS", "/LD /MT /W3"); + if (PHP_SANITIZER == "yes" && CLANG_TOOLSET) { + ADD_FLAG("CFLAGS", "/Od /D NDebug /D NDEBUG /D ZEND_WIN32_NEVER_INLINE /D ZEND_DEBUG=0"); + } else { diff --git a/src/globals/patch/php-src-patches/win32_80.patch b/src/globals/patch/php-src-patches/win32_80.patch new file mode 100644 index 000000000..4333bffd7 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_80.patch @@ -0,0 +1,21 @@ +diff -urN php-8.0.0-src/win32/build/confutils.js php-8.0.0-micro/win32/build/confutils.js +--- php-8.0.0-src/win32/build/confutils.js 2020-11-24 17:04:03.000000000 +0000 ++++ php-8.0.0-micro/win32/build/confutils.js 2020-12-03 06:16:12.949921700 +0000 +@@ -3407,7 +3407,7 @@ + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/LDd /MDd /Od /D _DEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/LDd /MTd /Od /D _DEBUG /D ZEND_DEBUG=1 " + + (X64?"/Zi":"/ZI")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3419,7 +3419,7 @@ + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/LD /MD"); ++ ADD_FLAG("CFLAGS", "/LD /MT"); + if (PHP_SANITIZER == "yes" && CLANG_TOOLSET) { + ADD_FLAG("CFLAGS", "/Od /D NDebug /D NDEBUG /D ZEND_WIN32_NEVER_INLINE /D ZEND_DEBUG=0"); + } else { diff --git a/src/globals/patch/php-src-patches/win32_82.patch b/src/globals/patch/php-src-patches/win32_82.patch new file mode 100644 index 000000000..d6c4de042 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_82.patch @@ -0,0 +1,22 @@ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index dc6675c6d2..587d4022a6 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -3454,7 +3454,7 @@ function toolset_setup_common_libs() + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/LDd /MDd /Od /D _DEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/LDd /MTd /Od /D _DEBUG /D ZEND_DEBUG=1 " + + (TARGET_ARCH == 'x86'?"/ZI":"/Zi")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3466,7 +3466,7 @@ function toolset_setup_build_mode() + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/LD /MD"); ++ ADD_FLAG("CFLAGS", "/LD /MT"); + if (PHP_SANITIZER == "yes" && CLANG_TOOLSET) { + ADD_FLAG("CFLAGS", "/Od /D NDebug /D NDEBUG /D ZEND_WIN32_NEVER_INLINE /D ZEND_DEBUG=0"); + } else { diff --git a/src/globals/patch/php-src-patches/win32_85.patch b/src/globals/patch/php-src-patches/win32_85.patch new file mode 100644 index 000000000..33d061844 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_85.patch @@ -0,0 +1,22 @@ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 0f97a1a2..4797967d 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -3450,7 +3450,7 @@ function toolset_setup_common_libs() + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/MDd /Od /U NDebug /U NDEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/MTd /Od /U NDebug /U NDEBUG /D ZEND_DEBUG=1 " + + (TARGET_ARCH == 'x86'?"/ZI":"/Zi")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3462,7 +3462,7 @@ function toolset_setup_build_mode() + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/MD"); ++ ADD_FLAG("CFLAGS", "/MT"); + if (PHP_SANITIZER == "yes") { + if (VS_TOOLSET) { + ADD_FLAG("CFLAGS", "/Ox /U NDebug /U NDEBUG /D ZEND_DEBUG=1"); diff --git a/src/globals/patch/php-src-patches/win32_api_80.patch b/src/globals/patch/php-src-patches/win32_api_80.patch new file mode 100644 index 000000000..82d834f2c --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_api_80.patch @@ -0,0 +1,88 @@ +diff --git a/win32/dllmain.c b/win32/dllmain.c +index a507f1e1..ab625bf3 100644 +--- a/win32/dllmain.c ++++ b/win32/dllmain.c +@@ -38,20 +38,6 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) + switch (reason) + { + case DLL_PROCESS_ATTACH: +- /* +- * We do not need to check the return value of php_win32_init_gettimeofday() +- * because the symbol bare minimum symbol we need is always available on our +- * lowest supported platform. +- * +- * On Windows 8 or greater, we use a more precise symbol to obtain the system +- * time, which is dynamically. The fallback allows us to proper support +- * Vista/7/Server 2003 R2/Server 2008/Server 2008 R2. +- * +- * Instead simply initialize the global in win32/time.c for gettimeofday() +- * use later on +- */ +- php_win32_init_gettimeofday(); +- + ret = ret && php_win32_ioutil_init(); + if (!ret) { + fprintf(stderr, "ioutil initialization failed"); +diff --git a/win32/time.c b/win32/time.c +index d1fe5145..57db914e 100644 +--- a/win32/time.c ++++ b/win32/time.c +@@ -23,42 +23,13 @@ + #include + #include "php_win32_globals.h" + +-typedef VOID (WINAPI *MyGetSystemTimeAsFileTime)(LPFILETIME lpSystemTimeAsFileTime); +- +-static MyGetSystemTimeAsFileTime timefunc = NULL; +- +-#ifdef PHP_EXPORTS +-static zend_always_inline MyGetSystemTimeAsFileTime get_time_func(void) +-{/*{{{*/ +- MyGetSystemTimeAsFileTime timefunc = NULL; +- HMODULE hMod = GetModuleHandle("kernel32.dll"); +- +- if (hMod) { +- /* Max possible resolution <1us, win8/server2012 */ +- timefunc = (MyGetSystemTimeAsFileTime)GetProcAddress(hMod, "GetSystemTimePreciseAsFileTime"); +- } +- +- if(!timefunc) { +- /* 100ns blocks since 01-Jan-1641 */ +- timefunc = (MyGetSystemTimeAsFileTime) GetSystemTimeAsFileTime; +- } +- +- return timefunc; +-}/*}}}*/ +- +-void php_win32_init_gettimeofday(void) +-{/*{{{*/ +- timefunc = get_time_func(); +-}/*}}}*/ +-#endif +- + static zend_always_inline int getfilesystemtime(struct timeval *tv) + {/*{{{*/ + FILETIME ft; + unsigned __int64 ff = 0; + ULARGE_INTEGER fft; + +- timefunc(&ft); ++ GetSystemTimePreciseAsFileTime(&ft); + + /* + * Do not cast a pointer to a FILETIME structure to either a +diff --git a/win32/time.h b/win32/time.h +index 51090ccf..77d1cbfd 100644 +--- a/win32/time.h ++++ b/win32/time.h +@@ -54,10 +54,4 @@ PHPAPI int nanosleep( const struct timespec * rqtp, struct timespec * rmtp ); + + PHPAPI int usleep(unsigned int useconds); + +-#ifdef PHP_EXPORTS +-/* This symbols are needed only for the DllMain, but should not be exported +- or be available when used with PHP binaries. */ +-void php_win32_init_gettimeofday(void); +-#endif +- + #endif diff --git a/src/globals/patch/php-src-patches/win32_api_84.patch b/src/globals/patch/php-src-patches/win32_api_84.patch new file mode 100644 index 000000000..e69de29bb diff --git a/src/globals/patch/php-src-patches/zend_stream.patch b/src/globals/patch/php-src-patches/zend_stream.patch new file mode 100644 index 000000000..09295afd2 --- /dev/null +++ b/src/globals/patch/php-src-patches/zend_stream.patch @@ -0,0 +1,12 @@ +--- php-8.0.0-src/Zend/zend_stream.c 2020-11-24 17:04:03.000000000 +0000 ++++ php-8.0.0-micro/Zend/zend_stream.c 2020-12-03 07:01:36.375355300 +0000 +@@ -23,7 +23,9 @@ + #include "zend_compile.h" + #include "zend_stream.h" + ++#if !defined(_CRT_INTERNAL_NONSTDC_NAMES) || !_CRT_INTERNAL_NONSTDC_NAMES + ZEND_DLIMPORT int isatty(int fd); ++#endif + + static ssize_t zend_stream_stdio_reader(void *handle, char *buf, size_t len) /* {{{ */ + { From 5a0fd40dc49ffca1a5889f0bed98105e1fc843de Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 1 Dec 2025 09:55:46 +0100 Subject: [PATCH 004/682] update libwebp and libxml2 --- config/source.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/source.json b/config/source.json index 9a80cd059..689594a76 100644 --- a/config/source.json +++ b/config/source.json @@ -771,8 +771,9 @@ ] }, "libwebp": { - "type": "url", - "url": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.3.2.tar.gz", + "type": "ghtagtar", + "repo": "webmproject/libwebp", + "match": "v1\\.\\d+\\.\\d+$", "provide-pre-built": true, "license": { "type": "file", @@ -780,8 +781,10 @@ } }, "libxml2": { - "type": "url", - "url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.12.5.tar.gz", + "type": "ghtagtar", + "repo": "GNOME/libxml2", + "match": "v2\\.\\d+\\.\\d+$", + "provide-pre-built": false, "license": { "type": "file", "path": "Copyright" From 7204d277b4f3e92b3b3dd9fe030b1ae7bc9b7cb5 Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 1 Dec 2025 11:39:56 +0100 Subject: [PATCH 005/682] Update PHP extensions for Linux and Darwin --- src/globals/test-extensions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index f44914ece..8a30b008e 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -50,14 +50,14 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'bcmath', + 'Linux', 'Darwin' => 'bcmath,xsl,xml', 'Windows' => 'bcmath', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'pcov', - 'Darwin' => 'pcov', + 'Linux' => '', + 'Darwin' => '', 'Windows' => '', }; From 14b822a185379b62c9daae3259bccd320942fcd7 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 16:55:52 +0100 Subject: [PATCH 006/682] don't build avx2 if we don't have it --- src/SPC/builder/unix/library/libjxl.php | 18 +++++++++++------- src/SPC/builder/unix/library/libwebp.php | 7 ++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/SPC/builder/unix/library/libjxl.php b/src/SPC/builder/unix/library/libjxl.php index 13f8481b1..4c922d9df 100644 --- a/src/SPC/builder/unix/library/libjxl.php +++ b/src/SPC/builder/unix/library/libjxl.php @@ -29,13 +29,17 @@ protected function build(): void ); if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { - $cmake->addConfigureArgs( - '-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE' - ); + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); + if (!$has_avx512) { + $cmake->addConfigureArgs( + '-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE' + ); + } } $cmake->build(); diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 46a88af49..788d069fb 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -10,8 +10,13 @@ trait libwebp { protected function build(): void { + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx2 = str_contains($cflags, '-mavx2') || str_contains($cflags, '-march=x86-64-v2') || str_contains($cflags, '-march=x86-64-v3'); UnixCMakeExecutor::create($this) - ->addConfigureArgs('-DWEBP_BUILD_EXTRAS=ON') + ->addConfigureArgs( + '-DWEBP_BUILD_EXTRAS=ON', + '-DWEBP_ENABLE_SIMD=' . ($has_avx2 ? 'ON' : 'OFF'), + ) ->build(); // patch pkgconfig $this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR); From d1041c57dcc12adef278fe592ba7e1edb5b998bd Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 17:05:50 +0100 Subject: [PATCH 007/682] remove openssl source/test dir (4.1gb?!) --- src/SPC/builder/linux/library/openssl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/linux/library/openssl.php b/src/SPC/builder/linux/library/openssl.php index d9e04b323..7a4681976 100644 --- a/src/SPC/builder/linux/library/openssl.php +++ b/src/SPC/builder/linux/library/openssl.php @@ -80,5 +80,6 @@ public function build(): void } FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc', '/Libs.private:.*/m', 'Requires.private: zlib'); FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/cmake/OpenSSL/OpenSSLConfig.cmake', '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")'); + FileSystem::removeDir($this->source_dir . '/test'); } } From 7f863d182f80811792da2eac9edf4f43eaed7677 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 17:10:56 +0100 Subject: [PATCH 008/682] don't remove dir, just don't build tests --- src/SPC/builder/linux/library/openssl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/linux/library/openssl.php b/src/SPC/builder/linux/library/openssl.php index 7a4681976..a78b6a642 100644 --- a/src/SPC/builder/linux/library/openssl.php +++ b/src/SPC/builder/linux/library/openssl.php @@ -62,6 +62,7 @@ public function build(): void "{$zlib_extra}" . 'enable-pie ' . 'no-legacy ' . + 'no-tests ' . "linux-{$arch}" ) ->exec('make clean') @@ -80,6 +81,5 @@ public function build(): void } FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc', '/Libs.private:.*/m', 'Requires.private: zlib'); FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/cmake/OpenSSL/OpenSSLConfig.cmake', '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")'); - FileSystem::removeDir($this->source_dir . '/test'); } } From b965ffcd820cfc3c319b7f41ae6170fe7ca82616 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 17:16:59 +0100 Subject: [PATCH 009/682] don't build extra programs --- src/SPC/builder/unix/library/libwebp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 788d069fb..2ad616e0b 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -14,7 +14,7 @@ protected function build(): void $has_avx2 = str_contains($cflags, '-mavx2') || str_contains($cflags, '-march=x86-64-v2') || str_contains($cflags, '-march=x86-64-v3'); UnixCMakeExecutor::create($this) ->addConfigureArgs( - '-DWEBP_BUILD_EXTRAS=ON', + '-DWEBP_BUILD_EXTRAS=OFF', '-DWEBP_ENABLE_SIMD=' . ($has_avx2 ? 'ON' : 'OFF'), ) ->build(); From c051a48d56ee79e855ebc188bf2550167c28e374 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 17:28:59 +0100 Subject: [PATCH 010/682] don't add -l:libstdc++.a if we're not actually using gcc/clang --- src/SPC/builder/extension/imagick.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/imagick.php b/src/SPC/builder/extension/imagick.php index bef772ee6..0fda41ea8 100644 --- a/src/SPC/builder/extension/imagick.php +++ b/src/SPC/builder/extension/imagick.php @@ -5,6 +5,8 @@ namespace SPC\builder\extension; use SPC\builder\Extension; +use SPC\toolchain\ToolchainManager; +use SPC\toolchain\ZigToolchain; use SPC\util\CustomExt; #[CustomExt('imagick')] @@ -19,7 +21,7 @@ public function getUnixConfigureArg(bool $shared = false): string protected function splitLibsIntoStaticAndShared(string $allLibs): array { [$static, $shared] = parent::splitLibsIntoStaticAndShared($allLibs); - if (str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset')) { + if (ToolchainManager::getToolchainClass() !== ZigToolchain::class && str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset')) { $static .= ' -l:libstdc++.a'; $shared = str_replace('-lstdc++', '', $shared); } From 150d866c1504a3a8928b1b763f980f672ea826a5 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 19:12:43 +0100 Subject: [PATCH 011/682] revert turning off sse for libwebp, need to check why debian fails building --- src/SPC/builder/extension/imagick.php | 4 +++- src/SPC/builder/unix/library/libwebp.php | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/SPC/builder/extension/imagick.php b/src/SPC/builder/extension/imagick.php index 0fda41ea8..a548a8a3d 100644 --- a/src/SPC/builder/extension/imagick.php +++ b/src/SPC/builder/extension/imagick.php @@ -21,7 +21,9 @@ public function getUnixConfigureArg(bool $shared = false): string protected function splitLibsIntoStaticAndShared(string $allLibs): array { [$static, $shared] = parent::splitLibsIntoStaticAndShared($allLibs); - if (ToolchainManager::getToolchainClass() !== ZigToolchain::class && str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset')) { + if (ToolchainManager::getToolchainClass() !== ZigToolchain::class && + (str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset')) + ) { $static .= ' -l:libstdc++.a'; $shared = str_replace('-lstdc++', '', $shared); } diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 2ad616e0b..88a07f8c3 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -10,12 +10,9 @@ trait libwebp { protected function build(): void { - $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; - $has_avx2 = str_contains($cflags, '-mavx2') || str_contains($cflags, '-march=x86-64-v2') || str_contains($cflags, '-march=x86-64-v3'); UnixCMakeExecutor::create($this) ->addConfigureArgs( - '-DWEBP_BUILD_EXTRAS=OFF', - '-DWEBP_ENABLE_SIMD=' . ($has_avx2 ? 'ON' : 'OFF'), + '-DWEBP_BUILD_EXTRAS=OFF' ) ->build(); // patch pkgconfig From 22d263c0a85ccb5e99a8a19dfcae7e8532a7ac63 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 19:27:44 +0100 Subject: [PATCH 012/682] maybe explicit mavx2?! --- src/SPC/builder/unix/library/libwebp.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 88a07f8c3..ad00259a6 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -11,6 +11,9 @@ trait libwebp protected function build(): void { UnixCMakeExecutor::create($this) + ->appendEnv([ + 'CFLAGS' => GNU_ARCH === 'x86_64' ? '-mavx2' : '', + ]) ->addConfigureArgs( '-DWEBP_BUILD_EXTRAS=OFF' ) From 5b4f4f8e55d4716cc222652376d42c3522f7b3df Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 19:55:51 +0100 Subject: [PATCH 013/682] maybe? --- src/SPC/builder/unix/library/libwebp.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index ad00259a6..54f9e7847 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -5,17 +5,25 @@ namespace SPC\builder\unix\library; use SPC\util\executor\UnixCMakeExecutor; +use SPC\util\SPCTarget; trait libwebp { protected function build(): void { UnixCMakeExecutor::create($this) - ->appendEnv([ - 'CFLAGS' => GNU_ARCH === 'x86_64' ? '-mavx2' : '', - ]) ->addConfigureArgs( - '-DWEBP_BUILD_EXTRAS=OFF' + '-DWEBP_BUILD_EXTRAS=OFF', + '-DWEBP_BUILD_ANIM_UTILS=OFF', + '-DWEBP_BUILD_CWEBP=OFF', + '-DWEBP_BUILD_DWEBP=OFF', + '-DWEBP_BUILD_GIF2WEBP=OFF', + '-DWEBP_BUILD_IMG2WEBP=OFF', + '-DWEBP_BUILD_VWEBP=OFF', + '-DWEBP_BUILD_WEBPINFO=OFF', + '-DWEBP_BUILD_WEBPMUX=OFF', + '-DWEBP_BUILD_FUZZTEST=OFF', + SPCTarget::getLibcVersion() === '2.31' && GNU_ARCH === 'x86_64' ? '-DWEBP_ENABLE_SIMD=OFF' : '' // fix an edge bug for debian 11 with gcc 10 ) ->build(); // patch pkgconfig From b8444070eea11ca75bd29764a5dfd159580e01d8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 1 Dec 2025 20:41:58 +0100 Subject: [PATCH 014/682] update go-xcaddy version automatically --- src/SPC/store/pkg/GoXcaddy.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SPC/store/pkg/GoXcaddy.php b/src/SPC/store/pkg/GoXcaddy.php index 0c1c6f8c6..93821aaa9 100644 --- a/src/SPC/store/pkg/GoXcaddy.php +++ b/src/SPC/store/pkg/GoXcaddy.php @@ -48,10 +48,10 @@ public function fetch(string $name, bool $force = false, ?array $config = null): 'macos' => 'darwin', default => throw new \InvalidArgumentException('Unsupported OS: ' . $name), }; - $go_version = '1.25.0'; + [$go_version] = explode("\n", Downloader::curlExec('https://go.dev/VERSION?m=text')); $config = [ 'type' => 'url', - 'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz", + 'url' => "https://go.dev/dl/{$go_version}.{$os}-{$arch}.tar.gz", ]; Downloader::downloadPackage($name, $config, $force); } From c38f174a6b66de3f97cbe6dca6ccab69166d4da8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 2 Dec 2025 13:42:46 +0800 Subject: [PATCH 015/682] Forward-port #978 --- src/StaticPHP/Artifact/ArtifactExtractor.php | 22 +++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index fd3204e8f..86e2951fd 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -571,18 +571,30 @@ protected function unzipWithStrip(string $zip_file, string $extract_path): void } /** - * Move file or directory to destination. + * Move file or directory, handling cross-device scenarios + * Uses rename() if possible, falls back to copy+delete for cross-device moves + * + * @param string $source Source path + * @param string $dest Destination path */ - protected function moveFileOrDir(string $source, string $dest): void + private static function moveFileOrDir(string $source, string $dest): void { $source = FileSystem::convertPath($source); $dest = FileSystem::convertPath($dest); - // Try rename first (fast, atomic) - if (@rename($source, $dest)) { - return; + // Check if source and dest are on the same device to avoid cross-device rename errors + $source_stat = @stat($source); + $dest_parent = dirname($dest); + $dest_stat = @stat($dest_parent); + + // Only use rename if on same device + if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) { + if (@rename($source, $dest)) { + return; + } } + // Fall back to copy + delete for cross-device moves or if rename failed if (is_dir($source)) { FileSystem::copyDir($source, $dest); FileSystem::removeDir($source); From 719d818fd1e75be83a9b34c90551c7b1ed0bb0a8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 2 Dec 2025 21:34:32 +0100 Subject: [PATCH 016/682] we need to check for structure of pdo_sqlsrv extension --- src/SPC/builder/extension/pdo_sqlsrv.php | 26 ++++++++ src/SPC/store/FileSystem.php | 76 ++++++++++++------------ 2 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 src/SPC/builder/extension/pdo_sqlsrv.php diff --git a/src/SPC/builder/extension/pdo_sqlsrv.php b/src/SPC/builder/extension/pdo_sqlsrv.php new file mode 100644 index 000000000..32697b169 --- /dev/null +++ b/src/SPC/builder/extension/pdo_sqlsrv.php @@ -0,0 +1,26 @@ +source_dir . '/config.m4') && is_dir($this->source_dir . '/source/pdo_sqlsrv')) { + FileSystem::moveFileOrDir($this->source_dir . '/LICENSE', $this->source_dir . '/source/pdo_sqlsrv/LICENSE'); + FileSystem::moveFileOrDir($this->source_dir . '/source/shared', $this->source_dir . '/source/pdo_sqlsrv/shared'); + FileSystem::moveFileOrDir($this->source_dir . '/source/pdo_sqlsrv', SOURCE_PATH . '/pdo_sqlsrv'); + FileSystem::removeDir($this->source_dir); + FileSystem::moveFileOrDir(SOURCE_PATH . '/pdo_sqlsrv', $this->source_dir); + return true; + } + return false; + } +} diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 3b88a2bce..f6c538bdf 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -572,6 +572,44 @@ public static function replaceFileLineContainsString(string $file, string $find, return file_put_contents($file, implode('', $lines)); } + /** + * Move file or directory, handling cross-device scenarios + * Uses rename() if possible, falls back to copy+delete for cross-device moves + * + * @param string $source Source path + * @param string $dest Destination path + */ + public static function moveFileOrDir(string $source, string $dest): void + { + $source = self::convertPath($source); + $dest = self::convertPath($dest); + + // Check if source and dest are on the same device to avoid cross-device rename errors + $source_stat = @stat($source); + $dest_parent = dirname($dest); + $dest_stat = @stat($dest_parent); + + // Only use rename if on same device + if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) { + if (@rename($source, $dest)) { + return; + } + } + + // Fall back to copy + delete for cross-device moves or if rename failed + if (is_dir($source)) { + self::copyDir($source, $dest); + self::removeDir($source); + } else { + if (!copy($source, $dest)) { + throw new FileSystemException("Failed to copy file from {$source} to {$dest}"); + } + if (!unlink($source)) { + throw new FileSystemException("Failed to remove source file: {$source}"); + } + } + } + private static function extractArchive(string $filename, string $target): void { // Create base dir @@ -648,44 +686,6 @@ private static function extractWithType(string $source_type, string $filename, s }; } - /** - * Move file or directory, handling cross-device scenarios - * Uses rename() if possible, falls back to copy+delete for cross-device moves - * - * @param string $source Source path - * @param string $dest Destination path - */ - private static function moveFileOrDir(string $source, string $dest): void - { - $source = self::convertPath($source); - $dest = self::convertPath($dest); - - // Check if source and dest are on the same device to avoid cross-device rename errors - $source_stat = @stat($source); - $dest_parent = dirname($dest); - $dest_stat = @stat($dest_parent); - - // Only use rename if on same device - if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) { - if (@rename($source, $dest)) { - return; - } - } - - // Fall back to copy + delete for cross-device moves or if rename failed - if (is_dir($source)) { - self::copyDir($source, $dest); - self::removeDir($source); - } else { - if (!copy($source, $dest)) { - throw new FileSystemException("Failed to copy file from {$source} to {$dest}"); - } - if (!unlink($source)) { - throw new FileSystemException("Failed to remove source file: {$source}"); - } - } - } - /** * Unzip file with stripping top-level directory */ From 98773ee5a6fab2c7366cf9e8c4024ff7043ca92e Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 3 Dec 2025 15:02:14 +0100 Subject: [PATCH 017/682] zig toolchain can always use libc --- src/SPC/builder/linux/library/liburing.php | 23 +++++++++------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/SPC/builder/linux/library/liburing.php b/src/SPC/builder/linux/library/liburing.php index 82249cf80..9a67f50c4 100644 --- a/src/SPC/builder/linux/library/liburing.php +++ b/src/SPC/builder/linux/library/liburing.php @@ -6,6 +6,8 @@ use SPC\builder\linux\SystemUtil; use SPC\store\FileSystem; +use SPC\toolchain\GccNativeToolchain; +use SPC\toolchain\ToolchainManager; use SPC\util\executor\UnixAutoconfExecutor; use SPC\util\SPCTarget; @@ -15,26 +17,19 @@ class liburing extends LinuxLibraryBase public function patchBeforeBuild(): bool { - if (!SystemUtil::isMuslDist()) { - return false; + if (SystemUtil::isMuslDist()) { + FileSystem::replaceFileStr($this->source_dir . '/configure', 'realpath -s', 'realpath'); + return true; } - FileSystem::replaceFileStr($this->source_dir . '/configure', 'realpath -s', 'realpath'); - return true; + return false; } protected function build(): void { - $use_libc = SPCTarget::getLibc() !== 'glibc' || version_compare(SPCTarget::getLibcVersion(), '2.30', '>='); + $use_libc = ToolchainManager::getToolchainClass() !== GccNativeToolchain::class || version_compare(SPCTarget::getLibcVersion(), '2.30', '>='); $make = UnixAutoconfExecutor::create($this); - if (!$use_libc) { - $make->appendEnv([ - 'CC' => 'gcc', // libc-less version fails to compile with clang or zig - 'CXX' => 'g++', - 'AR' => 'ar', - 'LD' => 'ld', - ]); - } else { + if ($use_libc) { $make->appendEnv([ 'CFLAGS' => '-D_GNU_SOURCE', ]); @@ -51,7 +46,7 @@ protected function build(): void $use_libc ? '--use-libc' : '', ) ->configure() - ->make('library', 'install ENABLE_SHARED=0', with_clean: false); + ->make('library ENABLE_SHARED=0', 'install ENABLE_SHARED=0', with_clean: false); $this->patchPkgconfPrefix(); } From daa87e135018b828136a00cdf2aeb1cddee5c52a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 10:53:49 +0800 Subject: [PATCH 018/682] Add DirDiff utility and enhance package build process - Introduced DirDiff class for tracking directory file changes. - Updated ConsoleApplication to use addCommand for build targets. - Enhanced PackageBuilder with methods for deploying binaries and extracting debug info. - Improved package installation logic to support shared extensions. - Added readline extension with patching for static builds. --- bin/spc-alpine-docker | 3 +- src/Package/Extension/readline.php | 34 +++ src/Package/Library/imap.php | 24 ++ src/Package/Target/php.php | 245 +++++++++++++++++- src/StaticPHP/ConsoleApplication.php | 3 +- src/StaticPHP/Package/PackageBuilder.php | 88 +++++++ src/StaticPHP/Package/PackageInstaller.php | 13 +- src/StaticPHP/Package/PackageLoader.php | 4 +- src/StaticPHP/Package/PhpExtensionPackage.php | 5 + src/StaticPHP/Registry/Registry.php | 6 + src/StaticPHP/Util/DirDiff.php | 95 +++++++ src/StaticPHP/Util/SourcePatcher.php | 35 +++ 12 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 src/Package/Extension/readline.php create mode 100644 src/Package/Library/imap.php create mode 100644 src/StaticPHP/Util/DirDiff.php diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker index 2790a5c34..2640ffbad 100755 --- a/bin/spc-alpine-docker +++ b/bin/spc-alpine-docker @@ -3,7 +3,7 @@ set -e # This file is using docker to run commands -SPC_DOCKER_VERSION=v6 +SPC_DOCKER_VERSION=v7 # Detect docker can run if ! which docker >/dev/null; then @@ -123,6 +123,7 @@ COPY ./composer.* /app/ ADD ./bin /app/bin RUN composer install --no-dev ADD ./config /app/config +ADD ./spc.registry.json /app/spc.registry.json RUN bin/spc doctor --auto-fix RUN bin/spc install-pkg upx diff --git a/src/Package/Extension/readline.php b/src/Package/Extension/readline.php new file mode 100644 index 000000000..2ecc533a5 --- /dev/null +++ b/src/Package/Extension/readline.php @@ -0,0 +1,34 @@ +isStatic()) { + $php_src = $installer->getBuildPackage('php')->getSourceDir(); + SourcePatcher::patchFile('musl_static_readline.patch', $php_src); + } + } + + #[AfterStage('php', 'unix-make-cli')] + public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void + { + if ($toolchain->isStatic()) { + $php_src = $installer->getBuildPackage('php')->getSourceDir(); + SourcePatcher::patchFile('musl_static_readline.patch', $php_src, true); + } + } +} diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php new file mode 100644 index 000000000..58e9397fe --- /dev/null +++ b/src/Package/Library/imap.php @@ -0,0 +1,24 @@ +isBuildPackage('php-cli')) { $package->runStage('unix-make-cli'); } + if ($installer->isBuildPackage('php-cgi')) { + $package->runStage('unix-make-cgi'); + } if ($installer->isBuildPackage('php-fpm')) { $package->runStage('unix-make-fpm'); } - if ($installer->isBuildPackage('php-cgi')) { - $package->runStage('unix-make-cgi'); + if ($installer->isBuildPackage('php-micro')) { + $package->runStage('unix-make-micro'); + } + if ($installer->isBuildPackage('php-embed')) { + $package->runStage('unix-make-embed'); } } @@ -330,9 +338,103 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install ->exec("make -j{$concurrency} cli"); } + #[Stage('unix-make-cgi')] + 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"); + } + + #[Stage('unix-make-fpm')] + 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"); + } + + #[Stage('unix-make-micro')] + 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"); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } + } + + #[Stage('unix-make-embed')] + public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + $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; + shell()->cd($package->getSourceDir()) + ->setEnv($this->makeVars($installer)) + ->exec('sed -i "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); + } + + // process shared extensions that built-with-php + $increment_files = $diff->getChangedFiles(); + foreach ($increment_files as $increment_file) { + $builder->deployBinary($increment_file, $libphp_so, false); + } + + // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- + + // process libphp.a for static embed + $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('patch-embed-scripts'); + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] - public function build(TargetPackage $package): void + public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void { // virtual target, do nothing if ($package->getName() !== 'php') { @@ -342,6 +444,68 @@ public function build(TargetPackage $package): void $package->runStage('unix-buildconf'); $package->runStage('unix-configure'); $package->runStage('unix-make'); + + // 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 { + foreach ($shared_extensions as $extension) { + logger()->info('Building shared extensions...'); + $extension->buildSharedExtension(); + } + } finally { + // restore php-config + if (!empty($shared_extensions)) { + FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config'); + FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4'); + } + } + } + + /** + * Patch phpize and php-config if needed + */ + #[Stage('patch-embed-scripts')] + public function patchPhpScripts(): 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#'); + } + // 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); + } } /** @@ -381,6 +545,10 @@ private function makeStaticExtensionString(PackageInstaller $installer): string 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())); @@ -394,4 +562,75 @@ private function makeVars(PackageInstaller $installer): array '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/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index b60fd10eb..1f63190b3 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -35,10 +35,11 @@ public function __construct() // only add target that contains artifact.source if ($package->hasStage('build')) { logger()->debug("Registering build target command for package: {$name}"); - $this->add(new BuildTargetCommand($name)); + $this->addCommand(new BuildTargetCommand($name)); } } + // add core commands $this->addCommands([ new DownloadCommand(), new DoctorCommand(), diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index b40888722..c87b7f29b 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -9,9 +9,11 @@ use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\WrongUsageException; 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; class PackageBuilder { @@ -85,6 +87,92 @@ public function getOption(string $key, mixed $default = null): mixed return $this->options[$key] ?? $default; } + /** + * Deploy the binary file from src to dst. + */ + public function deployBinary(string $src, string $dst, bool $executable = true): string + { + logger()->debug("Deploying binary from {$src} to {$dst}"); + + // file must exists + if (!file_exists($src)) { + throw new SPCInternalException("Deploy failed. Cannot find file: {$src}"); + } + // dst dir must exists + FileSystem::createDir(dirname($dst)); + + // ignore copy to self + if (realpath($src) !== realpath($dst)) { + shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); + } + + // file exist + if (!file_exists($dst)) { + throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); + } + + // extract debug info + $this->extractDebugInfo($dst); + + // strip + if (!$this->getOption('no-strip')) { + $this->stripBinary($dst); + } + + // UPX for linux + $upx_option = $this->getOption('with-upx-pack'); + if ($upx_option && SystemTarget::getTargetOS() === 'Linux' && $executable) { + if ($this->getOption('no-strip')) { + logger()->warning('UPX compression is not recommended when --no-strip is enabled.'); + } + logger()->info("Compressing {$dst} with UPX"); + shell()->exec(getenv('UPX_EXEC') . " --best {$dst}"); + } + + return $dst; + } + + /** + * Extract debug information from binary file. + * + * @param string $binary_path the path to the binary file, including executables, shared libraries, etc + */ + 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') { + shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); + } elseif (SystemTarget::getTargetOS() === 'Linux') { + if ($eu_strip = LinuxUtil::findCommand('eu-strip')) { + shell() + ->exec("{$eu_strip} -f {$debug_file} {$binary_path}") + ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + } else { + shell() + ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}") + ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + } + } else { + throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS'); + } + return $debug_file; + } + + /** + * Strip unneeded symbols from binary file. + */ + public function stripBinary(string $binary_path): void + { + shell()->exec(match (SystemTarget::getTargetOS()) { + 'Darwin' => "strip -S {$binary_path}", + 'Linux' => "strip --strip-unneeded {$binary_path}", + default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'), + }); + } + private function installLicense(Package $package, array $license): void { $dir = BUILD_ROOT_PATH . '/source-licenses/' . $package->getName(); diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 15bab0dc9..34e246214 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -195,15 +195,20 @@ public function isBuildPackage(Package|string $package): bool /** * Get all resolved packages. + * You can filter by package type class if needed. * - * @return array + * @template T + * @param class-string $package_type Filter by package type + * @return array */ - public function getResolvedPackages(): array + public function getResolvedPackages(mixed $package_type = Package::class): array { - return $this->packages; + return array_filter($this->packages, function (Package $pkg) use ($package_type): bool { + return $pkg instanceof $package_type; + }); } - public function isPackageBeingResolved(string $package_name): bool + public function isPackageResolved(string $package_name): bool { return isset($this->packages[$package_name]); } diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Package/PackageLoader.php index cdbb21962..c89bf392c 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Package/PackageLoader.php @@ -232,7 +232,7 @@ public static function getBeforeStageCallbacks(string $package_name, string $sta $installer = ApplicationContext::get(PackageInstaller::class); $stages = self::$before_stages[$package_name][$stage] ?? []; foreach ($stages as [$callback, $only_when_package_resolved]) { - if ($only_when_package_resolved !== null && !$installer->isPackageBeingResolved($only_when_package_resolved)) { + if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } yield $callback; @@ -246,7 +246,7 @@ public static function getAfterStageCallbacks(string $package_name, string $stag $stages = self::$after_stage[$package_name][$stage] ?? []; $result = []; foreach ($stages as [$callback, $only_when_package_resolved]) { - if ($only_when_package_resolved !== null && !$installer->isPackageBeingResolved($only_when_package_resolved)) { + if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } $result[] = $callback; diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 673d1c82f..562d8f8e4 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -107,4 +107,9 @@ public function isBuildWithPhp(): bool { return $this->build_with_php; } + + public function buildSharedExtension(): void + { + // TODO: build common shared extensions code here... + } } diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 8e598863f..57ac82d11 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -252,6 +252,12 @@ private static function requireClassFile(string $class, ?string $file_path, stri ); } + /** + * Return full path, resolving relative paths against a base path. + * + * @param string $path Input path (relative or absolute) + * @param string $relative_path_base Base path for relative paths + */ private static function fullpath(string $path, string $relative_path_base): string { if (FileSystem::isRelativePath($path)) { diff --git a/src/StaticPHP/Util/DirDiff.php b/src/StaticPHP/Util/DirDiff.php new file mode 100644 index 000000000..7f80f3640 --- /dev/null +++ b/src/StaticPHP/Util/DirDiff.php @@ -0,0 +1,95 @@ +reset(); + } + + /** + * Reset the baseline to current state. + */ + public function reset(): void + { + $this->before = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + + if ($this->track_content_changes) { + $this->before_file_hashes = []; + foreach ($this->before as $file) { + $this->before_file_hashes[$file] = md5_file($this->dir . DIRECTORY_SEPARATOR . $file); + } + } + } + + /** + * Get the list of incremented files. + * + * @param bool $relative Return relative paths or absolute paths + * @return array List of incremented files + */ + public function getIncrementFiles(bool $relative = false): array + { + $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + $diff = array_diff($after, $this->before); + if ($relative) { + return $diff; + } + return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $diff); + } + + /** + * Get the list of changed files (including new files). + * + * @param bool $relative Return relative paths or absolute paths + * @param bool $include_new_files Include new files as changed files + * @return array List of changed files + */ + public function getChangedFiles(bool $relative = false, bool $include_new_files = true): array + { + $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + $changed = []; + foreach ($after as $file) { + if (isset($this->before_file_hashes[$file])) { + $after_hash = md5_file($this->dir . DIRECTORY_SEPARATOR . $file); + if ($after_hash !== $this->before_file_hashes[$file]) { + $changed[] = $file; + } + } elseif ($include_new_files) { + // New file, consider as changed + $changed[] = $file; + } + } + if ($relative) { + return $changed; + } + return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $changed); + } + + /** + * Get the list of removed files. + * + * @param bool $relative Return relative paths or absolute paths + * @return array List of removed files + */ + public function getRemovedFiles(bool $relative = false): array + { + $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + $removed = array_diff($this->before, $after); + if ($relative) { + return $removed; + } + return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $removed); + } +} diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 6f57cb00a..42ea8c369 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -159,4 +159,39 @@ public static function unpatchHardcodedINI(string $php_source_dir): bool return $result; } + + /** + * Patch micro SAPI to support compressed phar loading from the current executable. + * + * @param int $version_id PHP version ID + */ + public static function patchMicroPhar(int $version_id): void + { + FileSystem::backupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/phar/phar.c', + 'static zend_op_array *phar_compile_file', + "char *micro_get_filename(void);\n\nstatic zend_op_array *phar_compile_file" + ); + if ($version_id < 80100) { + // PHP 8.0.x + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/phar/phar.c', + 'if (strstr(file_handle->filename, ".phar") && !strstr(file_handle->filename, "://")) {', + 'if ((strstr(file_handle->filename, micro_get_filename()) || strstr(file_handle->filename, ".phar")) && !strstr(file_handle->filename, "://")) {' + ); + } else { + // PHP >= 8.1 + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/phar/phar.c', + 'if (strstr(ZSTR_VAL(file_handle->filename), ".phar") && !strstr(ZSTR_VAL(file_handle->filename), "://")) {', + 'if ((strstr(ZSTR_VAL(file_handle->filename), micro_get_filename()) || strstr(ZSTR_VAL(file_handle->filename), ".phar")) && !strstr(ZSTR_VAL(file_handle->filename), "://")) {' + ); + } + } + + public static function unpatchMicroPhar(): void + { + FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); + } } From 71d803d36fd9d9d2d7b807e6eba8d1216b9e8054 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 10:54:05 +0800 Subject: [PATCH 019/682] cs fix --- src/StaticPHP/Artifact/Artifact.php | 2 +- src/StaticPHP/Artifact/ArtifactExtractor.php | 30 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 778e9c600..5e5e8b558 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -493,7 +493,7 @@ public function emitAfterSourceExtract(string $target_path): void * Emit all after binary extract callbacks for the specified platform. * * @param null|string $target_path The directory where binary was extracted - * @param string $platform The platform string (e.g., 'linux-x86_64') + * @param string $platform The platform string (e.g., 'linux-x86_64') */ public function emitAfterBinaryExtract(?string $target_path, string $platform): void { diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 86e2951fd..ac0e6ae2c 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -570,6 +570,21 @@ protected function unzipWithStrip(string $zip_file, string $extract_path): void FileSystem::removeDir($temp_dir); } + /** + * Replace path variables. + */ + protected function replacePathVariables(string $path): string + { + $replacement = [ + '{pkg_root_path}' => PKG_ROOT_PATH, + '{build_root_path}' => BUILD_ROOT_PATH, + '{source_path}' => SOURCE_PATH, + '{download_path}' => DOWNLOAD_PATH, + '{working_dir}' => WORKING_DIR, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $path); + } + /** * Move file or directory, handling cross-device scenarios * Uses rename() if possible, falls back to copy+delete for cross-device moves @@ -608,21 +623,6 @@ private static function moveFileOrDir(string $source, string $dest): void } } - /** - * Replace path variables. - */ - protected function replacePathVariables(string $path): string - { - $replacement = [ - '{pkg_root_path}' => PKG_ROOT_PATH, - '{build_root_path}' => BUILD_ROOT_PATH, - '{source_path}' => SOURCE_PATH, - '{download_path}' => DOWNLOAD_PATH, - '{working_dir}' => WORKING_DIR, - ]; - return str_replace(array_keys($replacement), array_values($replacement), $path); - } - private function copyFile(string $source_file, string $target_path): void { FileSystem::createDir(dirname($target_path)); From 2f8570b59e63f1931871ef8de25be93cc4a19975 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:19:11 +0800 Subject: [PATCH 020/682] Implement missing legacy options --- src/StaticPHP/Command/DownloadCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 967d38858..ab8e00a8e 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -68,7 +68,8 @@ public function handle(): int } // resolve package dependencies and get artifacts directly - $resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests')); + $suggests = !($this->getOption('without-suggests') || $this->getOption('without-suggestions')); + $resolved = DependencyResolver::resolve($packages, [], $suggests); foreach ($resolved as $pkg_name) { $pkg = PackageLoader::getPackage($pkg_name); if ($artifact = $pkg->getArtifact()) { From e9d3f7e7ebc0a4c57ed77c37bff54ab22e415ed0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:19:34 +0800 Subject: [PATCH 021/682] Change wrong option name --- src/StaticPHP/Command/ExtractCommand.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index b897e59af..d28d2bbe3 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -29,14 +29,14 @@ public function configure(): void $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Extract artifacts for libraries, e.g "libcares,openssl"'); $this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Extract artifacts for packages, e.g "php,libssl,libcurl"'); $this->addOption('without-suggests', null, null, 'Do not include suggested packages when using --for-extensions'); - $this->addOption('force-source', null, null, 'Force extract source even if binary is available'); + $this->addOption('source-only', null, null, 'Force extract source even if binary is available'); } public function handle(): int { $cache = ApplicationContext::get(ArtifactCache::class); $extractor = new ArtifactExtractor($cache); - $force_source = (bool) $this->getOption('force-source'); + $force_source = (bool) $this->getOption('source-only'); $artifacts = []; @@ -59,6 +59,9 @@ public function handle(): int $packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts)); // Include php package when using for-extensions array_unshift($packages, 'php'); + array_unshift($packages, 'php-micro'); + array_unshift($packages, 'php-embed'); + array_unshift($packages, 'php-fpm'); } if ($libs = $this->getOption('for-libs')) { $packages = array_merge($packages, parse_comma_list($libs)); From 20892ab1940df01243242017321dee8e29bbe714 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:19:55 +0800 Subject: [PATCH 022/682] Auto-append prefix for php-extension packages --- src/StaticPHP/Attribute/Package/Extension.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Attribute/Package/Extension.php b/src/StaticPHP/Attribute/Package/Extension.php index b55430fb2..7ce6a65b1 100644 --- a/src/StaticPHP/Attribute/Package/Extension.php +++ b/src/StaticPHP/Attribute/Package/Extension.php @@ -8,7 +8,12 @@ * Indicates that the annotated class defines a PHP extension. */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] -readonly class Extension +class Extension { - public function __construct(public string $name) {} + public function __construct(public string $name) + { + if (!str_starts_with($name, 'ext-')) { + $this->name = "ext-{$name}"; + } + } } From dc5bf6dc98d55b7d07e18c8f3a03c7f69652ed21 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:20:22 +0800 Subject: [PATCH 023/682] Correct install-pkg argument name, add alias --- src/StaticPHP/Command/InstallPackageCommand.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 37cb04b37..754d6ef29 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -8,19 +8,19 @@ use StaticPHP\Package\PackageInstaller; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand('install-pkg')] +#[AsCommand('install-pkg', 'Install additional packages', ['i', 'install-package'])] class InstallPackageCommand extends BaseCommand { public function configure() { - $this->addArgument('package', null, 'The package to install (name or path)'); + $this->addArgument('packages', null, 'The package to install (name or path)'); } public function handle(): int { ApplicationContext::set('elephant', true); $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); - $installer->addInstallPackage($this->input->getArgument('package')); + $installer->addInstallPackage($this->input->getArgument('packages')); $installer->run(true, true); return static::SUCCESS; } From 64fde5fd8c7372e4198e780408f84b5648bd5ce8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:20:44 +0800 Subject: [PATCH 024/682] Allow loading config dir from registry --- src/StaticPHP/Registry/Registry.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 57ac82d11..1b579ef2c 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -87,7 +87,11 @@ public static function loadRegistry(string $registry_file, bool $auto_require = if (isset($data['package']['config']) && is_array($data['package']['config'])) { foreach ($data['package']['config'] as $path) { $path = self::fullpath($path, dirname($registry_file)); - PackageConfig::loadFromFile($path); + if (is_file($path)) { + PackageConfig::loadFromFile($path); + } elseif (is_dir($path)) { + PackageConfig::loadFromDir($path); + } } } @@ -95,7 +99,11 @@ public static function loadRegistry(string $registry_file, bool $auto_require = if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) { foreach ($data['artifact']['config'] as $path) { $path = self::fullpath($path, dirname($registry_file)); - ArtifactConfig::loadFromFile($path); + if (is_file($path)) { + ArtifactConfig::loadFromFile($path); + } elseif (is_dir($path)) { + ArtifactConfig::loadFromDir($path); + } } } @@ -232,7 +240,7 @@ private static function parseClassEntry(int|string $key, string $value): array */ private static function requireClassFile(string $class, ?string $file_path, string $base_path, bool $auto_require): void { - if (!$auto_require || class_exists($class, true)) { + if (!$auto_require || class_exists($class)) { return; } From ee46c1c387fd63b65a538b25a91468984e902ff6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:21:27 +0800 Subject: [PATCH 025/682] Fix switch-php-version command not working bug --- src/Package/Command/SwitchPhpVersionCommand.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php index 0e40c43ea..38649dd75 100644 --- a/src/Package/Command/SwitchPhpVersionCommand.php +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -67,13 +67,11 @@ public function handle(): int InteractiveTerm::finish('Removed: ' . $source_dir); } - // Set the PHP version for download - // This defines the version that will be used when resolving php-src artifact - define('SPC_BUILD_PHP_VERSION', $php_ver); - // Download new PHP source $this->output->writeln("Downloading PHP {$php_ver} source..."); + $this->input->setOption('with-php', $php_ver); + $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->input->getOptions()); $downloader = new ArtifactDownloader($downloaderOptions); From d16f5a972c9a6263ec10afeabc77496bde7974b6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 4 Dec 2025 21:21:48 +0800 Subject: [PATCH 026/682] Add --with-packages option for spc-config command --- src/StaticPHP/Command/SPCConfigCommand.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Command/SPCConfigCommand.php b/src/StaticPHP/Command/SPCConfigCommand.php index 399e1eae2..0a242c608 100644 --- a/src/StaticPHP/Command/SPCConfigCommand.php +++ b/src/StaticPHP/Command/SPCConfigCommand.php @@ -18,6 +18,7 @@ public function configure(): void { $this->addArgument('extensions', InputArgument::OPTIONAL, 'The extensions will be compiled, comma separated'); $this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', ''); + $this->addOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', ''); $this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs'); $this->addOption('with-suggests', null, null, 'Build with suggested packages for selected exts and libs'); $this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts'); @@ -31,15 +32,16 @@ public function configure(): void public function handle(): int { // transform string to array - $libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs')))); + $libraries = parse_comma_list($this->getOption('with-libs')); + $libraries = array_merge($libraries, $this->getOption('with-packages')); // transform string to array $extensions = $this->getArgument('extensions') ? parse_extension_list($this->getArgument('extensions')) : []; $include_suggests = $this->getOption('with-suggests') ?: $this->getOption('with-suggested-libs') || $this->getOption('with-suggested-exts'); $util = new SPCConfigUtil(options: [ - 'no_php' => $this->getOption('no-php'), - 'libs_only_deps' => $this->getOption('libs-only-deps'), - 'absolute_libs' => $this->getOption('absolute-libs'), + 'no_php' => (bool) $this->getOption('no-php'), + 'libs_only_deps' => (bool) $this->getOption('libs-only-deps'), + 'absolute_libs' => (bool) $this->getOption('absolute-libs'), ]); $packages = array_merge(array_map(fn ($x) => "ext-{$x}", $extensions), $libraries); $config = $util->config($packages, $include_suggests); From c9259149257eb06b2e1960a03b3a2c29fa88321f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 5 Dec 2025 11:15:04 +0800 Subject: [PATCH 027/682] Add version getter and checksum for go-xcaddy artifact --- src/Package/Artifact/go_xcaddy.php | 40 +++++++++++++++-------- src/StaticPHP/Command/DownloadCommand.php | 2 ++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index fd7ef2a4f..152dc2e5c 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; -use StaticPHP\Exception\ValidationException; +use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\System\LinuxUtil; @@ -28,29 +28,41 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $arch = match (explode('-', $name)[1]) { 'x86_64' => 'amd64', 'aarch64' => 'arm64', - default => throw new ValidationException('Unsupported architecture: ' . $name), + default => throw new DownloaderException('Unsupported architecture: ' . $name), }; $os = match (explode('-', $name)[0]) { 'linux' => 'linux', 'macos' => 'darwin', - default => throw new ValidationException('Unsupported OS: ' . $name), + default => throw new DownloaderException('Unsupported OS: ' . $name), }; - $hash = match ("{$os}-{$arch}") { - 'linux-amd64' => '2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613', - 'linux-arm64' => '05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae', - 'darwin-amd64' => '5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef', - 'darwin-arm64' => '544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c', - }; - $go_version = '1.25.0'; - $url = "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz"; - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "go{$go_version}.{$os}-{$arch}.tar.gz"; + + // get version and hash + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + $page = default_shell()->executeCurl('https://go.dev/dl/'); + if ($page === '' || $page === false) { + throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); + } + + $version_regex = str_replace('.', '\.', $version); + $pattern = "/href=\"\\/dl\\/{$version_regex}\\.{$os}-{$arch}\\.tar\\.gz\">.*?([a-f0-9]{64})<\\/tt>/s"; + if (preg_match($pattern, $page, $matches)) { + $hash = $matches[1]; + } else { + throw new DownloaderException("Failed to find download hash for Go {$version} {$os}-{$arch}"); + } + + $url = "https://go.dev/dl/{$version}.{$os}-{$arch}.tar.gz"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.{$os}-{$arch}.tar.gz"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); // verify hash $file_hash = hash_file('sha256', $path); if ($file_hash !== $hash) { - throw new ValidationException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); + throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $go_version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $go_version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); } #[AfterBinaryExtract('go-xcaddy', [ diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index ab8e00a8e..92b80be17 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -27,6 +27,8 @@ public function configure(): void $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Fetch by libraries, e.g "libcares,openssl,onig"'); $this->addOption('without-suggests', null, null, 'Do not fetch suggested sources when using --for-extensions'); + $this->addOption('without-suggestions', null, null, '(deprecated) Do not fetch suggested sources when using --for-extensions'); + // download command specific options $this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); $this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Fetch by packages, e.g "php,libssl,libcurl"'); From 52553fb5ed2af947fd20383d256902f432d557b6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 5 Dec 2025 12:05:13 +0800 Subject: [PATCH 028/682] Fix PHPStan errors --- src/StaticPHP/Util/FileSystem.php | 11 ----------- src/StaticPHP/Util/SPCConfigUtil.php | 29 +++++++++++++--------------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index d1e43de23..46d15a1ed 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -370,17 +370,6 @@ public static function resetDir(string $dir_name): void self::createDir($dir_name); } - /** - * Add source extraction hook - * - * @param string $name Source name - * @param callable $callback Callback function - */ - public static function addSourceExtractHook(string $name, callable $callback): void - { - self::$_extract_hook[$name][] = $callback; - } - /** * Check if path is relative * diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index b3e073c99..317258d03 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -6,6 +6,8 @@ use StaticPHP\Config\PackageConfig; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Runtime\SystemTarget; class SPCConfigUtil @@ -99,26 +101,21 @@ public function config(array $packages = [], bool $include_suggests = false): ar * [Helper function] * Get configuration for a specific extension(s) dependencies. * - * @param Extension|Extension[] $extension Extension instance or list - * @param bool $include_suggest_ext Whether to include suggested extensions - * @param bool $include_suggest_lib Whether to include suggested libraries + * @param array|PhpExtensionPackage $extension_packages Extension instance or list * @return array{ * cflags: string, * ldflags: string, * libs: string * } */ - public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array { - if (!is_array($extension)) { - $extension = [$extension]; + if (!is_array($extension_packages)) { + $extension_packages = [$extension_packages]; } - $libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension))); return $this->config( - extensions: array_map(fn ($x) => $x->getName(), $extension), - libraries: $libs, - include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false, - include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + packages: array_map(fn ($y) => $y->getName(), $extension_packages), + include_suggests: $include_suggests, ); } @@ -126,15 +123,15 @@ public function getExtensionConfig(array|Extension $extension, bool $include_sug * [Helper function] * Get configuration for a specific library(s) dependencies. * - * @param LibraryBase|LibraryBase[] $lib Library instance or list - * @param bool $include_suggest_lib Whether to include suggested libraries + * @param array|LibraryPackage $lib Library instance or list + * @param bool $include_suggests Whether to include suggested libraries * @return array{ * cflags: string, * ldflags: string, * libs: string * } */ - public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array + public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array { if (!is_array($lib)) { $lib = [$lib]; @@ -144,8 +141,8 @@ public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_l $save_libs_only_deps = $this->libs_only_deps; $this->libs_only_deps = true; $ret = $this->config( - libraries: array_map(fn ($x) => $x->getName(), $lib), - include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + packages: array_map(fn ($y) => $y->getName(), $lib), + include_suggests: $include_suggests, ); $this->no_php = $save_no_php; $this->libs_only_deps = $save_libs_only_deps; From 7fa6fd08d4a3b74173525c60e7cd2308f24d89ba Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 5 Dec 2025 14:39:05 +0800 Subject: [PATCH 029/682] Add HostedPackageBin downloader and enhance artifact handling --- src/StaticPHP/Artifact/ArtifactDownloader.php | 27 ++++---- .../Downloader/Type/GitHubRelease.php | 20 ++++++ .../Downloader/Type/GitHubTokenSetupTrait.php | 5 ++ .../Downloader/Type/HostedPackageBin.php | 63 +++++++++++++++++++ src/StaticPHP/Command/SPCConfigCommand.php | 2 +- src/StaticPHP/Config/ConfigValidator.php | 8 ++- 6 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b28c11dc0..9600068dd 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -12,6 +12,7 @@ use StaticPHP\Artifact\Downloader\Type\Git; use StaticPHP\Artifact\Downloader\Type\GitHubRelease; use StaticPHP\Artifact\Downloader\Type\GitHubTarball; +use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; use StaticPHP\Artifact\Downloader\Type\PhpRelease; use StaticPHP\Artifact\Downloader\Type\PIE; @@ -35,6 +36,19 @@ */ class ArtifactDownloader { + public const array DOWNLOADERS = [ + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar', 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, + ]; + /** @var array Artifact objects */ protected array $artifacts = []; @@ -355,18 +369,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, foreach ($queue as $item) { try { $instance = null; - $call = match ($item['config']['type']) { - 'bitbuckettag' => BitBucketTag::class, - 'filelist' => FileList::class, - 'git' => Git::class, - 'ghrel' => GitHubRelease::class, - 'ghtar', 'ghtagtar' => GitHubTarball::class, - 'local' => LocalDir::class, - 'pie' => PIE::class, - 'url' => Url::class, - 'php-release' => PhpRelease::class, - default => null, - }; + $call = self::DOWNLOADERS[$item['config']['type']] ?? null; $type_display_name = match (true) { $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader', $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader', diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 2e8a499e3..731e8297e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -21,6 +21,26 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface private ?string $version = null; + public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true): array + { + logger()->debug("Fetching {$name} GitHub releases from {$repo}"); + $url = str_replace('{repo}', $repo, self::API_URL); + $headers = $this->getGitHubTokenHeaders(); + $data2 = default_shell()->executeCurl($url, headers: $headers); + $data = json_decode($data2 ?: '', true); + if (!is_array($data)) { + throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); + } + $releases = []; + foreach ($data as $release) { + if ($prefer_stable && $release['prerelease'] === true) { + continue; + } + $releases[] = $release; + } + return $releases; + } + /** * Get the latest GitHub release assets for a given repository. * match_asset is provided, only return the asset that matches the regex. diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index d773bde73..90c425075 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -7,6 +7,11 @@ trait GitHubTokenSetupTrait { public function getGitHubTokenHeaders(): array + { + return self::getGitHubTokenHeadersStatic(); + } + + public static function getGitHubTokenHeadersStatic(): array { // GITHUB_TOKEN support if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php new file mode 100644 index 000000000..25d6098a7 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -0,0 +1,63 @@ + '{name}-{arch}-{os}-{libc}-{libcver}.txz', + 'darwin' => '{name}-{arch}-{os}.txz', + 'windows' => '{name}-{arch}-{os}.tgz', + ]; + + private static array $release_info = []; + + public static function getReleaseInfo(): array + { + if (empty(self::$release_info)) { + $rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO); + if (empty($rel)) { + throw new DownloaderException('No releases found for hosted package-bin'); + } + self::$release_info = $rel[0]; + } + return self::$release_info; + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + $info = self::getReleaseInfo(); + $replace = [ + '{name}' => $name, + '{arch}' => SystemTarget::getTargetArch(), + '{os}' => strtolower(SystemTarget::getTargetOS()), + '{libc}' => SystemTarget::getLibc() ?? 'default', + '{libcver}' => SystemTarget::getLibcVersion() ?? 'default', + ]; + $find_str = str_replace(array_keys($replace), array_values($replace), self::ASSET_MATCHES[strtolower(SystemTarget::getTargetOS())]); + foreach ($info['assets'] as $asset) { + if ($asset['name'] === $find_str) { + $download_url = $asset['browser_download_url']; + $filename = $asset['name']; + $version = ltrim($info['tag_name'], 'v'); + logger()->debug("Downloading hosted package-bin {$name} version {$version} from GitHub: {$download_url}"); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + $headers = $this->getGitHubTokenHeaders(); + default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); + } + } + throw new DownloaderException("No matching asset found for hosted package-bin {$name} with criteria: {$find_str}"); + } +} diff --git a/src/StaticPHP/Command/SPCConfigCommand.php b/src/StaticPHP/Command/SPCConfigCommand.php index 0a242c608..f8afd0e44 100644 --- a/src/StaticPHP/Command/SPCConfigCommand.php +++ b/src/StaticPHP/Command/SPCConfigCommand.php @@ -33,7 +33,7 @@ public function handle(): int { // transform string to array $libraries = parse_comma_list($this->getOption('with-libs')); - $libraries = array_merge($libraries, $this->getOption('with-packages')); + $libraries = array_merge($libraries, parse_comma_list($this->getOption('with-packages'))); // transform string to array $extensions = $this->getArgument('extensions') ? parse_extension_list($this->getArgument('extensions')) : []; $include_suggests = $this->getOption('with-suggests') ?: $this->getOption('with-suggested-libs') || $this->getOption('with-suggested-exts'); diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 3ddb9bab0..4de0529f0 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -137,8 +137,14 @@ public static function validateAndLintArtifacts(string $config_file_name, mixed ]; continue; } - // TODO: expand hosted to static-php hosted download urls if ($v === 'hosted') { + $data[$name][$k] = [ + 'linux-x86_64' => ['type' => 'hosted'], + 'linux-aarch64' => ['type' => 'hosted'], + 'windows-x86_64' => ['type' => 'hosted'], + 'macos-x86_64' => ['type' => 'hosted'], + 'macos-aarch64' => ['type' => 'hosted'], + ]; continue; } if (is_assoc_array($v)) { From 93a697ebbf34cce5f8fbe021070e3f4dfddc89e2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 5 Dec 2025 14:44:27 +0800 Subject: [PATCH 030/682] Fix artifact downloader constants and improve error message for hosted package-bin --- src/StaticPHP/Artifact/ArtifactDownloader.php | 4 +++- src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 9600068dd..74097325e 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -36,12 +36,14 @@ */ class ArtifactDownloader { + /** @var array> */ public const array DOWNLOADERS = [ 'bitbuckettag' => BitBucketTag::class, 'filelist' => FileList::class, 'git' => Git::class, 'ghrel' => GitHubRelease::class, - 'ghtar', 'ghtagtar' => GitHubTarball::class, + 'ghtar' => GitHubTarball::class, + 'ghtagtar' => GitHubTarball::class, 'local' => LocalDir::class, 'pie' => PIE::class, 'url' => Url::class, diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php index 25d6098a7..c5cbb3b50 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -58,6 +58,6 @@ public function download(string $name, array $config, ArtifactDownloader $downlo return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); } } - throw new DownloaderException("No matching asset found for hosted package-bin {$name} with criteria: {$find_str}"); + throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}"); } } From 106b55d4e70770c75df38ab8ab56a7d010c16c41 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 5 Dec 2025 15:32:32 +0800 Subject: [PATCH 031/682] [v3] Add musl-wrapper and musl-toolchain installation support (#984) --- config/artifact.json | 15 ++++- src/StaticPHP/Artifact/ArtifactDownloader.php | 10 ++-- src/StaticPHP/Artifact/ArtifactExtractor.php | 13 +++-- src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 56 ++++++++++++++----- src/StaticPHP/Util/System/LinuxUtil.php | 2 +- 5 files changed, 71 insertions(+), 25 deletions(-) diff --git a/config/artifact.json b/config/artifact.json index a2c6ba4fb..de30a1e4c 100644 --- a/config/artifact.json +++ b/config/artifact.json @@ -8,6 +8,9 @@ } } }, + "musl-wrapper": { + "source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz" + }, "php-src": { "source": { "type": "php-release" @@ -28,8 +31,16 @@ }, "musl-toolchain": { "binary": { - "linux-x86_64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", - "linux-aarch64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz" + "linux-x86_64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", + "extract": "{pkg_root_path}/musl-toolchain" + }, + "linux-aarch64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz", + "extract": "{pkg_root_path}/musl-toolchain" + } } }, "pkg-config": { diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 74097325e..a42bed7b4 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -320,7 +320,7 @@ public function download(bool $interactive = true): void $skipped = []; foreach ($this->artifacts as $artifact) { ++$current; - if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { + if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { $skipped[] = $artifact->getName(); continue; } @@ -358,7 +358,7 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } - private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int { $queue = $this->generateQueue($artifact); // already downloaded @@ -379,7 +379,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, }; $try_h = $try ? 'Try downloading' : 'Downloading'; logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); - if ($parallel === false) { + if ($parallel === false && $interactive) { InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); } // is valid download type @@ -413,13 +413,13 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, } // process lock ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); - if ($parallel === false) { + if ($parallel === false && $interactive) { $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : ''; InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} ."); } return SPC_DOWNLOAD_STATUS_SUCCESS; } catch (DownloaderException|ExecutionException $e) { - if ($parallel === false) { + if ($parallel === false && $interactive) { InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index ac0e6ae2c..11b738a70 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -78,12 +78,17 @@ public function extractForPackages(array $packages, bool $force_source = false): /** * Extract a single artifact. * - * @param Artifact $artifact The artifact to extract - * @param bool $force_source If true, always extract source (ignore binary) + * @param Artifact|string $artifact The artifact to extract + * @param bool $force_source If true, always extract source (ignore binary) */ - public function extract(Artifact $artifact, bool $force_source = false): int + public function extract(Artifact|string $artifact, bool $force_source = false): int { - $name = $artifact->getName(); + if (is_string($artifact)) { + $name = $artifact; + $artifact = ArtifactLoader::getArtifactInstance($name); + } else { + $name = $artifact->getName(); + } // Already extracted in this session if (isset($this->extracted[$name])) { diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index a48300e7b..1cfc7afdd 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -4,13 +4,20 @@ namespace StaticPHP\Doctor\Item; +use StaticPHP\Artifact\ArtifactCache; +use StaticPHP\Artifact\ArtifactDownloader; +use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Attribute\Doctor\OptionalCheck; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Toolchain\MuslToolchain; use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\InteractiveTerm; +use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\System\LinuxUtil; #[OptionalCheck([self::class, 'optionalCheck'])] @@ -51,23 +58,46 @@ public function checkMuslCrossMake(): CheckResult #[FixItem('fix-musl-wrapper')] public function fixMusl(): bool { - // TODO: implement musl-wrapper installation - // This should: - // 1. Download musl source using Downloader::downloadSource() - // 2. Extract the source using FileSystem::extractSource() - // 3. Apply CVE patches using SourcePatcher::patchFile() - // 4. Build and install musl wrapper - // 5. Add path using putenv instead of editing /etc/profile - return false; + $downloader = new ArtifactDownloader(); + $downloader->add('musl-wrapper')->download(false); + $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); + $extractor->extract('musl-wrapper'); + + // Apply CVE-2025-26519 patch and install musl wrapper + SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . '/musl-wrapper'); + SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0002.patch', SOURCE_PATH . '/musl-wrapper'); + + $prefix = ''; + if (get_current_user() !== 'root') { + $prefix = 'sudo '; + logger()->warning('Current user is not root, using sudo for running command'); + } + shell()->cd(SOURCE_PATH . '/musl-wrapper') + ->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper') + ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j') + ->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + return true; } #[FixItem('fix-musl-cross-make')] public function fixMuslCrossMake(): bool { - // TODO: implement musl-cross-make installation - // This should: - // 1. Install musl-toolchain package using PackageManager::installPackage() - // 2. Copy toolchain files to /usr/local/musl - return false; + // sudo + $prefix = ''; + if (get_current_user() !== 'root') { + $prefix = 'sudo '; + logger()->warning('Current user is not root, using sudo for running command'); + } + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + $downloader = new ArtifactDownloader(); + $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); + $downloader->add('musl-toolchain')->download(false); + $extractor->extract('musl-toolchain'); + $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; + shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); + FileSystem::removeDir($pkg_root); + return true; } } diff --git a/src/StaticPHP/Util/System/LinuxUtil.php b/src/StaticPHP/Util/System/LinuxUtil.php index 2b1d44f43..570907d9e 100644 --- a/src/StaticPHP/Util/System/LinuxUtil.php +++ b/src/StaticPHP/Util/System/LinuxUtil.php @@ -91,7 +91,7 @@ public static function getSupportedDistros(): array { return [ // debian-like - 'debian', 'ubuntu', 'Deepin', + 'debian', 'ubuntu', 'Deepin', 'neon', // rhel-like 'redhat', // centos From 66840a8eed896b8d3769861394198ff336d7336e Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 5 Dec 2025 09:15:22 +0100 Subject: [PATCH 032/682] update xdebug to use pie sources --- config/source.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/source.json b/config/source.json index 689594a76..c96822d6b 100644 --- a/config/source.json +++ b/config/source.json @@ -1172,9 +1172,8 @@ } }, "xdebug": { - "type": "url", - "url": "https://pecl.php.net/get/xdebug", - "filename": "xdebug.tgz", + "type": "pie", + "repo": "xdebug/xdebug", "license": { "type": "file", "path": "LICENSE" From 7bdcda1d626fa6c1b935168c3741079908630230 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 5 Dec 2025 11:37:35 +0100 Subject: [PATCH 033/682] gmp can't build with std=c23 (default with gcc 15) --- src/SPC/builder/unix/library/gmp.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/library/gmp.php b/src/SPC/builder/unix/library/gmp.php index f09976d8c..cb5d76f9c 100644 --- a/src/SPC/builder/unix/library/gmp.php +++ b/src/SPC/builder/unix/library/gmp.php @@ -10,7 +10,12 @@ trait gmp { protected function build(): void { - UnixAutoconfExecutor::create($this)->configure()->make(); + UnixAutoconfExecutor::create($this) + ->appendEnv([ + 'CFLAGS' => '-std=gnu99', + ]) + ->configure() + ->make(); $this->patchPkgconfPrefix(['gmp.pc']); } } From 6b5f7027196a6e36e9ca1c755177fe71282a2638 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 5 Dec 2025 11:43:51 +0100 Subject: [PATCH 034/682] ncurses can't build with std=c23 (default with gcc 15) --- src/SPC/builder/unix/library/ncurses.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/library/ncurses.php b/src/SPC/builder/unix/library/ncurses.php index 27725c3d5..31e61b1e7 100644 --- a/src/SPC/builder/unix/library/ncurses.php +++ b/src/SPC/builder/unix/library/ncurses.php @@ -16,6 +16,7 @@ protected function build(): void UnixAutoconfExecutor::create($this) ->appendEnv([ + 'CFLAGS' => '-std=gnu99', 'LDFLAGS' => SPCTarget::isStatic() ? '-static' : '', ]) ->configure( From 1d5aec037b6f4bae508a2a679cf25c4eb0076023 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 5 Dec 2025 12:14:57 +0100 Subject: [PATCH 035/682] c17 instead --- src/SPC/builder/extension/mongodb.php | 5 +++++ src/SPC/builder/unix/library/gmp.php | 2 +- src/SPC/builder/unix/library/ncurses.php | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/extension/mongodb.php b/src/SPC/builder/extension/mongodb.php index 745417bb1..1c22f2899 100644 --- a/src/SPC/builder/extension/mongodb.php +++ b/src/SPC/builder/extension/mongodb.php @@ -24,4 +24,9 @@ public function getUnixConfigureArg(bool $shared = false): string $arg .= $this->builder->getLib('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; return clean_spaces($arg); } + + protected function getExtraEnv(): array + { + return ['CFLAGS' => '-std=c17']; + } } diff --git a/src/SPC/builder/unix/library/gmp.php b/src/SPC/builder/unix/library/gmp.php index cb5d76f9c..f625274f6 100644 --- a/src/SPC/builder/unix/library/gmp.php +++ b/src/SPC/builder/unix/library/gmp.php @@ -12,7 +12,7 @@ protected function build(): void { UnixAutoconfExecutor::create($this) ->appendEnv([ - 'CFLAGS' => '-std=gnu99', + 'CFLAGS' => '-std=c17', ]) ->configure() ->make(); diff --git a/src/SPC/builder/unix/library/ncurses.php b/src/SPC/builder/unix/library/ncurses.php index 31e61b1e7..2859f556d 100644 --- a/src/SPC/builder/unix/library/ncurses.php +++ b/src/SPC/builder/unix/library/ncurses.php @@ -16,7 +16,7 @@ protected function build(): void UnixAutoconfExecutor::create($this) ->appendEnv([ - 'CFLAGS' => '-std=gnu99', + 'CFLAGS' => '-std=c17', 'LDFLAGS' => SPCTarget::isStatic() ? '-static' : '', ]) ->configure( From b2182b4fe1d0278c1efcdbfce7a5b0268fb4aae0 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 5 Dec 2025 12:20:14 +0100 Subject: [PATCH 036/682] use source extract hook for pdo_sqlsrv --- src/SPC/builder/extension/pdo_sqlsrv.php | 26 ------------------------ src/SPC/store/SourcePatcher.php | 18 ++++++++++++++++ 2 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 src/SPC/builder/extension/pdo_sqlsrv.php diff --git a/src/SPC/builder/extension/pdo_sqlsrv.php b/src/SPC/builder/extension/pdo_sqlsrv.php deleted file mode 100644 index 32697b169..000000000 --- a/src/SPC/builder/extension/pdo_sqlsrv.php +++ /dev/null @@ -1,26 +0,0 @@ -source_dir . '/config.m4') && is_dir($this->source_dir . '/source/pdo_sqlsrv')) { - FileSystem::moveFileOrDir($this->source_dir . '/LICENSE', $this->source_dir . '/source/pdo_sqlsrv/LICENSE'); - FileSystem::moveFileOrDir($this->source_dir . '/source/shared', $this->source_dir . '/source/pdo_sqlsrv/shared'); - FileSystem::moveFileOrDir($this->source_dir . '/source/pdo_sqlsrv', SOURCE_PATH . '/pdo_sqlsrv'); - FileSystem::removeDir($this->source_dir); - FileSystem::moveFileOrDir(SOURCE_PATH . '/pdo_sqlsrv', $this->source_dir); - return true; - } - return false; - } -} diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index f628544fe..0068f53e3 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -25,6 +25,7 @@ public static function init(): void FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); + FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']); FileSystem::addSourceExtractHook('yaml', [__CLASS__, 'patchYamlWin32']); FileSystem::addSourceExtractHook('libyaml', [__CLASS__, 'patchLibYaml']); FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchImapLicense']); @@ -432,6 +433,23 @@ public static function patchSQLSRVWin32(string $source_name): bool return false; } + /** + * Fix the compilation issue of pdo_sqlsrv with php 8.5 + */ + public static function patchSQLSRVPhp85(): bool + { + $source_dir = SOURCE_PATH . '/php-src/ext/pdo_sqlsrv'; + if (!file_exists($source_dir . '/config.m4') && is_dir($source_dir . '/source/pdo_sqlsrv')) { + FileSystem::moveFileOrDir($source_dir . '/LICENSE', $source_dir . '/source/pdo_sqlsrv/LICENSE'); + FileSystem::moveFileOrDir($source_dir . '/source/shared', $source_dir . '/source/pdo_sqlsrv/shared'); + FileSystem::moveFileOrDir($source_dir . '/source/pdo_sqlsrv', SOURCE_PATH . '/pdo_sqlsrv'); + FileSystem::removeDir($source_dir); + FileSystem::moveFileOrDir(SOURCE_PATH . '/pdo_sqlsrv', $source_dir); + return true; + } + return false; + } + public static function patchYamlWin32(): bool { FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/yaml/config.w32', "lib.substr(lib.length - 6, 6) == '_a.lib'", "lib.substr(lib.length - 6, 6) == '_a.lib' || 'yes' == 'yes'"); From 47ab5d7584f68f1963d9fd8996e103040b65af85 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 5 Dec 2025 13:57:28 +0100 Subject: [PATCH 037/682] use c17 for extensions as well? --- src/SPC/builder/Extension.php | 2 +- src/SPC/builder/extension/mongodb.php | 5 ----- src/SPC/builder/extension/pgsql.php | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 08b403e61..57ed9dd38 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -576,7 +576,7 @@ protected function addExtensionDependency(string $name, bool $optional = false): protected function getExtraEnv(): array { - return []; + return ['CFLAGS' => '-std=c17']; } /** diff --git a/src/SPC/builder/extension/mongodb.php b/src/SPC/builder/extension/mongodb.php index 1c22f2899..745417bb1 100644 --- a/src/SPC/builder/extension/mongodb.php +++ b/src/SPC/builder/extension/mongodb.php @@ -24,9 +24,4 @@ public function getUnixConfigureArg(bool $shared = false): string $arg .= $this->builder->getLib('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; return clean_spaces($arg); } - - protected function getExtraEnv(): array - { - return ['CFLAGS' => '-std=c17']; - } } diff --git a/src/SPC/builder/extension/pgsql.php b/src/SPC/builder/extension/pgsql.php index f22f7ba68..06efa8b13 100644 --- a/src/SPC/builder/extension/pgsql.php +++ b/src/SPC/builder/extension/pgsql.php @@ -45,7 +45,7 @@ public function getWindowsConfigureArg(bool $shared = false): string protected function getExtraEnv(): array { return [ - 'CFLAGS' => '-Wno-int-conversion', + 'CFLAGS' => '-std=c17 -Wno-int-conversion', ]; } } From 9ad7147155711bbf61db412336c643033b7ee7a2 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 6 Dec 2025 16:50:36 +0800 Subject: [PATCH 038/682] Enhance musl-wrapper and musl-toolchain installation process (#988) --- src/SPC/builder/linux/SystemUtil.php | 2 +- src/SPC/doctor/item/LinuxToolCheckList.php | 4 ++-- src/StaticPHP/Artifact/ArtifactDownloader.php | 7 ++++++- src/StaticPHP/Doctor/Doctor.php | 7 ++++--- src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 5 +++-- src/StaticPHP/Util/System/LinuxUtil.php | 16 +++++++++++++++- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/SPC/builder/linux/SystemUtil.php b/src/SPC/builder/linux/SystemUtil.php index 056af090d..30b36b888 100644 --- a/src/SPC/builder/linux/SystemUtil.php +++ b/src/SPC/builder/linux/SystemUtil.php @@ -141,7 +141,7 @@ public static function getSupportedDistros(): array { return [ // debian-like - 'debian', 'ubuntu', 'Deepin', + 'debian', 'ubuntu', 'Deepin', 'neon', // rhel-like 'redhat', // centos diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index ab8144a21..08a2b4dc9 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -112,7 +112,7 @@ public function checkSystemOSPackages(): ?CheckResult public function fixBuildTools(array $distro, array $missing): bool { $install_cmd = match ($distro['dist']) { - 'ubuntu', 'debian', 'Deepin' => 'apt-get install -y', + 'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y', 'alpine' => 'apk add', 'redhat' => 'dnf install -y', 'centos' => 'yum install -y', @@ -128,7 +128,7 @@ public function fixBuildTools(array $distro, array $missing): bool logger()->warning('Current user (' . $user . ') is not root, using sudo for running command (may require password input)'); } - $is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin']); + $is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin', 'neon']); $to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing; // debian, alpine libtool -> libtoolize $to_install = str_replace('libtoolize', 'libtool', $to_install); diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index a42bed7b4..4fb651cfb 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -397,7 +397,12 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, $instance = new $call(); $lock = $instance->download($artifact->getName(), $item['config'], $this); } else { - throw new ValidationException("Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}."); + if ($item['config']['type'] === 'custom') { + $msg = "Artifact [{$artifact->getName()}] has no valid custom " . SystemTarget::getCurrentPlatformString() . ' download callback defined.'; + } else { + $msg = "Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}."; + } + throw new ValidationException($msg); } if (!$lock instanceof DownloadResult) { throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult."); diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 8141fa32f..692a5b8c5 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -34,7 +34,7 @@ public function checkAll(bool $interactive = true): bool InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { - if (!$this->checkItem($check)) { + if (!$this->checkItem($check, $interactive)) { return false; } } @@ -47,7 +47,7 @@ public function checkAll(bool $interactive = true): bool * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ - public function checkItem(CheckItem|string $check): bool + public function checkItem(CheckItem|string $check, bool $interactive = true): bool { if (is_string($check)) { $found = null; @@ -63,7 +63,8 @@ public function checkItem(CheckItem|string $check): bool } $check = $found; } - $this->output?->write("Checking {$check->item_name} ... "); + $prepend = $interactive ? ' - ' : ''; + $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check $result = call_user_func($check->callback); diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index 1cfc7afdd..4d7a86bee 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -13,6 +13,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Doctor\CheckResult; use StaticPHP\Runtime\Shell\Shell; +use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\MuslToolchain; use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\FileSystem; @@ -25,8 +26,8 @@ class LinuxMuslCheck { public static function optionalCheck(): bool { - return getenv('SPC_TOOLCHAIN') === MuslToolchain::class || - (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist()); + $toolchain = ApplicationContext::get(ToolchainInterface::class); + return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist(); } /** @noinspection PhpUnused */ diff --git a/src/StaticPHP/Util/System/LinuxUtil.php b/src/StaticPHP/Util/System/LinuxUtil.php index 570907d9e..79ad89ed7 100644 --- a/src/StaticPHP/Util/System/LinuxUtil.php +++ b/src/StaticPHP/Util/System/LinuxUtil.php @@ -13,13 +13,14 @@ class LinuxUtil extends UnixUtil * Get current linux distro name and version. * * @noinspection PhpMissingBreakStatementInspection - * @return array{dist: string, ver: string} Linux distro info (unknown if not found) + * @return array{dist: string, ver: string, family: string} Linux distro info (unknown if not found) */ public static function getOSRelease(): array { $ret = [ 'dist' => 'unknown', 'ver' => 'unknown', + 'family' => 'unknown', ]; switch (true) { case file_exists('/etc/centos-release'): @@ -44,6 +45,9 @@ public static function getOSRelease(): array if (preg_match('/^ID=(.*)$/', $line, $matches)) { $ret['dist'] = $matches[1]; } + if (preg_match('/^ID_LIKE=(.*)$/', $line, $matches)) { + $ret['family'] = $matches[1]; + } if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) { $ret['ver'] = $matches[1]; } @@ -103,6 +107,16 @@ public static function getSupportedDistros(): array ]; } + /** + * Check if current linux distro is debian-based. + */ + public static function isDebianDist(): bool + { + $dist = static::getOSRelease()['dist']; + $family = explode(' ', static::getOSRelease()['family']); + return in_array($dist, ['debian', 'ubuntu', 'Deepin', 'neon']) || in_array('debian', $family); + } + /** * Get libc version string from ldd. */ From d3b0f5de79945ff9af319aef3d245fd8742e1cc5 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 6 Dec 2025 16:57:16 +0800 Subject: [PATCH 039/682] Fix argument naming in InstallPackageCommand for clarity (#989) --- src/StaticPHP/Command/InstallPackageCommand.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 754d6ef29..89814f013 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -8,19 +8,19 @@ use StaticPHP\Package\PackageInstaller; use Symfony\Component\Console\Attribute\AsCommand; -#[AsCommand('install-pkg', 'Install additional packages', ['i', 'install-package'])] +#[AsCommand('install-pkg', 'Install additional package', ['i', 'install-package'])] class InstallPackageCommand extends BaseCommand { public function configure() { - $this->addArgument('packages', null, 'The package to install (name or path)'); + $this->addArgument('package', null, 'The package to install (name or path)'); } public function handle(): int { ApplicationContext::set('elephant', true); $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); - $installer->addInstallPackage($this->input->getArgument('packages')); + $installer->addInstallPackage($this->input->getArgument('package')); $installer->run(true, true); return static::SUCCESS; } From dce63d3c87ae88dce86731a61b68b525f4c2a535 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 6 Dec 2025 11:18:10 +0100 Subject: [PATCH 040/682] we need extensions to explicitly tell which c std they need --- src/SPC/builder/Extension.php | 2 +- src/SPC/builder/extension/mongodb.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 57ed9dd38..bc475d115 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -576,7 +576,7 @@ protected function addExtensionDependency(string $name, bool $optional = false): protected function getExtraEnv(): array { - return ['CFLAGS' => '-std=c17']; + return ['CFLAGS' => '']; } /** diff --git a/src/SPC/builder/extension/mongodb.php b/src/SPC/builder/extension/mongodb.php index 745417bb1..08861e4ef 100644 --- a/src/SPC/builder/extension/mongodb.php +++ b/src/SPC/builder/extension/mongodb.php @@ -24,4 +24,9 @@ public function getUnixConfigureArg(bool $shared = false): string $arg .= $this->builder->getLib('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; return clean_spaces($arg); } + + public function getExtraEnv(): array + { + return ['CFLAGS' => '-std=c17']; + } } From 2f09ace82fca4091506823ef8fb612a8a807d781 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 09:49:11 +0800 Subject: [PATCH 041/682] Add LinuxToolCheck --- src/StaticPHP/Doctor/Item/LinuxToolCheck.php | 147 +++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/StaticPHP/Doctor/Item/LinuxToolCheck.php diff --git a/src/StaticPHP/Doctor/Item/LinuxToolCheck.php b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php new file mode 100644 index 000000000..09161c74e --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php @@ -0,0 +1,147 @@ + '/usr/share/perl5/FindBin.pm', + 'binutils-gold' => 'ld.gold', + 'base-devel' => 'automake', + 'gettext-devel' => 'gettextize', + 'gettext-dev' => 'gettextize', + 'perl-IPC-Cmd' => '/usr/share/perl5/vendor_perl/IPC/Cmd.pm', + 'perl-Time-Piece' => '/usr/lib64/perl5/Time/Piece.pm', + ]; + + /** @noinspection PhpUnused */ + #[CheckItem('if necessary tools are installed', limit_os: 'Linux', level: 999)] + public function checkCliTools(): ?CheckResult + { + $distro = LinuxUtil::getOSRelease(); + + $required = match ($distro['dist']) { + 'alpine' => self::TOOLS_ALPINE, + 'redhat' => self::TOOLS_RHEL, + 'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd', 'perl-Time-Piece']), + 'arch' => self::TOOLS_ARCH, + default => self::TOOLS_DEBIAN, + }; + $missing = []; + foreach ($required as $package) { + if (LinuxUtil::findCommand(self::PROVIDED_COMMAND[$package] ?? $package) === null) { + $missing[] = $package; + } + } + if (!empty($missing)) { + return CheckResult::fail(implode(', ', $missing) . ' not installed on your system', 'install-linux-tools', [$distro, $missing]); + } + return CheckResult::ok(); + } + + #[CheckItem('if cmake version >= 3.22', limit_os: 'Linux')] + public function checkCMakeVersion(): ?CheckResult + { + $ver = get_cmake_version(); + if ($ver === null) { + return CheckResult::fail('Failed to get cmake version'); + } + if (version_compare($ver, '3.22.0') < 0) { + return CheckResult::fail('cmake version is too low (' . $ver . '), please update it manually!'); + } + return CheckResult::ok($ver); + } + + /** @noinspection PhpUnused */ + #[CheckItem('if necessary linux headers are installed', limit_os: 'Linux')] + public function checkSystemOSPackages(): ?CheckResult + { + if (LinuxUtil::isMuslDist()) { + // check linux-headers installation + if (!file_exists('/usr/include/linux/mman.h')) { + return CheckResult::fail('linux-headers not installed on your system', 'install-linux-tools', [LinuxUtil::getOSRelease(), ['linux-headers']]); + } + } + return CheckResult::ok(); + } + + #[FixItem('install-linux-tools')] + public function fixBuildTools(array $distro, array $missing): bool + { + $install_cmd = match ($distro['dist']) { + 'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y', + 'alpine' => 'apk add', + 'redhat' => 'dnf install -y', + 'centos' => 'yum install -y', + 'arch' => 'pacman -S --noconfirm', + default => null, + }; + if ($install_cmd === null) { + // try family + $family = explode(' ', strtolower($distro['family'])); + if (in_array('debian', $family)) { + $install_cmd = 'apt-get install -y'; + } elseif (in_array('rhel', $family) || in_array('fedora', $family)) { + $install_cmd = 'dnf install -y'; + } else { + throw new EnvironmentException( + "Current linux distro [{$distro['dist']}] does not have an auto-install script for packages yet.", + 'You can submit an issue to request support: https://github.com/crazywhalecc/static-php-cli/issues' + ); + } + } + $prefix = ''; + if (($user = exec('whoami')) !== 'root') { + $prefix = 'sudo '; + logger()->warning("Current user ({$user}) is not root, using sudo for running command (may require password input)"); + } + + $is_debian = LinuxUtil::isDebianDist(); + $to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing; + // debian, alpine libtool -> libtoolize + $to_install = str_replace('libtoolize', 'libtool', $to_install); + shell()->exec($prefix . $install_cmd . ' ' . implode(' ', $to_install)); + + return true; + } +} From baddd601138ef231bdd3ab301a301308840877ed Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 10:36:45 +0800 Subject: [PATCH 042/682] Add dev commands: is-installed, shell (for debugging package status) --- .../Command/Dev/IsInstalledCommand.php | 34 +++++++++++++++++++ src/StaticPHP/Command/Dev/ShellCommand.php | 33 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 6 ++++ 3 files changed, 73 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/IsInstalledCommand.php create mode 100644 src/StaticPHP/Command/Dev/ShellCommand.php diff --git a/src/StaticPHP/Command/Dev/IsInstalledCommand.php b/src/StaticPHP/Command/Dev/IsInstalledCommand.php new file mode 100644 index 000000000..a3f693217 --- /dev/null +++ b/src/StaticPHP/Command/Dev/IsInstalledCommand.php @@ -0,0 +1,34 @@ +no_motd = true; + $this->addArgument('package', InputArgument::REQUIRED, 'The package name to check installation status'); + } + + public function handle(): int + { + $installer = new PackageInstaller(); + $package = $this->input->getArgument('package'); + $installer->addInstallPackage($package); + $installed = $installer->isPackageInstalled($package); + if ($installed) { + $this->output->writeln("Package [{$package}] is installed correctly."); + return static::SUCCESS; + } + $this->output->writeln("Package [{$package}] is not installed."); + return static::FAILURE; + } +} diff --git a/src/StaticPHP/Command/Dev/ShellCommand.php b/src/StaticPHP/Command/Dev/ShellCommand.php new file mode 100644 index 000000000..560cc7fed --- /dev/null +++ b/src/StaticPHP/Command/Dev/ShellCommand.php @@ -0,0 +1,33 @@ +output->writeln("Entering interactive shell. Type 'exit' to leave."); + + if (SystemTarget::isUnix()) { + passthru('PS1=\'[StaticPHP] > \' /bin/bash', $code); + return $code; + } + if (SystemTarget::getTargetOS() === 'Windows') { + passthru('cmd.exe', $code); + return $code; + } + $this->output->writeln('Unsupported OS for shell command.'); + return static::FAILURE; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 1f63190b3..0484c1114 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,8 @@ use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\Dev\IsInstalledCommand; +use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\ExtractCommand; @@ -47,6 +49,10 @@ public function __construct() new BuildLibsCommand(), new ExtractCommand(), new SPCConfigCommand(), + + // dev commands + new ShellCommand(), + new IsInstalledCommand(), ]); // add additional commands from registries From dbc6dbee53b9aca2dd8a11c79407d2b4824f21b9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 10:57:51 +0800 Subject: [PATCH 043/682] Add Zig package support with downloader and installation checks --- config/pkg.target.json | 5 +- src/Package/Artifact/zig.php | 98 ++++++++++++++++++++++++++ src/StaticPHP/Doctor/Item/ZigCheck.php | 53 ++++++++++++++ src/globals/scripts/zig-cc.sh | 65 +++++++++++++++++ 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/Package/Artifact/zig.php create mode 100644 src/StaticPHP/Doctor/Item/ZigCheck.php create mode 100755 src/globals/scripts/zig-cc.sh diff --git a/config/pkg.target.json b/config/pkg.target.json index b5e4a7c8b..8f47502b5 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -82,7 +82,10 @@ }, "zig": { "type": "target", - "artifact": "zig" + "artifact": "zig", + "static-bins": [ + "{pkg_root_path}/zig/zig" + ] }, "nasm": { "type": "target", diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php new file mode 100644 index 000000000..2ac7b454b --- /dev/null +++ b/src/Package/Artifact/zig.php @@ -0,0 +1,98 @@ +executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); + $index_json = json_decode($index_json ?: '', true); + $latest_version = null; + foreach ($index_json as $version => $data) { + $latest_version = $version; + break; + } + + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Zig version'); + } + $zig_arch = SystemTarget::getTargetArch(); + $zig_os = match (SystemTarget::getTargetOS()) { + 'Windows' => 'win', + 'Darwin' => 'macos', + 'Linux' => 'linux', + default => throw new DownloaderException('Unsupported OS for Zig: ' . SystemTarget::getTargetOS()), + }; + $platform_key = "{$zig_arch}-{$zig_os}"; + if (!isset($index_json[$latest_version][$platform_key])) { + throw new DownloaderException("No download available for {$platform_key} in Zig version {$latest_version}"); + } + $download_info = $index_json[$latest_version][$platform_key]; + $url = $download_info['tarball']; + $sha256 = $download_info['shasum']; + $filename = basename($url); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + // verify hash + $file_hash = hash_file('sha256', $path); + if ($file_hash !== $sha256) { + throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}"); + } + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); + } + + #[AfterBinaryExtract('zig', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function postExtractZig(string $target_path): void + { + $files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy']; + $all_exist = true; + foreach ($files as $file) { + if (!file_exists("{$target_path}/{$file}")) { + $all_exist = false; + break; + } + } + if ($all_exist) { + return; + } + + $script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh'; + $script_content = file_get_contents($script_path); + + file_put_contents("{$target_path}/zig-cc", $script_content); + chmod("{$target_path}/zig-cc", 0755); + + $script_content = str_replace('zig cc', 'zig c++', $script_content); + file_put_contents("{$target_path}/zig-c++", $script_content); + file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@"); + file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@"); + file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@"); + file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@"); + chmod("{$target_path}/zig-c++", 0755); + chmod("{$target_path}/zig-ar", 0755); + chmod("{$target_path}/zig-ld.lld", 0755); + chmod("{$target_path}/zig-ranlib", 0755); + chmod("{$target_path}/zig-objcopy", 0755); + } +} diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php new file mode 100644 index 000000000..c8d00574b --- /dev/null +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -0,0 +1,53 @@ +addInstallPackage($package); + $installed = $installer->isPackageInstalled($package); + if ($installed) { + return CheckResult::ok(); + } + return CheckResult::fail('zig is not installed', 'install-zig'); + } + + #[FixItem('install-zig')] + public function installZig(): bool + { + $arch = arch2gnu(php_uname('m')); + $os = match (PHP_OS_FAMILY) { + 'Windows' => 'win', + 'Darwin' => 'macos', + 'BSD' => 'freebsd', + default => 'linux', + }; + $installer = new PackageInstaller(); + $installer->addInstallPackage('zig'); + $installer->run(false); + return $installer->isPackageInstalled('zig'); + } +} diff --git a/src/globals/scripts/zig-cc.sh b/src/globals/scripts/zig-cc.sh new file mode 100755 index 000000000..5c9017c7f --- /dev/null +++ b/src/globals/scripts/zig-cc.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +if [ "$BUILD_ROOT_PATH" = "" ]; then + echo "The script must be run in the SPC build environment." + exit 1 +fi + +SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" +BUILDROOT_ABS=$BUILD_ROOT_PATH +PARSED_ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + -isystem) + shift + ARG="$1" + shift + ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" + [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG") + ;; + -isystem*) + ARG="${1#-isystem}" + shift + ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" + [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") + ;; + -march=*|-mcpu=*) + OPT_NAME="${1%%=*}" + OPT_VALUE="${1#*=}" + # Skip armv8- flags entirely as Zig doesn't support them + if [[ "$OPT_VALUE" == armv8-* ]]; then + shift + continue + fi + # replace -march=x86-64 with -march=x86_64 + OPT_VALUE="${OPT_VALUE//-/_}" + PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}") + shift + ;; + *) + PARSED_ARGS+=("$1") + shift + ;; + esac +done + +[[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET="" + +if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then + output=$(zig cc $TARGET $SPC_COMPILER_EXTRA "${PARSED_ARGS[@]}" 2>&1) + status=$? + + if [[ $status -eq 0 ]]; then + echo "$output" + exit 0 + fi + + if echo "$output" | grep -qE "version '.*' in target triple"; then + filtered_output=$(echo "$output" | grep -vE "version '.*' in target triple") + echo "$filtered_output" + exit 0 + fi +fi + +exec zig cc $TARGET $SPC_COMPILER_EXTRA "${PARSED_ARGS[@]}" From 88b86d3eaf70aa5008c2a7d4a73937cb93595438 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 10:58:16 +0800 Subject: [PATCH 044/682] Fix artifact downloade does not accept boolean options bug --- src/StaticPHP/Artifact/ArtifactDownloader.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 4fb651cfb..2b7ac0de6 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -113,7 +113,7 @@ public function __construct(protected array $options = []) foreach ($ls as $name) { $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE; } - } elseif ($options['prefer-source'] === null) { + } elseif ($options['prefer-source'] === null || $options['prefer-source'] === true) { $this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE; } } @@ -124,7 +124,7 @@ public function __construct(protected array $options = []) foreach ($ls as $name) { $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY; } - } elseif ($options['prefer-binary'] === null) { + } elseif ($options['prefer-binary'] === null || $options['prefer-binary'] === true) { $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY; } } @@ -134,7 +134,7 @@ public function __construct(protected array $options = []) foreach ($ls as $name) { $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY; } - } elseif ($options['prefer-pre-built'] === null) { + } elseif ($options['prefer-pre-built'] === null || $options['prefer-pre-built'] === true) { $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY; } } @@ -145,7 +145,7 @@ public function __construct(protected array $options = []) foreach ($ls as $name) { $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE; } - } elseif ($options['source-only'] === null) { + } elseif ($options['source-only'] === null || $options['source-only'] === true) { $this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE; } } @@ -156,7 +156,7 @@ public function __construct(protected array $options = []) foreach ($ls as $name) { $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY; } - } elseif ($options['binary-only'] === null) { + } elseif ($options['binary-only'] === null || $options['binary-only'] === true) { $this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY; } } @@ -164,7 +164,7 @@ public function __construct(protected array $options = []) if (array_key_exists('ignore-cache', $options)) { if (is_string($options['ignore-cache'])) { $this->ignore_cache = parse_comma_list($options['ignore-cache']); - } elseif ($options['ignore-cache'] === null) { + } elseif ($options['ignore-cache'] === null || $options['ignore-cache'] === true) { $this->ignore_cache = true; } } @@ -172,7 +172,7 @@ public function __construct(protected array $options = []) if (array_key_exists('ignore-cache-sources', $options)) { if (is_string($options['ignore-cache-sources'])) { $this->ignore_cache = parse_comma_list($options['ignore-cache-sources']); - } elseif ($options['ignore-cache-sources'] === null) { + } elseif ($options['ignore-cache-sources'] === null || $options['ignore-cache-sources'] === true) { $this->ignore_cache = true; } } From 6775cb4674177da7634aa58181d6d028aba9df2a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 10:58:44 +0800 Subject: [PATCH 045/682] Fix pkg-config doctor fix using source bug --- src/StaticPHP/Doctor/Item/PkgConfigCheck.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php index 757c14ed8..4a0ba498d 100644 --- a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php +++ b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php @@ -45,7 +45,7 @@ public function checkFunctional(): CheckResult public function fix(): bool { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller(['dl-prefer-binary' => true]); + $installer = new PackageInstaller(['dl-binary-only' => true]); $installer->addInstallPackage('pkg-config'); $installer->run(false, true); return true; From 3ff762c4c8d1931670757bb13a292479ec84bc4a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 10:59:07 +0800 Subject: [PATCH 046/682] Fix wrong namespace in go-xcaddy package --- src/Package/Artifact/go_xcaddy.php | 2 +- src/Package/Target/go_xcaddy.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 152dc2e5c..293f7c62b 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -4,13 +4,13 @@ namespace Package\Artifact; -use SPC\util\GlobalEnvManager; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\System\LinuxUtil; class go_xcaddy diff --git a/src/Package/Target/go_xcaddy.php b/src/Package/Target/go_xcaddy.php index cafaacf7e..01c4ade3d 100644 --- a/src/Package/Target/go_xcaddy.php +++ b/src/Package/Target/go_xcaddy.php @@ -4,9 +4,9 @@ namespace Package\Target; -use SPC\util\GlobalEnvManager; use StaticPHP\Attribute\Package\InitPackage; use StaticPHP\Attribute\Package\Target; +use StaticPHP\Util\GlobalEnvManager; #[Target('go-xcaddy')] class go_xcaddy From df6c27c98d37ab044eba354a311d00736bbb7ddc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 10:59:25 +0800 Subject: [PATCH 047/682] Allow absolute paths for configs --- src/StaticPHP/Package/LibraryPackage.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index c83985501..2063ee897 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -33,22 +33,28 @@ public function addBuildFunction(string $platform, callable $func): void public function isInstalled(): bool { foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) { - if (!file_exists("{$this->getLibDir()}/{$lib}")) { + $path = FileSystem::isRelativePath($lib) ? "{$this->getLibDir()}/{$lib}" : $lib; + if (!file_exists($path)) { return false; } } foreach (PackageConfig::get($this->getName(), 'headers', []) as $header) { - if (!file_exists("{$this->getIncludeDir()}/{$header}")) { + $path = FileSystem::isRelativePath($header) ? "{$this->getIncludeDir()}/{$header}" : $header; + if (!file_exists($path)) { return false; } } foreach (PackageConfig::get($this->getName(), 'pkg-configs', []) as $pc) { - if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}.pc")) { + if (!str_ends_with($pc, '.pc')) { + $pc .= '.pc'; + } + if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}")) { return false; } } foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { - if (!file_exists("{$this->getBinDir()}/{$bin}")) { + $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; + if (!file_exists($path)) { return false; } } From abd6c2fa3a969661863eedc8bddf4b657383fd69 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 11:01:36 +0800 Subject: [PATCH 048/682] Add PackageInstaller::isPackageInstalled() API --- src/StaticPHP/Package/PackageInstaller.php | 36 ++++++++++++++++++++-- src/StaticPHP/Util/SPCConfigUtil.php | 12 +++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 34e246214..c6aa34213 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -108,8 +108,10 @@ public function setDownload(bool $download = true): static */ public function run(bool $interactive = true, bool $disable_delay_msg = false): void { - // resolve input, make dependency graph - $this->resolvePackages(); + if (empty($this->packages)) { + // resolve input, make dependency graph + $this->resolvePackages(); + } if ($interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output @@ -148,7 +150,10 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): } $builder = ApplicationContext::get(PackageBuilder::class); foreach ($this->packages as $package) { - if ($this->isBuildPackage($package) || $package instanceof LibraryPackage && $package->hasStage('build')) { + if ( + $this->isBuildPackage($package) || + $package instanceof LibraryPackage && $package->hasStage('build') && !$package->getArtifact()->shouldUseBinary() + ) { if ($interactive) { InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } @@ -213,6 +218,31 @@ public function isPackageResolved(string $package_name): bool return isset($this->packages[$package_name]); } + public function isPackageInstalled(Package|string $package_name): bool + { + if (empty($this->packages)) { + $this->resolvePackages(); + } + if (is_string($package_name)) { + $package = $this->getPackage($package_name); + if ($package === null) { + throw new WrongUsageException("Package '{$package_name}' is not resolved."); + } + } else { + $package = $package_name; + } + + // check if package is built/installed + if ($this->isBuildPackage($package)) { + return $package->isInstalled(); + } + if ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) { + $artifact = $package->getArtifact(); + return $artifact->isBinaryExtracted(); + } + return false; + } + /** * Returns the download status of all artifacts for the resolved packages. * diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 317258d03..c525f8c79 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -225,11 +225,15 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st // convert all static-libs to short names $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); foreach ($libs as $lib) { - // check file existence - if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { - throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); + if (FileSystem::isRelativePath($lib)) { + // check file existence + if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { + throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); + } + $lib_names[] = $this->getShortLibName($lib); + } else { + $lib_names[] = $lib; } - $lib_names[] = $this->getShortLibName($lib); } // add frameworks for macOS if (SystemTarget::getTargetOS() === 'Darwin') { From eab105965d39a70b5efb3217ab251383d1bf0f9f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 11:04:11 +0800 Subject: [PATCH 049/682] Remove redundant path --- config/pkg.target.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/config/pkg.target.json b/config/pkg.target.json index 8f47502b5..b5e4a7c8b 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -82,10 +82,7 @@ }, "zig": { "type": "target", - "artifact": "zig", - "static-bins": [ - "{pkg_root_path}/zig/zig" - ] + "artifact": "zig" }, "nasm": { "type": "target", From 127c935106de7b598a0e70b3c8a8a9f5aa7c8e17 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Mon, 8 Dec 2025 12:33:37 +0800 Subject: [PATCH 050/682] Refactor BUILDROOT_ABS initialization to provide a default path (#991) --- src/globals/scripts/zig-cc.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/globals/scripts/zig-cc.sh b/src/globals/scripts/zig-cc.sh index 5c9017c7f..56ae95055 100755 --- a/src/globals/scripts/zig-cc.sh +++ b/src/globals/scripts/zig-cc.sh @@ -1,12 +1,7 @@ #!/usr/bin/env bash -if [ "$BUILD_ROOT_PATH" = "" ]; then - echo "The script must be run in the SPC build environment." - exit 1 -fi - SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" -BUILDROOT_ABS=$BUILD_ROOT_PATH +BUILDROOT_ABS="${BUILD_ROOT_PATH:-$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)}" PARSED_ARGS=() while [[ $# -gt 0 ]]; do From a1cadecc54ce4f57a5f3cda8eae8dfcf5c9f3af9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 12:45:12 +0800 Subject: [PATCH 051/682] Refactor re2c fix-item --- src/StaticPHP/Doctor/Item/Re2cVersionCheck.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php index eb9b917fb..fce3350be 100644 --- a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -7,6 +7,7 @@ use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Package\PackageInstaller; class Re2cVersionCheck { @@ -29,7 +30,9 @@ public function checkRe2cVersion(): ?CheckResult #[FixItem('build-re2c')] public function buildRe2c(): bool { - // TODO: implement re2c build process - return false; + $installer = new PackageInstaller(); + $installer->addInstallPackage('re2c'); + $installer->run(false); + return true; } } From 80d922ab3b04f7c44b730cb6b1e379d9b212fc72 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:58:04 +0800 Subject: [PATCH 052/682] Use patch for current package exclusively --- src/Package/Extension/readline.php | 6 ++++-- src/Package/Library/imap.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Package/Extension/readline.php b/src/Package/Extension/readline.php index 2ecc533a5..6395057e5 100644 --- a/src/Package/Extension/readline.php +++ b/src/Package/Extension/readline.php @@ -7,6 +7,7 @@ use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PackageInstaller; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Util\SourcePatcher; @@ -14,7 +15,8 @@ #[Extension('readline')] class readline { - #[BeforeStage('php', 'unix-make-cli')] + #[BeforeStage('php', 'unix-make-cli', 'ext-readline')] + #[PatchDescription('Fix readline static build with musl')] public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { if ($toolchain->isStatic()) { @@ -23,7 +25,7 @@ public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterfa } } - #[AfterStage('php', 'unix-make-cli')] + #[AfterStage('php', 'unix-make-cli', 'ext-readline')] public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { if ($toolchain->isStatic()) { diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index 58e9397fe..a80ff015d 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -13,7 +13,7 @@ #[Library('imap')] class imap { - #[AfterStage('php', 'patch-embed-scripts')] + #[AfterStage('php', 'patch-embed-scripts', 'imap')] #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] public function afterPatchScripts(): void { From 20e0711747d6b2fd7ffe12230ec3bdb35146d976 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:58:42 +0800 Subject: [PATCH 053/682] Add libedit package build --- src/Package/Library/libedit.php | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/Package/Library/libedit.php diff --git a/src/Package/Library/libedit.php b/src/Package/Library/libedit.php new file mode 100644 index 000000000..2dac2817e --- /dev/null +++ b/src/Package/Library/libedit.php @@ -0,0 +1,37 @@ +getSourceDir()}/src/sys.h", + '|//#define\s+strl|', + '#define strl' + ); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(): void + { + UnixAutoconfExecutor::create($this) + ->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L']) + ->configure() + ->make(); + $this->patchPkgconfPrefix(['libedit.pc']); + } +} From 11e7a590c89eeae49c1fbecfb64b235a7b17bb10 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:58:47 +0800 Subject: [PATCH 054/682] Add ncurses package build --- src/Package/Library/ncurses.php | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Package/Library/ncurses.php diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php new file mode 100644 index 000000000..f0a217148 --- /dev/null +++ b/src/Package/Library/ncurses.php @@ -0,0 +1,61 @@ +appendEnv([ + 'LDFLAGS' => $toolchain->isStatic() ? '-static' : '', + ]) + ->configure( + '--enable-overwrite', + '--with-curses-h', + '--enable-pc-files', + '--enable-echo', + '--disable-widec', + '--with-normal', + '--with-ticlib', + '--without-tests', + '--without-dlsym', + '--without-debug', + '-enable-symlinks', + "--bindir={$package->getBinDir()}", + "--includedir={$package->getIncludeDir()}", + "--libdir={$package->getLibDir()}", + "--prefix={$package->getBuildRootPath()}", + ) + ->make(); + $new_files = $dirdiff->getIncrementFiles(true); + foreach ($new_files as $file) { + @unlink(BUILD_BIN_PATH . '/' . $file); + } + + shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); + shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); + + $pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc']; + $package->patchPkgconfPrefix($pkgconf_list); + + foreach ($pkgconf_list as $pkgconf) { + FileSystem::replaceFileStr("{$package->getLibDir()}/pkgconfig/{$pkgconf}", "-L{$package->getLibDir()}", '-L${libdir}'); + } + } +} From 321f2e13e88e54e8eafca3afb5007e1841fb1171 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:59:38 +0800 Subject: [PATCH 055/682] Allow all types of package can be built --- src/StaticPHP/Package/LibraryPackage.php | 17 ----------------- src/StaticPHP/Package/Package.php | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 2063ee897..81c549965 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -13,23 +13,6 @@ */ class LibraryPackage extends Package { - /** @var array $build_functions Build functions for different OS binding */ - protected array $build_functions = []; - - /** - * Add a build function for a specific platform. - * - * @param string $platform PHP_OS_FAMILY - * @param callable $func Function to build for the platform - */ - public function addBuildFunction(string $platform, callable $func): void - { - $this->build_functions[$platform] = $func; - if ($platform === PHP_OS_FAMILY) { - $this->addStage('build', $func); - } - } - public function isInstalled(): bool { foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) { diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 263a88821..6f590b025 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -19,6 +19,9 @@ abstract class Package */ protected array $stages = []; + /** @var array $build_functions Build functions for different OS binding */ + protected array $build_functions = []; + /** * @param string $name Name of the package * @param string $type Type of the package @@ -55,6 +58,20 @@ public function runStage(string $name, array $context = []): mixed return $ret; } + /** + * Add a build function for a specific platform. + * + * @param string $os_family PHP_OS_FAMILY + * @param callable $func Function to build for the platform + */ + public function addBuildFunction(string $os_family, callable $func): void + { + $this->build_functions[$os_family] = $func; + if ($os_family === PHP_OS_FAMILY) { + $this->addStage('build', $func); + } + } + public function isInstalled(): bool { // By default, assume package is not installed. From f4bb0263f68b30eef36cc8158302d4bee44fdb08 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:03 +0800 Subject: [PATCH 056/682] Fix ncurses static-libs --- config/pkg.lib.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/pkg.lib.json b/config/pkg.lib.json index 52531b214..79e1a8534 100644 --- a/config/pkg.lib.json +++ b/config/pkg.lib.json @@ -711,6 +711,9 @@ "ncurses": { "type": "library", "artifact": "ncurses", + "static-libs@unix": [ + "libncurses.a" + ], "license": { "type": "file", "path": "COPYING" From b38434572326a367bff82f5962d7f58d0ce24cdf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:18 +0800 Subject: [PATCH 057/682] Add php-micro patch for embed mode --- src/Package/Target/micro.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Package/Target/micro.php diff --git a/src/Package/Target/micro.php b/src/Package/Target/micro.php new file mode 100644 index 000000000..a95d4b4d6 --- /dev/null +++ b/src/Package/Target/micro.php @@ -0,0 +1,22 @@ +getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); + } +} From 80128edd39ac2e45124e17e622507b19d32601a5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:34 +0800 Subject: [PATCH 058/682] Add patch description display --- src/StaticPHP/DI/ApplicationContext.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index c27203532..9b702ccbc 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -7,8 +7,10 @@ use DI\Container; use DI\ContainerBuilder; use Psr\Container\ContainerInterface; +use StaticPHP\Attribute\PatchDescription; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use ZM\Logger\ConsoleColor; use function DI\factory; @@ -138,6 +140,14 @@ public static function getInvoker(): CallbackInvoker public static function invoke(callable $callback, array $context = []): mixed { logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + + // get if callback has attribute PatchDescription + $ref = new \ReflectionFunction(\Closure::fromCallable($callback)); + $attributes = $ref->getAttributes(PatchDescription::class); + foreach ($attributes as $attribute) { + $attrInstance = $attribute->newInstance(); + logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}")); + } return self::getInvoker()->invoke($callback, $context); } From 78234ef14778c5c6284da3de8f791d9295319d77 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:56 +0800 Subject: [PATCH 059/682] Add missing patchPkgconfPrefix function --- src/StaticPHP/Package/LibraryPackage.php | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 81c549965..97ec80077 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -129,6 +129,36 @@ public function getLibExtraLdFlags(): string return trim($env); } + /** + * Patch pkgconfig file prefix, exec_prefix, libdir, includedir for correct build. + * + * @param array $files File list to patch, if empty, will use pkg-configs from config (e.g. ['zlib.pc', 'openssl.pc']) + * @param int $patch_option Patch options + * @param null|array $custom_replace Custom replace rules, if provided, will be used to replace in the format [regex, replacement] + */ + public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCONF_PATCH_ALL, ?array $custom_replace = null): void + { + logger()->info("Patching library [{$this->getName()}] pkgconfig"); + if ($files === [] && ($conf_pc = PackageConfig::get($this->getName(), 'pkg-configs', [])) !== []) { + $files = array_map(fn ($x) => "{$x}.pc", $conf_pc); + } + foreach ($files as $name) { + $realpath = realpath("{$this->getLibDir()}/pkgconfig/{$name}"); + if ($realpath === false) { + throw new PatchException('pkg-config prefix patcher', "Cannot find library [{$this->getName()}] pkgconfig file [{$name}] in {$this->getLibDir()}/pkgconfig/ !"); + } + logger()->debug("Patching {$realpath}"); + // replace prefix + $file = FileSystem::readFile($realpath); + $file = ($patch_option & PKGCONF_PATCH_PREFIX) === PKGCONF_PATCH_PREFIX ? preg_replace('/^prefix\s*=.*$/m', 'prefix=' . BUILD_ROOT_PATH, $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_EXEC_PREFIX) === PKGCONF_PATCH_EXEC_PREFIX ? preg_replace('/^exec_prefix\s*=.*$/m', 'exec_prefix=${prefix}', $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_LIBDIR) === PKGCONF_PATCH_LIBDIR ? preg_replace('/^libdir\s*=.*$/m', 'libdir=${prefix}/lib', $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_INCLUDEDIR) === PKGCONF_PATCH_INCLUDEDIR ? preg_replace('/^includedir\s*=.*$/m', 'includedir=${prefix}/include', $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_CUSTOM) === PKGCONF_PATCH_CUSTOM && $custom_replace !== null ? preg_replace($custom_replace[0], $custom_replace[1], $file) : $file; + FileSystem::writeFile($realpath, $file); + } + } + /** * Get extra LIBS for current package. * You need to define the environment variable in the format of {LIBRARY_NAME}_LIBS From 7b16f683fc76e466b5adbffadbc572183d445c66 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:01:29 +0800 Subject: [PATCH 060/682] Allow package implementation using parent class functions --- src/StaticPHP/Package/PackageLoader.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Package/PackageLoader.php index c89bf392c..878b3b94f 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Package/PackageLoader.php @@ -143,8 +143,6 @@ public static function loadFromClass(mixed $class): void } self::$loaded_classes[$class_name] = true; - $instance_class = $refClass->newInstance(); - $attributes = $refClass->getAttributes(); foreach ($attributes as $attribute) { $pkg = null; @@ -160,6 +158,19 @@ public static function loadFromClass(mixed $class): void if ($package_type === null) { throw new WrongUsageException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); } + + // if class has parent class and matches the attribute instance, use custom class + if ($refClass->getParentClass() !== false) { + if (is_a($class_name, Package::class, true)) { + self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type); + $instance_class = self::$packages[$attribute_instance->name]; + } + } + + if (!isset($instance_class)) { + $instance_class = $refClass->newInstance(); + } + $pkg = self::$packages[$attribute_instance->name]; // validate package type matches @@ -272,9 +283,6 @@ private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, ca private static function addBuildFunction(Package $pkg, object $attr, callable $fn): void { - if (!$pkg instanceof LibraryPackage) { - throw new ValidationException("Class [{$pkg->getName()}] must implement LibraryPackage for BuildFor attribute."); - } $pkg->addBuildFunction($attr->os, $fn); } } From a4bd2a79a9341fee723ff544a2e4f8c2671e8272 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:01:58 +0800 Subject: [PATCH 061/682] Add shared extension build support --- src/Package/Target/php.php | 98 +++++++---- src/StaticPHP/Package/PhpExtensionPackage.php | 158 +++++++++++++++++- 2 files changed, 219 insertions(+), 37 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c2e01647f..4d3683ce1 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -14,6 +14,7 @@ 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\SPCException; use StaticPHP\Exception\WrongUsageException; @@ -111,7 +112,7 @@ public function init(TargetPackage $package): void } #[ResolveBuild] - public function resolveBuild(TargetPackage $package): array + public function resolveBuild(TargetPackage $package, PackageInstaller $installer): array { // Parse extensions and additional packages for all php-* targets $static_extensions = parse_extension_list($package->getBuildArgument('extensions')); @@ -128,6 +129,7 @@ public function resolveBuild(TargetPackage $package): array // get instances foreach ($extensions_pkg as $extension) { $extname = substr($extension, 4); + $config = PackageConfig::get($extension, 'php-extension', []); if (!PackageLoader::hasPackage($extension)) { throw new WrongUsageException("Extension [{$extname}] does not exist. Please check your extension name."); } @@ -137,13 +139,25 @@ public function resolveBuild(TargetPackage $package): array } // set build static/shared if (in_array($extname, $static_extensions)) { + if (($config['build-static'] ?? true) === false) { + throw new WrongUsageException("Extension [{$extname}] cannot be built as static extension."); + } $instance->setBuildStatic(); } if (in_array($extname, $shared_extensions)) { + if (($config['build-shared'] ?? true) === false) { + throw new WrongUsageException("Extension [{$extname}] cannot be built as shared extension, please remove it from --build-shared option."); + } $instance->setBuildShared(); + $instance->setBuildWithPhp($config['build-with-php'] ?? false); } } + // building shared extensions need embed SAPI + if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -178,14 +192,14 @@ public function info(Package $package, PackageInstaller $installer): array return []; } $sapis = array_filter([ - $installer->getBuildPackage('php-cli') ? 'cli' : null, - $installer->getBuildPackage('php-fpm') ? 'fpm' : null, - $installer->getBuildPackage('php-micro') ? 'micro' : null, - $installer->getBuildPackage('php-cgi') ? 'cgi' : null, - $installer->getBuildPackage('php-embed') ? 'embed' : null, - $installer->getBuildPackage('frankenphp') ? 'frankenphp' : null, + $installer->isPackageResolved('php-cli') ? 'cli' : null, + $installer->isPackageResolved('php-fpm') ? 'fpm' : null, + $installer->isPackageResolved('php-micro') ? 'micro' : null, + $installer->isPackageResolved('php-cgi') ? 'cgi' : null, + $installer->isPackageResolved('php-embed') ? 'embed' : null, + $installer->isPackageResolved('frankenphp') ? 'frankenphp' : null, ]); - $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() === 'php-extension'); + $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic()); $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); $install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-')); return [ @@ -281,15 +295,15 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args[] = "--with-config-file-scan-dir={$option}"; } // perform enable cli options - $args[] = $installer->isBuildPackage('php-cli') ? '--enable-cli' : '--disable-cli'; - $args[] = $installer->isBuildPackage('php-fpm') ? '--enable-fpm' : '--disable-fpm'; - $args[] = $installer->isBuildPackage('php-micro') ? match (SystemTarget::getTargetOS()) { + $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->isBuildPackage('php-cgi') ? '--enable-cgi' : '--disable-cgi'; + $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - $args[] = $installer->isBuildPackage('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; $args = implode(' ', array_filter($args)); @@ -311,19 +325,19 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - if ($installer->isBuildPackage('php-cli')) { + if ($installer->isPackageResolved('php-cli')) { $package->runStage('unix-make-cli'); } - if ($installer->isBuildPackage('php-cgi')) { + if ($installer->isPackageResolved('php-cgi')) { $package->runStage('unix-make-cgi'); } - if ($installer->isBuildPackage('php-fpm')) { + if ($installer->isPackageResolved('php-fpm')) { $package->runStage('unix-make-fpm'); } - if ($installer->isBuildPackage('php-micro')) { + if ($installer->isPackageResolved('php-micro')) { $package->runStage('unix-make-micro'); } - if ($installer->isBuildPackage('php-embed')) { + if ($installer->isPackageResolved('php-embed')) { $package->runStage('unix-make-embed'); } } @@ -359,6 +373,7 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install } #[Stage('unix-make-micro')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { $phar_patched = false; @@ -375,6 +390,8 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta 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'); } finally { if ($phar_patched) { SourcePatcher::unpatchMicroPhar(); @@ -385,6 +402,7 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta #[Stage('unix-make-embed')] 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() @@ -395,9 +413,11 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $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 -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') + ->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 ------------- @@ -417,12 +437,15 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta // process shared extensions that built-with-php $increment_files = $diff->getChangedFiles(); foreach ($increment_files as $increment_file) { - $builder->deployBinary($increment_file, $libphp_so, false); + $builder->deployBinary($increment_file, $increment_file, false); } // ------------- 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}"); @@ -432,19 +455,9 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $package->runStage('patch-embed-scripts'); } - #[BuildFor('Darwin')] - #[BuildFor('Linux')] - public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void + #[Stage('unix-build-shared-ext')] + public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void { - // virtual target, do nothing - if ($package->getName() !== 'php') { - return; - } - - $package->runStage('unix-buildconf'); - $package->runStage('unix-configure'); - $package->runStage('unix-make'); - // collect shared extensions /** @var PhpExtensionPackage[] $shared_extensions */ $shared_extensions = array_filter( @@ -470,9 +483,10 @@ public function build(TargetPackage $package, PackageInstaller $installer, Toolc } try { + logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - logger()->info('Building shared extensions...'); - $extension->buildSharedExtension(); + InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); + $extension->buildShared(); } } finally { // restore php-config @@ -483,6 +497,22 @@ public function build(TargetPackage $package, PackageInstaller $installer, Toolc } } + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(TargetPackage $package): void + { + // virtual target, do nothing + if ($package->getName() !== 'php') { + return; + } + + $package->runStage('unix-buildconf'); + $package->runStage('unix-configure'); + $package->runStage('unix-make'); + + $package->runStage('unix-build-shared-ext'); + } + /** * Patch phpize and php-config if needed */ diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 562d8f8e4..667d9688b 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -6,8 +6,10 @@ use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\SPCConfigUtil; /** * Represents a PHP extension package. @@ -41,6 +43,23 @@ public function __construct(string $name, string $type = 'php-extension', protec parent::__construct($name, $type); } + public function getSourceDir(): string + { + if ($this->getArtifact() === null) { + $path = SOURCE_PATH . '/php-src/ext/' . $this->getExtensionName(); + if (!is_dir($path)) { + throw new ValidationException("Extension source directory not found: {$path}", validation_module: "Extension {$this->getExtensionName()} source"); + } + return $path; + } + return parent::getSourceDir(); + } + + public function getExtensionName(): string + { + return str_replace('ext-', '', $this->getName()); + } + public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void { if ($os === '') { @@ -59,7 +78,7 @@ public function getPhpConfigureArg(string $os, bool $shared): string return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', substr($this->getName(), 4)); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -81,6 +100,22 @@ public function getPhpConfigureArg(string $os, bool $shared): string public function setBuildShared(bool $build_shared = true): void { $this->build_shared = $build_shared; + // Add build stages for shared build on Unix-like systems + // TODO: Windows shared build support + if ($build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { + if (!$this->hasStage('build')) { + $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, '_buildSharedUnix']); + } + if (!$this->hasStage('phpize')) { + $this->addStage('phpize', [$this, '_phpize']); + } + if (!$this->hasStage('configure')) { + $this->addStage('configure', [$this, '_configure']); + } + if (!$this->hasStage('make')) { + $this->addStage('make', [$this, '_make']); + } + } } public function setBuildStatic(bool $build_static = true): void @@ -108,8 +143,125 @@ public function isBuildWithPhp(): bool return $this->build_with_php; } - public function buildSharedExtension(): void + public function buildShared(): void { - // TODO: build common shared extensions code here... + if ($this->hasStage('build')) { + $this->runStage('build'); + } else { + throw new WrongUsageException("Extension [{$this->getExtensionName()}] cannot build shared target yet."); + } + } + + /** + * Get shared extension build environment variables for Unix. + * + * @return array{ + * CFLAGS: string, + * CXXFLAGS: string, + * LDFLAGS: string, + * LIBS: string, + * LD_LIBRARY_PATH: string + * } + */ + public function getSharedExtensionEnv(): array + { + $config = (new SPCConfigUtil())->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; + $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + return [ + 'CFLAGS' => $config['cflags'], + 'CXXFLAGS' => $config['cflags'], + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + } + + /** + * @internal + * #[Stage('phpize')] + */ + public function _phpize(array $env, PhpExtensionPackage $package): void + { + shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize'); + } + + /** + * @internal + * #[Stage('configure')] + */ + public function _configure(array $env, PhpExtensionPackage $package): void + { + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + shell()->cd($package->getSourceDir()) + ->setEnv($env) + ->exec( + './configure ' . $this->getPhpConfigureArg(SystemTarget::getCurrentPlatformString(), true) . + ' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' . + "--enable-shared --disable-static {$phpvars}" + ); + } + + /** + * @internal + * #[Stage('make')] + */ + public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void + { + shell()->cd($package->getSourceDir()) + ->setEnv($env) + ->exec('make clean') + ->exec("make -j{$builder->concurrency}") + ->exec('make install'); + } + + /** + * Build shared extension on Unix-like systems. + * Only for internal calling. For external use, call buildShared() instead. + * @internal + * #[Stage('build')] + */ + public function _buildSharedUnix(PackageBuilder $builder): void + { + $env = $this->getSharedExtensionEnv(); + + $this->runStage('phpize', ['env' => $env]); + $this->runStage('configure', ['env' => $env]); + $this->runStage('make', ['env' => $env]); + + // process *.so file + $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; + if (!file_exists($soFile)) { + throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build"); + } + $builder->deployBinary($soFile, $soFile, false); + } + + /** + * Splits a given string of library flags into static and shared libraries. + * + * @param string $allLibs A space-separated string of library flags (e.g., -lxyz). + * @return array an array containing two elements: the first is a space-separated string + * of static library flags, and the second is a space-separated string + * of shared library flags + */ + protected function splitLibsIntoStaticAndShared(string $allLibs): array + { + $staticLibString = ''; + $sharedLibString = ''; + $libs = explode(' ', $allLibs); + foreach ($libs as $lib) { + $staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a'; + if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) { + $staticLib = $lib; + } + if ($lib === '-lphp' || !file_exists($staticLib)) { + $sharedLibString .= " {$lib}"; + } else { + $staticLibString .= " {$lib}"; + } + } + return [trim($staticLibString), trim($sharedLibString)]; } } From 0db26be826e392e7625a279161b9b20fd38abd24 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:02:12 +0800 Subject: [PATCH 062/682] Correct SAPI-packages to be installed --- src/StaticPHP/Package/PackageInstaller.php | 47 +++++++++++----------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index c6aa34213..4be980904 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -150,44 +150,45 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): } $builder = ApplicationContext::get(PackageBuilder::class); foreach ($this->packages as $package) { - if ( - $this->isBuildPackage($package) || - $package instanceof LibraryPackage && $package->hasStage('build') && !$package->getArtifact()->shouldUseBinary() - ) { + $is_to_build = $this->isBuildPackage($package); + $has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build'); + $should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false); + $has_source = $package->hasSource(); + if (!$is_to_build && $should_use_binary) { + // install binary if ($interactive) { - InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); + InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { - /** @var LibraryPackage $package */ - $status = $builder->buildPackage($package, $this->isBuildPackage($package)); + $status = $this->installBinary($package); } catch (\Throwable $e) { if ($interactive) { - InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); + InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } if ($interactive) { - InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); + InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } - } elseif ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) { - // install binary + } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { if ($interactive) { - InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); + InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { - $status = $this->installBinary($package); + /** @var LibraryPackage $package */ + $status = $builder->buildPackage($package, $this->isBuildPackage($package)); } catch (\Throwable $e) { if ($interactive) { - InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); + InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } if ($interactive) { - InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); + InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } - } elseif ($package instanceof LibraryPackage) { + } elseif ($package->getType() === 'library') { throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS."); } } @@ -442,32 +443,32 @@ private function handlePhpTargetPackage(TargetPackage $package): void if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) { $cli = PackageLoader::getPackage('php-cli'); - $this->build_packages[$cli->getName()] = $cli; + $this->install_packages[$cli->getName()] = $cli; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) { $fpm = PackageLoader::getPackage('php-fpm'); - $this->build_packages[$fpm->getName()] = $fpm; + $this->install_packages[$fpm->getName()] = $fpm; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) { $micro = PackageLoader::getPackage('php-micro'); - $this->build_packages[$micro->getName()] = $micro; + $this->install_packages[$micro->getName()] = $micro; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) { $cgi = PackageLoader::getPackage('php-cgi'); - $this->build_packages[$cgi->getName()] = $cgi; + $this->install_packages[$cgi->getName()] = $cgi; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) { $embed = PackageLoader::getPackage('php-embed'); - $this->build_packages[$embed->getName()] = $embed; + $this->install_packages[$embed->getName()] = $embed; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { $frankenphp = PackageLoader::getPackage('frankenphp'); - $this->build_packages[$frankenphp->getName()] = $frankenphp; + $this->install_packages[$frankenphp->getName()] = $frankenphp; $added = true; } $this->build_packages[$package->getName()] = $package; @@ -481,7 +482,7 @@ private function handlePhpTargetPackage(TargetPackage $package): void } else { // process specific php sapi targets $this->build_packages['php'] = PackageLoader::getPackage('php'); - $this->build_packages[$package->getName()] = $package; + $this->install_packages[$package->getName()] = $package; } } From e004d108611307c4a7cc55d7bb23605b56675efe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:04:00 +0800 Subject: [PATCH 063/682] Fix phpstan --- src/StaticPHP/Package/PackageLoader.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Package/PackageLoader.php index 878b3b94f..5093dd7b5 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Package/PackageLoader.php @@ -221,6 +221,10 @@ public static function loadFromClass(mixed $class): void self::$packages[$pkg->getName()] = $pkg; } + if (!isset($instance_class)) { + $instance_class = $refClass->newInstance(); + } + // parse non-package available attributes foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { $method_attributes = $method->getAttributes(); From 808aed2a6620fd7312a626c50265a7cbed38f612 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 14:58:11 +0800 Subject: [PATCH 064/682] Refactor package stage handling and update class structures for improved flexibility --- composer.json | 1 + composer.lock | 163 +++++++++++++++++- .../Command/SwitchPhpVersionCommand.php | 2 +- src/Package/Extension/readline.php | 5 +- src/Package/Library/imap.php | 3 +- src/Package/Library/libedit.php | 2 +- src/Package/Library/postgresql.php | 3 +- src/Package/Target/micro.php | 2 +- src/Package/Target/php.php | 48 +++--- src/StaticPHP/Artifact/ArtifactDownloader.php | 1 + src/StaticPHP/Artifact/ArtifactExtractor.php | 1 + .../Attribute/Package/AfterStage.php | 2 +- .../Attribute/Package/BeforeStage.php | 9 +- src/StaticPHP/Attribute/Package/Stage.php | 2 +- src/StaticPHP/Command/BuildTargetCommand.php | 2 +- src/StaticPHP/Command/DownloadCommand.php | 2 +- src/StaticPHP/Command/ExtractCommand.php | 4 +- src/StaticPHP/ConsoleApplication.php | 6 +- src/StaticPHP/Doctor/Doctor.php | 1 + src/StaticPHP/Doctor/Item/ZigCheck.php | 7 - src/StaticPHP/Exception/ExceptionHandler.php | 3 + src/StaticPHP/Exception/RegistryException.php | 7 + src/StaticPHP/Package/Package.php | 38 ++-- src/StaticPHP/Package/PackageInstaller.php | 1 + src/StaticPHP/Package/PhpExtensionPackage.php | 63 ++++--- .../{Artifact => Registry}/ArtifactLoader.php | 3 +- .../{Doctor => Registry}/DoctorLoader.php | 2 +- .../{Package => Registry}/PackageLoader.php | 121 +++++++++++-- src/StaticPHP/Registry/Registry.php | 27 +-- 29 files changed, 416 insertions(+), 115 deletions(-) create mode 100644 src/StaticPHP/Exception/RegistryException.php rename src/StaticPHP/{Artifact => Registry}/ArtifactLoader.php (99%) rename src/StaticPHP/{Doctor => Registry}/DoctorLoader.php (99%) rename src/StaticPHP/{Package => Registry}/PackageLoader.php (64%) diff --git a/composer.json b/composer.json index 360cdbdbe..eadd2732c 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "ext-mbstring": "*", "ext-zlib": "*", "laravel/prompts": "~0.1", + "nette/php-generator": "^4.2", "php-di/php-di": "^7.1", "symfony/console": "^5.4 || ^6 || ^7", "symfony/process": "^7.2", diff --git a/composer.lock b/composer.lock index 6afc39dfa..a0538ce8e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "14b3ad42c138807fa9288e6b510ac69f", + "content-hash": "edb3243ddaa8b05d8f6545266a146e93", "packages": [ { "name": "laravel/prompts", @@ -126,6 +126,167 @@ }, "time": "2025-11-21T20:52:36+00:00" }, + { + "name": "nette/php-generator", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/4707546a1f11badd72f5d82af4f8a6bc64bd56ac", + "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0.6", + "php": "8.1 - 8.5" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.4", + "nikic/php-parser": "^5.0", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.2.0" + }, + "time": "2025-08-06T18:24:31+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.0" + }, + "time": "2025-12-01T17:49:23+00:00" + }, { "name": "php-di/invoker", "version": "2.3.7", diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php index 38649dd75..3782a6452 100644 --- a/src/Package/Command/SwitchPhpVersionCommand.php +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Command\BaseCommand; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; diff --git a/src/Package/Extension/readline.php b/src/Package/Extension/readline.php index 6395057e5..80f3fa334 100644 --- a/src/Package/Extension/readline.php +++ b/src/Package/Extension/readline.php @@ -4,6 +4,7 @@ namespace Package\Extension; +use Package\Target\php; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; @@ -15,7 +16,7 @@ #[Extension('readline')] class readline { - #[BeforeStage('php', 'unix-make-cli', 'ext-readline')] + #[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')] #[PatchDescription('Fix readline static build with musl')] public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { @@ -25,7 +26,7 @@ public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterfa } } - #[AfterStage('php', 'unix-make-cli', 'ext-readline')] + #[AfterStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')] public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { if ($toolchain->isStatic()) { diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index a80ff015d..bacfbe2eb 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -4,6 +4,7 @@ namespace Package\Library; +use Package\Target\php; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\PatchDescription; @@ -13,7 +14,7 @@ #[Library('imap')] class imap { - #[AfterStage('php', 'patch-embed-scripts', 'imap')] + #[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')] #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] public function afterPatchScripts(): void { diff --git a/src/Package/Library/libedit.php b/src/Package/Library/libedit.php index 2dac2817e..08a435da7 100644 --- a/src/Package/Library/libedit.php +++ b/src/Package/Library/libedit.php @@ -14,7 +14,7 @@ #[Library('libedit')] class libedit extends LibraryPackage { - #[BeforeStage('libedit', 'build')] + #[BeforeStage(stage: 'build')] public function patchBeforeBuild(): void { FileSystem::replaceFileRegex( diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 6636ccad5..bd96da2c9 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -4,6 +4,7 @@ namespace Package\Library; +use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\PatchDescription; @@ -12,7 +13,7 @@ #[Library('postgresql')] class postgresql { - #[BeforeStage('php', 'unix-configure', 'postgresql')] + #[BeforeStage('php', [php::class, 'configureForUnix'], 'postgresql')] #[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')] public function patchBeforePHPConfigure(TargetPackage $package): void { diff --git a/src/Package/Target/micro.php b/src/Package/Target/micro.php index a95d4b4d6..64772efcf 100644 --- a/src/Package/Target/micro.php +++ b/src/Package/Target/micro.php @@ -13,7 +13,7 @@ #[Target('php-micro')] class micro { - #[BeforeStage('php', 'unix-make-embed', 'php-micro')] + #[BeforeStage('php', [php::class, 'makeEmbedForUnix'], 'php-micro')] #[PatchDescription('Patch Makefile to build only libphp.la for embedding')] public function patchBeforeEmbed(TargetPackage $package): void { diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 4d3683ce1..90c68a24b 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,7 +4,6 @@ namespace Package\Target; -use StaticPHP\Artifact\ArtifactLoader; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Info; @@ -21,9 +20,10 @@ use StaticPHP\Package\Package; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; -use StaticPHP\Package\PackageLoader; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\TargetPackage; +use StaticPHP\Registry\ArtifactLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ToolchainManager; @@ -241,7 +241,7 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void FileSystem::removeDir(BUILD_MODULES_PATH); } - #[BeforeStage('php', 'unix-buildconf')] + #[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 @@ -259,7 +259,7 @@ public function patchBeforeBuildconf(TargetPackage $package): void FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } - #[Stage('unix-buildconf')] + #[Stage] public function buildconfForUnix(TargetPackage $package): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); @@ -267,7 +267,7 @@ public function buildconfForUnix(TargetPackage $package): void shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } - #[Stage('unix-configure')] + #[Stage] public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure')); @@ -317,7 +317,7 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } - #[Stage('unix-make')] + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { V2CompatLayer::emitPatchPoint('before-php-make'); @@ -326,23 +326,23 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) shell()->cd($package->getSourceDir())->exec('make clean'); if ($installer->isPackageResolved('php-cli')) { - $package->runStage('unix-make-cli'); + $package->runStage([self::class, 'makeCliForUnix']); } if ($installer->isPackageResolved('php-cgi')) { - $package->runStage('unix-make-cgi'); + $package->runStage([self::class, 'makeCgiForUnix']); } if ($installer->isPackageResolved('php-fpm')) { - $package->runStage('unix-make-fpm'); + $package->runStage([self::class, 'makeFpmForUnix']); } if ($installer->isPackageResolved('php-micro')) { - $package->runStage('unix-make-micro'); + $package->runStage([self::class, 'makeMicroForUnix']); } if ($installer->isPackageResolved('php-embed')) { - $package->runStage('unix-make-embed'); + $package->runStage([self::class, 'makeEmbedForUnix']); } } - #[Stage('unix-make-cli')] + #[Stage] public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); @@ -352,7 +352,7 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install ->exec("make -j{$concurrency} cli"); } - #[Stage('unix-make-cgi')] + #[Stage] public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); @@ -362,7 +362,7 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install ->exec("make -j{$concurrency} cgi"); } - #[Stage('unix-make-fpm')] + #[Stage] public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); @@ -372,7 +372,7 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install ->exec("make -j{$concurrency} fpm"); } - #[Stage('unix-make-micro')] + #[Stage] #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { @@ -399,7 +399,7 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta } } - #[Stage('unix-make-embed')] + #[Stage] public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); @@ -452,10 +452,10 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta UnixUtil::exportDynamicSymbols($libphp_a); // deploy embed php scripts - $package->runStage('patch-embed-scripts'); + $package->runStage([$this, 'patchEmbedScripts']); } - #[Stage('unix-build-shared-ext')] + #[Stage] public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void { // collect shared extensions @@ -506,18 +506,18 @@ public function build(TargetPackage $package): void return; } - $package->runStage('unix-buildconf'); - $package->runStage('unix-configure'); - $package->runStage('unix-make'); + $package->runStage([$this, 'buildconfForUnix']); + $package->runStage([$this, 'configureForUnix']); + $package->runStage([$this, 'makeForUnix']); - $package->runStage('unix-build-shared-ext'); + $package->runStage([$this, 'unixBuildSharedExt']); } /** * Patch phpize and php-config if needed */ - #[Stage('patch-embed-scripts')] - public function patchPhpScripts(): void + #[Stage] + public function patchEmbedScripts(): void { // patch phpize if (file_exists(BUILD_BIN_PATH . '/phpize')) { diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 2b7ac0de6..315cfb11d 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -24,6 +24,7 @@ use StaticPHP\Exception\SPCException; use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 11b738a70..f860ec0f8 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -9,6 +9,7 @@ use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\Package; +use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; diff --git a/src/StaticPHP/Attribute/Package/AfterStage.php b/src/StaticPHP/Attribute/Package/AfterStage.php index 3c611d133..466a2d3a7 100644 --- a/src/StaticPHP/Attribute/Package/AfterStage.php +++ b/src/StaticPHP/Attribute/Package/AfterStage.php @@ -10,5 +10,5 @@ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] readonly class AfterStage { - public function __construct(public string $package_name, public string $stage, public ?string $only_when_package_resolved = null) {} + public function __construct(public string $package_name, public array|string $stage, public ?string $only_when_package_resolved = null) {} } diff --git a/src/StaticPHP/Attribute/Package/BeforeStage.php b/src/StaticPHP/Attribute/Package/BeforeStage.php index c781a4e6f..182f6b5b8 100644 --- a/src/StaticPHP/Attribute/Package/BeforeStage.php +++ b/src/StaticPHP/Attribute/Package/BeforeStage.php @@ -8,7 +8,12 @@ * Indicates that the annotated method should be executed before a specific stage of the build process for a given package. */ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -readonly class BeforeStage +class BeforeStage { - public function __construct(public string $package_name, public string $stage, public ?string $only_when_package_resolved = null) {} + public readonly array|string $stage; + + public function __construct(public string $package_name = '', array|callable|string $stage = '', public ?string $only_when_package_resolved = null) + { + $this->stage = $stage; + } } diff --git a/src/StaticPHP/Attribute/Package/Stage.php b/src/StaticPHP/Attribute/Package/Stage.php index e801cddf3..9f88bc94c 100644 --- a/src/StaticPHP/Attribute/Package/Stage.php +++ b/src/StaticPHP/Attribute/Package/Stage.php @@ -10,5 +10,5 @@ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] readonly class Stage { - public function __construct(public string $name) {} + public function __construct(public ?string $function = null) {} } diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index 5efb9f1af..e66f514b0 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -6,7 +6,7 @@ use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Package\PackageInstaller; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Input\InputOption; diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 92b80be17..277585e51 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -6,7 +6,7 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\DownloaderOptions; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index d28d2bbe3..14951a341 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -6,9 +6,9 @@ use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactExtractor; -use StaticPHP\Artifact\ArtifactLoader; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\ArtifactLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 0484c1114..a12227fc0 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -13,8 +13,9 @@ use StaticPHP\Command\ExtractCommand; use StaticPHP\Command\InstallPackageCommand; use StaticPHP\Command\SPCConfigCommand; -use StaticPHP\Package\PackageLoader; use StaticPHP\Package\TargetPackage; +use StaticPHP\Registry\PackageLoader; +use StaticPHP\Registry\Registry; use Symfony\Component\Console\Application; class ConsoleApplication extends Application @@ -29,6 +30,9 @@ public function __construct() require_once ROOT_DIR . '/src/bootstrap.php'; + // check registry + Registry::checkLoadedRegistries(); + /** * @var string $name * @var TargetPackage $package diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 692a5b8c5..22ca10f28 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -7,6 +7,7 @@ use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; +use StaticPHP\Registry\DoctorLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index c8d00574b..4157e9d60 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -38,13 +38,6 @@ public function checkZig(): CheckResult #[FixItem('install-zig')] public function installZig(): bool { - $arch = arch2gnu(php_uname('m')); - $os = match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; $installer = new PackageInstaller(); $installer->addInstallPackage('zig'); $installer->run(false); diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 36ed1a63c..db11b8fd8 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -25,11 +25,13 @@ class ExceptionHandler SPCInternalException::class, ValidationException::class, WrongUsageException::class, + RegistryException::class, ]; public const array MINOR_LOG_EXCEPTIONS = [ InterruptException::class, WrongUsageException::class, + RegistryException::class, ]; /** @var null|BuilderBase Builder binding */ @@ -52,6 +54,7 @@ public static function handleSPCException(SPCException $e): void SPCInternalException::class => "✗ SPC internal error: {$e->getMessage()}", ValidationException::class => "⚠ Validation failed: {$e->getMessage()}", WrongUsageException::class => $e->getMessage(), + RegistryException::class => "✗ Registry parsing error: {$e->getMessage()}", default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}", }; self::logError($head_msg); diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php new file mode 100644 index 000000000..347a132ad --- /dev/null +++ b/src/StaticPHP/Exception/RegistryException.php @@ -0,0 +1,7 @@ +stages[$name])) { + if (!$this->hasStage($name)) { + $name = match (true) { + is_string($name) => $name, + is_array($name) && count($name) === 2 => $name[1], // use function name + default => '{' . gettype($name) . '}', + }; throw new SPCInternalException("Stage '{$name}' is not defined for package '{$this->name}'."); } + $name = match (true) { + is_string($name) => $name, + is_array($name) && count($name) === 2 => $name[1], // use function name + default => throw new SPCInternalException('Invalid stage name type: ' . gettype($name)), + }; // Merge package context with provided context /** @noinspection PhpDuplicateArrayKeysInspection */ @@ -80,9 +91,6 @@ public function isInstalled(): bool /** * Add a stage to the package. - * - * @param string $name Stage name - * @param callable $stage Stage callable */ public function addStage(string $name, callable $stage): void { @@ -92,11 +100,17 @@ public function addStage(string $name, callable $stage): void /** * Check if the package has a specific stage defined. * - * @param string $name Stage name + * @param mixed $name Stage name */ - public function hasStage(string $name): bool + public function hasStage(mixed $name): bool { - return isset($this->stages[$name]); + if (is_array($name) && count($name) === 2) { + return isset($this->stages[$name[1]]); // use function name + } + if (is_string($name)) { + return isset($this->stages[$name]); // use defined name + } + return false; } /** diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 4be980904..9770a7370 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -11,6 +11,7 @@ use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 667d9688b..7853be08b 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -4,6 +4,7 @@ namespace StaticPHP\Package; +use StaticPHP\Attribute\Package\Stage; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\ValidationException; @@ -100,22 +101,6 @@ public function getPhpConfigureArg(string $os, bool $shared): string public function setBuildShared(bool $build_shared = true): void { $this->build_shared = $build_shared; - // Add build stages for shared build on Unix-like systems - // TODO: Windows shared build support - if ($build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { - if (!$this->hasStage('build')) { - $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, '_buildSharedUnix']); - } - if (!$this->hasStage('phpize')) { - $this->addStage('phpize', [$this, '_phpize']); - } - if (!$this->hasStage('configure')) { - $this->addStage('configure', [$this, '_configure']); - } - if (!$this->hasStage('make')) { - $this->addStage('make', [$this, '_make']); - } - } } public function setBuildStatic(bool $build_static = true): void @@ -180,18 +165,18 @@ public function getSharedExtensionEnv(): array /** * @internal - * #[Stage('phpize')] */ - public function _phpize(array $env, PhpExtensionPackage $package): void + #[Stage] + public function phpizeForUnix(array $env, PhpExtensionPackage $package): void { shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize'); } /** * @internal - * #[Stage('configure')] */ - public function _configure(array $env, PhpExtensionPackage $package): void + #[Stage] + public function configureForUnix(array $env, PhpExtensionPackage $package): void { $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; shell()->cd($package->getSourceDir()) @@ -205,9 +190,9 @@ public function _configure(array $env, PhpExtensionPackage $package): void /** * @internal - * #[Stage('make')] */ - public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void + #[Stage] + public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void { shell()->cd($package->getSourceDir()) ->setEnv($env) @@ -222,13 +207,13 @@ public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $ * @internal * #[Stage('build')] */ - public function _buildSharedUnix(PackageBuilder $builder): void + public function buildSharedForUnix(PackageBuilder $builder): void { $env = $this->getSharedExtensionEnv(); - $this->runStage('phpize', ['env' => $env]); - $this->runStage('configure', ['env' => $env]); - $this->runStage('make', ['env' => $env]); + $this->runStage('phpizeForUnix', ['env' => $env]); + $this->runStage('configureForUnix', ['env' => $env]); + $this->runStage('makeForUnix', ['env' => $env]); // process *.so file $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; @@ -238,6 +223,32 @@ public function _buildSharedUnix(PackageBuilder $builder): void $builder->deployBinary($soFile, $soFile, false); } + /** + * Register default stages if not already defined by attributes. + * This is called after all attributes have been loaded. + * + * @internal Called by PackageLoader after loading attributes + */ + public function registerDefaultStages(): void + { + // Add build stages for shared build on Unix-like systems + // TODO: Windows shared build support + if ($this->build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { + if (!$this->hasStage('build')) { + $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, 'buildSharedForUnix']); + } + if (!$this->hasStage('phpizeForUnix')) { + $this->addStage('phpizeForUnix', [$this, 'phpizeForUnix']); + } + if (!$this->hasStage('configureForUnix')) { + $this->addStage('configureForUnix', [$this, 'configureForUnix']); + } + if (!$this->hasStage('makeForUnix')) { + $this->addStage('makeForUnix', [$this, 'makeForUnix']); + } + } + } + /** * Splits a given string of library flags into static and shared libraries. * diff --git a/src/StaticPHP/Artifact/ArtifactLoader.php b/src/StaticPHP/Registry/ArtifactLoader.php similarity index 99% rename from src/StaticPHP/Artifact/ArtifactLoader.php rename to src/StaticPHP/Registry/ArtifactLoader.php index 6a839cb4b..22942452f 100644 --- a/src/StaticPHP/Artifact/ArtifactLoader.php +++ b/src/StaticPHP/Registry/ArtifactLoader.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace StaticPHP\Artifact; +namespace StaticPHP\Registry; +use StaticPHP\Artifact\Artifact; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\Artifact\BinaryExtract; diff --git a/src/StaticPHP/Doctor/DoctorLoader.php b/src/StaticPHP/Registry/DoctorLoader.php similarity index 99% rename from src/StaticPHP/Doctor/DoctorLoader.php rename to src/StaticPHP/Registry/DoctorLoader.php index 2bbbbd624..e992d5562 100644 --- a/src/StaticPHP/Doctor/DoctorLoader.php +++ b/src/StaticPHP/Registry/DoctorLoader.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace StaticPHP\Doctor; +namespace StaticPHP\Registry; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php similarity index 64% rename from src/StaticPHP/Package/PackageLoader.php rename to src/StaticPHP/Registry/PackageLoader.php index 5093dd7b5..15ff0f4a5 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace StaticPHP\Package; +namespace StaticPHP\Registry; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; @@ -19,8 +19,13 @@ use StaticPHP\Attribute\Package\Validate; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Exception\ValidationException; +use StaticPHP\Exception\RegistryException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\Package; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Package\TargetPackage; use StaticPHP\Util\FileSystem; class PackageLoader @@ -30,9 +35,7 @@ class PackageLoader private static array $before_stages = []; - private static array $after_stage = []; - - private static array $patch_before_builds = []; + private static array $after_stages = []; /** @var array Track loaded classes to prevent duplicates */ private static array $loaded_classes = []; @@ -53,7 +56,7 @@ public static function initPackageInstances(): void if ($pkg !== null) { self::$packages[$name] = $pkg; } else { - throw new WrongUsageException("Package [{$name}] has unknown type [{$item['type']}]"); + throw new RegistryException("Package [{$name}] has unknown type [{$item['type']}]"); } } } @@ -156,7 +159,7 @@ public static function loadFromClass(mixed $class): void } $package_type = PackageConfig::get($attribute_instance->name, 'type'); if ($package_type === null) { - throw new WrongUsageException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); + throw new RegistryException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); } // if class has parent class and matches the attribute instance, use custom class @@ -181,10 +184,10 @@ public static function loadFromClass(mixed $class): void default => null, }; if (!in_array($package_type, $pkg_type_attr, true)) { - throw new ValidationException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].'); + throw new RegistryException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].'); } if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) { - throw new ValidationException("Package [{$pkg->getName()}] config not found for class {$class}"); + throw new RegistryException("Package [{$pkg->getName()}] config not found for class {$class}"); } // init method attributes @@ -199,7 +202,7 @@ public static function loadFromClass(mixed $class): void // #[CustomPhpConfigureArg(PHP_OS_FAMILY)] CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]), // #[Stage('stage_name')] - Stage::class => $pkg->addStage($method_attribute->newInstance()->name, [$instance_class, $method->getName()]), + Stage::class => self::addStage($method, $pkg, $instance_class, $method_instance), // #[InitPackage] (run now with package context) InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [ Package::class => $pkg, @@ -232,9 +235,9 @@ public static function loadFromClass(mixed $class): void $method_instance = $method_attribute->newInstance(); match ($method_attribute->getName()) { // #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')] - BeforeStage::class => self::$before_stages[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved], - AfterStage::class => self::$after_stage[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved], - // #[PatchBeforeBuild() + BeforeStage::class => self::addBeforeStage($method, $pkg ?? null, $instance_class, $method_instance), + AfterStage::class => self::addAfterStage($method, $pkg ?? null, $instance_class, $method_instance), + default => null, }; } @@ -258,7 +261,7 @@ public static function getAfterStageCallbacks(string $package_name, string $stag { // match condition $installer = ApplicationContext::get(PackageInstaller::class); - $stages = self::$after_stage[$package_name][$stage] ?? []; + $stages = self::$after_stages[$package_name][$stage] ?? []; $result = []; foreach ($stages as [$callback, $only_when_package_resolved]) { if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { @@ -269,9 +272,53 @@ public static function getAfterStageCallbacks(string $package_name, string $stag return $result; } - public static function getPatchBeforeBuildCallbacks(string $package_name): array + /** + * Register default stages for all PhpExtensionPackage instances. + * Should be called after all registries have been loaded. + */ + public static function registerAllDefaultStages(): void { - return self::$patch_before_builds[$package_name] ?? []; + foreach (self::$packages as $pkg) { + if ($pkg instanceof PhpExtensionPackage) { + $pkg->registerDefaultStages(); + } + } + } + + /** + * Check loaded stage events for consistency. + */ + public static function checkLoadedStageEvents(): void + { + foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) { + foreach ($ev_all as $package_name => $stages) { + // check package exists + if (!self::hasPackage($package_name)) { + throw new RegistryException( + "{$event_name} event registered for unknown package [{$package_name}]." + ); + } + $pkg = self::getPackage($package_name); + foreach ($stages as $stage_name => $before_events) { + foreach ($before_events as [$event_callable, $only_when_package_resolved]) { + // check only_when_package_resolved package exists + if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) { + throw new RegistryException("{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}]."); + } + // check callable is valid + if (!is_callable($event_callable)) { + throw new RegistryException( + "{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has invalid callable.", + ); + } + } + // check stage exists + if (!$pkg->hasStage($stage_name)) { + throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}]."); + } + } + } + } } /** @@ -280,7 +327,7 @@ public static function getPatchBeforeBuildCallbacks(string $package_name): array private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void { if (!$pkg instanceof PhpExtensionPackage) { - throw new ValidationException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute."); + throw new RegistryException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute."); } $pkg->addCustomPhpConfigureArgCallback($attr->os, $fn); } @@ -289,4 +336,44 @@ private static function addBuildFunction(Package $pkg, object $attr, callable $f { $pkg->addBuildFunction($attr->os, $fn); } + + private static function addStage(\ReflectionMethod $method, Package $pkg, object $instance_class, object $method_instance): void + { + $name = $method_instance->function; + if ($name === null) { + $name = $method->getName(); + } + $pkg->addStage($name, [$instance_class, $method->getName()]); + } + + private static function addBeforeStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void + { + /** @var BeforeStage $method_instance */ + $stage = $method_instance->stage; + $stage = match (true) { + is_string($stage) => $stage, + is_array($stage) && count($stage) === 2 => $stage[1], + default => throw new RegistryException('Invalid stage definition in BeforeStage attribute.'), + }; + if ($method_instance->package_name === '' && $pkg === null) { + throw new RegistryException('Package name must not be empty when no package context is available for BeforeStage attribute.'); + } + $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; + self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + } + + private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void + { + $stage = $method_instance->stage; + $stage = match (true) { + is_string($stage) => $stage, + is_array($stage) && count($stage) === 2 => $stage[1], + default => throw new RegistryException('Invalid stage definition in AfterStage attribute.'), + }; + if ($method_instance->package_name === '' && $pkg === null) { + throw new RegistryException('Package name must not be empty when no package context is available for AfterStage attribute.'); + } + $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; + self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + } } diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 1b579ef2c..71d53f825 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -4,13 +4,10 @@ namespace StaticPHP\Registry; -use StaticPHP\Artifact\ArtifactLoader; use StaticPHP\Config\ArtifactConfig; use StaticPHP\Config\PackageConfig; use StaticPHP\ConsoleApplication; -use StaticPHP\Doctor\DoctorLoader; -use StaticPHP\Exception\EnvironmentException; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Exception\RegistryException; use StaticPHP\Util\FileSystem; use Symfony\Component\Yaml\Yaml; @@ -30,19 +27,19 @@ public static function loadRegistry(string $registry_file, bool $auto_require = { $yaml = file_get_contents($registry_file); if ($yaml === false) { - throw new EnvironmentException("Failed to read registry file: {$registry_file}"); + throw new RegistryException("Failed to read registry file: {$registry_file}"); } $data = match (pathinfo($registry_file, PATHINFO_EXTENSION)) { 'json' => json_decode($yaml, true), 'yaml', 'yml' => Yaml::parse($yaml), - default => throw new EnvironmentException("Unsupported registry file format: {$registry_file}"), + default => throw new RegistryException("Unsupported registry file format: {$registry_file}"), }; if (!is_array($data)) { - throw new EnvironmentException("Invalid registry format in file: {$registry_file}"); + throw new RegistryException("Invalid registry format in file: {$registry_file}"); } $registry_name = $data['name'] ?? null; if (!is_string($registry_name) || empty($registry_name)) { - throw new EnvironmentException("Registry 'name' is missing or invalid in file: {$registry_file}"); + throw new RegistryException("Registry 'name' is missing or invalid in file: {$registry_file}"); } // Prevent loading the same registry twice @@ -190,6 +187,16 @@ public static function loadFromEnvOrOption(?string $registries = null): void } } + public static function checkLoadedRegistries(): void + { + // Register default stages for all PhpExtensionPackage instances + // This must be done after all registries are loaded to ensure custom stages take precedence + PackageLoader::registerAllDefaultStages(); + + // check BeforeStage, AfterStage is valid + PackageLoader::checkLoadedStageEvents(); + } + /** * Get list of loaded registry names. * @@ -252,7 +259,7 @@ private static function requireClassFile(string $class, ?string $file_path, stri } // Class not found and no file path provided - throw new EnvironmentException( + throw new RegistryException( "Class '{$class}' not found. For external registries, either:\n" . " 1. Add an 'autoload' entry pointing to your composer autoload file\n" . " 2. Use 'psr-4' instead of 'classes' for auto-discovery\n" . @@ -272,7 +279,7 @@ private static function fullpath(string $path, string $relative_path_base): stri $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; } if (!file_exists($path)) { - throw new EnvironmentException("Path does not exist: {$path}"); + throw new RegistryException("Path does not exist: {$path}"); } return FileSystem::convertPath($path); } From ac01867e9c39e1101c6ea00df408d11c4a706442 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 15:01:41 +0800 Subject: [PATCH 065/682] Refactor stage execution to use method references for improved clarity --- src/StaticPHP/Package/PhpExtensionPackage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 7853be08b..84aa3020d 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -211,9 +211,9 @@ public function buildSharedForUnix(PackageBuilder $builder): void { $env = $this->getSharedExtensionEnv(); - $this->runStage('phpizeForUnix', ['env' => $env]); - $this->runStage('configureForUnix', ['env' => $env]); - $this->runStage('makeForUnix', ['env' => $env]); + $this->runStage([$this, 'phpizeForUnix'], ['env' => $env]); + $this->runStage([$this, 'configureForUnix'], ['env' => $env]); + $this->runStage([$this, 'makeForUnix'], ['env' => $env]); // process *.so file $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; From b0f630f95f0a7fb29da4dfa6cc7695535bb4a7f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 16:34:43 +0800 Subject: [PATCH 066/682] Add package outputs, colorize motd --- src/Package/Target/php.php | 19 +++++++- .../Attribute/Package/PatchBeforeBuild.php | 11 ----- src/StaticPHP/Command/BaseCommand.php | 5 +- src/StaticPHP/Command/BuildTargetCommand.php | 2 + src/StaticPHP/DI/CallbackInvoker.php | 46 +++++++++++++++++++ src/StaticPHP/Package/Package.php | 14 ++++++ src/StaticPHP/Package/PackageInstaller.php | 10 ++++ src/StaticPHP/Registry/PackageLoader.php | 17 ++----- 8 files changed, 98 insertions(+), 26 deletions(-) delete mode 100644 src/StaticPHP/Attribute/Package/PatchBeforeBuild.php diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 90c68a24b..1e1c5930c 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -45,7 +45,7 @@ #[Target('php-cgi')] #[Target('php-embed')] #[Target('frankenphp')] -class php +class php extends TargetPackage { public static function getPHPVersionID(): int { @@ -350,6 +350,9 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install 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] @@ -360,6 +363,9 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install 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] @@ -370,6 +376,9 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install 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] @@ -392,6 +401,7 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta ->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(); @@ -432,12 +442,17 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta } // 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(); 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 ------------- @@ -524,6 +539,7 @@ public function patchEmbedScripts(): void 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')) { @@ -535,6 +551,7 @@ public function patchEmbedScripts(): void // 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'); } } diff --git a/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php b/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php deleted file mode 100644 index 2343954b9..000000000 --- a/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php +++ /dev/null @@ -1,11 +0,0 @@ -getVersionWithCommit(); if (!$this->no_motd) { - echo str_replace('{version}', $version, self::$motd); + echo str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); } } diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index e66f514b0..2756070bf 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -51,6 +51,8 @@ public function handle(): int $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + $installer->printBuildPackageOutputs(); + return static::SUCCESS; } } diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php index f14f94680..fa11a7f13 100644 --- a/src/StaticPHP/DI/CallbackInvoker.php +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -26,6 +26,10 @@ public function __construct( * 4. Default value * 5. Null (if nullable) * + * Note: For object values in context, the invoker automatically registers + * the object under all its parent classes and interfaces, allowing type hints + * to match any type in the inheritance hierarchy. + * * @param callable $callback The callback to invoke * @param array $context Context parameters (type => value or name => value) * @@ -35,6 +39,9 @@ public function __construct( */ public function invoke(callable $callback, array $context = []): mixed { + // Expand context to include all parent classes and interfaces for objects + $context = $this->expandContextHierarchy($context); + $reflection = new \ReflectionFunction(\Closure::fromCallable($callback)); $args = []; @@ -95,4 +102,43 @@ private function isBuiltinType(string $typeName): bool 'void', 'null', 'false', 'true', 'never', ], true); } + + /** + * Expand context to include all parent classes and interfaces for object values. + * This allows type hints to match any type in the object's inheritance hierarchy. + * + * @param array $context Original context array + * @return array Expanded context with all class hierarchy mappings + */ + private function expandContextHierarchy(array $context): array + { + $expanded = []; + + foreach ($context as $key => $value) { + // Keep the original key-value pair + $expanded[$key] = $value; + + // If value is an object, add mappings for all parent classes and interfaces + if (is_object($value)) { + $reflection = new \ReflectionClass($value); + + // Add concrete class + $expanded[$reflection->getName()] = $value; + + // Add all parent classes + while ($parent = $reflection->getParentClass()) { + $expanded[$parent->getName()] = $value; + $reflection = $parent; + } + + // Add all interfaces + $interfaces = (new \ReflectionClass($value))->getInterfaceNames(); + foreach ($interfaces as $interface) { + $expanded[$interface] = $value; + } + } + } + + return $expanded; + } } diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 0317e1bc5..6cad1fabd 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -23,6 +23,9 @@ abstract class Package /** @var array $build_functions Build functions for different OS binding */ protected array $build_functions = []; + /** @var array */ + protected array $outputs = []; + /** * @param string $name Name of the package * @param string $type Type of the package @@ -69,6 +72,17 @@ public function runStage(mixed $name, array $context = []): mixed return $ret; } + public function setOutput(string $key, string $value): static + { + $this->outputs[$key] = $value; + return $this; + } + + public function getOutputs(): array + { + return $this->outputs; + } + /** * Add a build function for a specific platform. * diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 9770a7370..96316887a 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -104,6 +104,16 @@ public function setDownload(bool $download = true): static return $this; } + public function printBuildPackageOutputs(): void + { + foreach ($this->build_packages as $package) { + if (($outputs = $package->getOutputs()) !== []) { + InteractiveTerm::notice('Package ' . ConsoleColor::green($package->getName()) . ' outputs'); + $this->printArrayInfo(info: $outputs); + } + } + } + /** * Run the package installation process. */ diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 15ff0f4a5..0ef3fb8ee 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -12,7 +12,6 @@ use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; use StaticPHP\Attribute\Package\Library; -use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\Package\ResolveBuild; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; @@ -166,16 +165,14 @@ public static function loadFromClass(mixed $class): void if ($refClass->getParentClass() !== false) { if (is_a($class_name, Package::class, true)) { self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type); - $instance_class = self::$packages[$attribute_instance->name]; } } - if (!isset($instance_class)) { - $instance_class = $refClass->newInstance(); - } - $pkg = self::$packages[$attribute_instance->name]; + // Use the package instance if it's a Package subclass, otherwise create a new instance + $instance_class = is_a($class_name, Package::class, true) ? $pkg : $refClass->newInstance(); + // validate package type matches $pkg_type_attr = match ($attribute->getName()) { Target::class => ['target', 'virtual-target'], @@ -204,18 +201,13 @@ public static function loadFromClass(mixed $class): void // #[Stage('stage_name')] Stage::class => self::addStage($method, $pkg, $instance_class, $method_instance), // #[InitPackage] (run now with package context) - InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [ - Package::class => $pkg, - $pkg::class => $pkg, - ]), + InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], ['package' => $pkg]), // #[InitBuild] ResolveBuild::class => $pkg instanceof TargetPackage ? $pkg->setResolveBuildCallback([$instance_class, $method->getName()]) : null, // #[Info] Info::class => $pkg->setInfoCallback([$instance_class, $method->getName()]), // #[Validate] Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]), - // #[PatchBeforeBuild] - PatchBeforeBuild::class => $pkg->setPatchBeforeBuildCallback([$instance_class, $method->getName()]), default => null, }; } @@ -224,6 +216,7 @@ public static function loadFromClass(mixed $class): void self::$packages[$pkg->getName()] = $pkg; } + // For classes without package attributes, create a simple instance for non-package stage callbacks if (!isset($instance_class)) { $instance_class = $refClass->newInstance(); } From bcaef59a1551c40363317957af866b55c2f4ce91 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 16:54:29 +0800 Subject: [PATCH 067/682] Support full --no-ansi options --- src/StaticPHP/Command/BaseCommand.php | 3 +- src/StaticPHP/Exception/ExceptionHandler.php | 3 +- src/StaticPHP/Util/InteractiveTerm.php | 32 +++++++++++++++----- src/globals/functions.php | 4 +-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index b6c09f3f8..da01723ac 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -70,7 +70,8 @@ public function initialize(InputInterface $input, OutputInterface $output): void }); $version = $this->getVersionWithCommit(); if (!$this->no_motd) { - echo str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); + $str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); + echo $this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str; } } diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index db11b8fd8..a77327634 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -10,6 +10,7 @@ use SPC\builder\macos\MacOSBuilder; use SPC\builder\windows\WindowsBuilder; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; class ExceptionHandler @@ -189,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) { - echo ConsoleColor::red($line) . PHP_EOL; + InteractiveTerm::plain(ConsoleColor::red($line) . ''); } } } diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 01e4bdc92..479327633 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -6,6 +6,7 @@ use StaticPHP\DI\ApplicationContext; use Symfony\Component\Console\Helper\ProgressIndicator; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use ZM\Logger\ConsoleColor; @@ -15,50 +16,55 @@ class InteractiveTerm public static function notice(string $message, bool $indent = false): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->notice(strip_ansi_colors($message)); } else { - $output->writeln(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message)); } } public static function success(string $message, bool $indent = false): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { - $output->writeln(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); } } public static function plain(string $message): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { - $output->writeln($message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } } public static function info(string $message): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if (!$output->isVerbose()) { - $output->writeln(ConsoleColor::green('▶ ') . $message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green('▶ ') . $message)); } logger()->info(strip_ansi_colors($message)); } public static function error(string $message, bool $indent = true): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->error(strip_ansi_colors($message)); } else { - $output->writeln('' . ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message)); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message))); } } @@ -69,11 +75,14 @@ public static function advance(): void public static function setMessage(string $message): void { - self::$indicator?->setMessage($message); + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; + self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } public static function finish(string $message, bool $status = true): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; + $message = $no_ansi ? strip_ansi_colors($message) : $message; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { if ($status) { @@ -85,9 +94,9 @@ public static function finish(string $message, bool $status = true): void } if (self::$indicator !== null) { if (!$status) { - self::$indicator->finish($message, '' . ConsoleColor::red(' ✘')); + self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(' ✘'))); } else { - self::$indicator->finish($message, '' . ConsoleColor::green(' ✔')); + self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(' ✔'))); } self::$indicator = null; } @@ -95,6 +104,7 @@ public static function finish(string $message, bool $status = true): void public static function indicateProgress(string $message): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); @@ -106,6 +116,12 @@ public static function indicateProgress(string $message): void self::$indicator->advance(); return; } + // if no ansi, use a dot instead of spinner + if ($no_ansi) { + self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' •', ' •']); + self::$indicator->start(strip_ansi_colors($message)); + return; + } self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' ⠏', ' ⠛', ' ⠹', ' ⢸', ' ⣰', ' ⣤', ' ⣆', ' ⡇']); self::$indicator->start($message); } diff --git a/src/globals/functions.php b/src/globals/functions.php index 8621e7adc..93cd1ae09 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -271,11 +271,11 @@ function keyboard_interrupt_unregister(): void /** * Strip ANSI color codes from a string. */ -function strip_ansi_colors(string $text): string +function strip_ansi_colors(string|Stringable $text): string { // Regular expression to match ANSI escape sequences // Including color codes, cursor control, clear screen and other control sequences - return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text); + return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', strval($text)); } /** From 4a968757baadfa726f6a8362d40131b4ed0cff80 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 10 Dec 2025 09:49:20 +0800 Subject: [PATCH 068/682] Update src/Package/Library/ncurses.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Library/ncurses.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index f0a217148..c7c39dc1f 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -36,7 +36,7 @@ public function build(LibraryPackage $package, ToolchainInterface $toolchain): v '--without-tests', '--without-dlsym', '--without-debug', - '-enable-symlinks', + '--enable-symlinks', "--bindir={$package->getBinDir()}", "--includedir={$package->getIncludeDir()}", "--libdir={$package->getLibDir()}", From f68adc3256d52e74e9455bb0006223ef44bfccaa Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 10 Dec 2025 09:52:59 +0800 Subject: [PATCH 069/682] Update src/Package/Target/php.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Target/php.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 1e1c5930c..03b50073b 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -447,6 +447,7 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta // 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); From bde1440617e0c32ec585c9c33a962971167df65e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 10 Dec 2025 11:15:44 +0800 Subject: [PATCH 070/682] Refactor test structure and update paths for improved organization --- .php-cs-fixer.php | 2 +- composer.json | 2 +- src/StaticPHP/Artifact/ArtifactExtractor.php | 2 - src/StaticPHP/DI/ApplicationContext.php | 16 +- src/StaticPHP/DI/CallbackInvoker.php | 25 +- src/StaticPHP/Package/PackageInstaller.php | 2 - src/StaticPHP/Registry/Registry.php | 2 +- tests/SPC/GlobalDefinesTest.php | 25 - tests/SPC/GlobalFunctionsTest.php | 33 - tests/SPC/builder/BuilderTest.php | 246 ------ tests/SPC/builder/ExtensionTest.php | 94 --- tests/SPC/builder/linux/SystemUtilTest.php | 60 -- tests/SPC/builder/macos/SystemUtilTest.php | 31 - tests/SPC/builder/unix/UnixSystemUtilTest.php | 42 - tests/SPC/doctor/CheckListHandlerTest.php | 24 - tests/SPC/globals/GlobalFunctionsTest.php | 94 --- tests/SPC/store/ConfigTest.php | 68 -- tests/SPC/store/CurlHookTest.php | 29 - tests/SPC/store/DownloaderTest.php | 113 --- tests/SPC/store/FileSystemTest.php | 176 ---- tests/SPC/util/ConfigValidatorTest.php | 767 ------------------ tests/SPC/util/DependencyUtilTest.php | 113 --- tests/SPC/util/GlobalEnvManagerTest.php | 143 ---- tests/SPC/util/LicenseDumperTest.php | 110 --- tests/SPC/util/PkgConfigUtilTest.php | 210 ----- tests/SPC/util/SPCConfigUtilTest.php | 74 -- tests/SPC/util/SPCTargetTest.php | 106 --- tests/SPC/util/TestBase.php | 100 --- tests/SPC/util/UnixShellTest.php | 184 ----- tests/SPC/util/WindowsCmdTest.php | 68 -- tests/StaticPHP/Config/ArtifactConfigTest.php | 303 +++++++ tests/StaticPHP/Config/ConfigTypeTest.php | 196 +++++ .../StaticPHP/Config/ConfigValidatorTest.php | 627 ++++++++++++++ tests/StaticPHP/Config/PackageConfigTest.php | 434 ++++++++++ tests/StaticPHP/DI/ApplicationContextTest.php | 433 ++++++++++ tests/StaticPHP/DI/CallbackInvokerTest.php | 629 ++++++++++++++ .../StaticPHP/Registry/ArtifactLoaderTest.php | 440 ++++++++++ tests/StaticPHP/Registry/DoctorLoaderTest.php | 374 +++++++++ .../StaticPHP/Registry/PackageLoaderTest.php | 550 +++++++++++++ tests/StaticPHP/Registry/RegistryTest.php | 378 +++++++++ tests/assets/filelist.gz | Bin 2375 -> 0 bytes ..._api_AOMediaCodec_libavif_releases.json.gz | Bin 15506 -> 0 bytes tests/bootstrap.php | 12 +- 43 files changed, 4396 insertions(+), 2941 deletions(-) delete mode 100644 tests/SPC/GlobalDefinesTest.php delete mode 100644 tests/SPC/GlobalFunctionsTest.php delete mode 100644 tests/SPC/builder/BuilderTest.php delete mode 100644 tests/SPC/builder/ExtensionTest.php delete mode 100644 tests/SPC/builder/linux/SystemUtilTest.php delete mode 100644 tests/SPC/builder/macos/SystemUtilTest.php delete mode 100644 tests/SPC/builder/unix/UnixSystemUtilTest.php delete mode 100644 tests/SPC/doctor/CheckListHandlerTest.php delete mode 100644 tests/SPC/globals/GlobalFunctionsTest.php delete mode 100644 tests/SPC/store/ConfigTest.php delete mode 100644 tests/SPC/store/CurlHookTest.php delete mode 100644 tests/SPC/store/DownloaderTest.php delete mode 100644 tests/SPC/store/FileSystemTest.php delete mode 100644 tests/SPC/util/ConfigValidatorTest.php delete mode 100644 tests/SPC/util/DependencyUtilTest.php delete mode 100644 tests/SPC/util/GlobalEnvManagerTest.php delete mode 100644 tests/SPC/util/LicenseDumperTest.php delete mode 100644 tests/SPC/util/PkgConfigUtilTest.php delete mode 100644 tests/SPC/util/SPCConfigUtilTest.php delete mode 100644 tests/SPC/util/SPCTargetTest.php delete mode 100644 tests/SPC/util/TestBase.php delete mode 100644 tests/SPC/util/UnixShellTest.php delete mode 100644 tests/SPC/util/WindowsCmdTest.php create mode 100644 tests/StaticPHP/Config/ArtifactConfigTest.php create mode 100644 tests/StaticPHP/Config/ConfigTypeTest.php create mode 100644 tests/StaticPHP/Config/ConfigValidatorTest.php create mode 100644 tests/StaticPHP/Config/PackageConfigTest.php create mode 100644 tests/StaticPHP/DI/ApplicationContextTest.php create mode 100644 tests/StaticPHP/DI/CallbackInvokerTest.php create mode 100644 tests/StaticPHP/Registry/ArtifactLoaderTest.php create mode 100644 tests/StaticPHP/Registry/DoctorLoaderTest.php create mode 100644 tests/StaticPHP/Registry/PackageLoaderTest.php create mode 100644 tests/StaticPHP/Registry/RegistryTest.php delete mode 100644 tests/assets/filelist.gz delete mode 100644 tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d1c30090e..b9eb063ef 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -69,6 +69,6 @@ 'php_unit_data_provider_method_order' => false, ]) ->setFinder( - PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/SPC']) + PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/StaticPHP']) ) ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()); diff --git a/composer.json b/composer.json index eadd2732c..fb17f9a0d 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ }, "autoload-dev": { "psr-4": { - "SPC\\Tests\\": "tests/SPC" + "Tests\\StaticPHP\\": "tests/StaticPHP" } }, "bin": [ diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index f860ec0f8..778c24f31 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -352,8 +352,6 @@ protected function isAlreadyExtracted(string $path, ?string $expected_hash): boo * @param string $name Artifact name (for error messages) * @param string $source_file Path to the source file or directory * @param string $cache_type Cache type: archive, git, local - * - * @throws WrongUsageException if source file does not exist */ protected function validateSourceFile(string $name, string $source_file, string $cache_type): void { diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index 9b702ccbc..a845fba27 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -35,8 +35,6 @@ class ApplicationContext * @param array $options Initialization options * - 'debug': Enable debug mode (disables compilation) * - 'definitions': Additional container definitions - * - * @throws \RuntimeException If already initialized */ public static function initialize(array $options = []): Container { @@ -60,7 +58,8 @@ public static function initialize(array $options = []): Container self::$debug = $options['debug'] ?? false; self::$container = $builder->build(); - self::$invoker = new CallbackInvoker(self::$container); + // Get invoker from container to ensure singleton consistency + self::$invoker = self::$container->get(CallbackInvoker::class); return self::$container; } @@ -126,7 +125,8 @@ public static function bindCommandContext(InputInterface $input, OutputInterface public static function getInvoker(): CallbackInvoker { if (self::$invoker === null) { - self::$invoker = new CallbackInvoker(self::getContainer()); + // Get from container to ensure singleton consistency + self::$invoker = self::getContainer()->get(CallbackInvoker::class); } return self::$invoker; } @@ -139,14 +139,18 @@ public static function getInvoker(): CallbackInvoker */ public static function invoke(callable $callback, array $context = []): mixed { - logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + if (function_exists('logger')) { + logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + } // get if callback has attribute PatchDescription $ref = new \ReflectionFunction(\Closure::fromCallable($callback)); $attributes = $ref->getAttributes(PatchDescription::class); foreach ($attributes as $attribute) { $attrInstance = $attribute->newInstance(); - logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}")); + if (function_exists('logger')) { + logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}")); + } } return self::getInvoker()->invoke($callback, $context); } diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php index fa11a7f13..0d77f7aab 100644 --- a/src/StaticPHP/DI/CallbackInvoker.php +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -5,12 +5,13 @@ namespace StaticPHP\DI; use DI\Container; +use StaticPHP\Exception\SPCInternalException; /** * CallbackInvoker is responsible for invoking callbacks with automatic dependency injection. * It supports context-based parameter resolution, allowing temporary bindings without polluting the container. */ -class CallbackInvoker +readonly class CallbackInvoker { public function __construct( private Container $container @@ -34,8 +35,6 @@ public function __construct( * @param array $context Context parameters (type => value or name => value) * * @return mixed The return value of the callback - * - * @throws \RuntimeException If a required parameter cannot be resolved */ public function invoke(callable $callback, array $context = []): mixed { @@ -64,8 +63,13 @@ public function invoke(callable $callback, array $context = []): mixed // 3. Look up in container by type if ($typeName !== null && !$this->isBuiltinType($typeName) && $this->container->has($typeName)) { - $args[] = $this->container->get($typeName); - continue; + try { + $args[] = $this->container->get($typeName); + continue; + } catch (\Throwable $e) { + // Container failed to resolve (e.g., missing constructor params) + // Fall through to try default value or nullable + } } // 4. Use default value if available @@ -81,7 +85,7 @@ public function invoke(callable $callback, array $context = []): mixed } // Cannot resolve parameter - throw new \RuntimeException( + throw new SPCInternalException( "Cannot resolve parameter '{$paramName}'" . ($typeName ? " of type '{$typeName}'" : '') . ' for callback invocation' @@ -120,19 +124,20 @@ private function expandContextHierarchy(array $context): array // If value is an object, add mappings for all parent classes and interfaces if (is_object($value)) { - $reflection = new \ReflectionClass($value); + $originalReflection = new \ReflectionClass($value); // Add concrete class - $expanded[$reflection->getName()] = $value; + $expanded[$originalReflection->getName()] = $value; // Add all parent classes + $reflection = $originalReflection; while ($parent = $reflection->getParentClass()) { $expanded[$parent->getName()] = $value; $reflection = $parent; } - // Add all interfaces - $interfaces = (new \ReflectionClass($value))->getInterfaceNames(); + // Add all interfaces - reuse original reflection + $interfaces = $originalReflection->getInterfaceNames(); foreach ($interfaces as $interface) { $expanded[$interface] = $value; } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 96316887a..1e5e27e8a 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -404,8 +404,6 @@ public function getPackage(string $package_name): ?Package /** * Validate that a package has required artifacts. - * - * @throws WrongUsageException if target/library package has no source or platform binary */ private function validatePackageArtifact(Package $package): void { diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 71d53f825..4ae5df4f4 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -25,7 +25,7 @@ class Registry */ public static function loadRegistry(string $registry_file, bool $auto_require = true): void { - $yaml = file_get_contents($registry_file); + $yaml = @file_get_contents($registry_file); if ($yaml === false) { throw new RegistryException("Failed to read registry file: {$registry_file}"); } diff --git a/tests/SPC/GlobalDefinesTest.php b/tests/SPC/GlobalDefinesTest.php deleted file mode 100644 index 956b35e25..000000000 --- a/tests/SPC/GlobalDefinesTest.php +++ /dev/null @@ -1,25 +0,0 @@ -assertTrue(defined('WORKING_DIR')); - } - - public function testInternalEnv(): void - { - require __DIR__ . '/../../src/globals/internal-env.php'; - $this->assertTrue(defined('GNU_ARCH')); - } -} diff --git a/tests/SPC/GlobalFunctionsTest.php b/tests/SPC/GlobalFunctionsTest.php deleted file mode 100644 index 6a8bd7388..000000000 --- a/tests/SPC/GlobalFunctionsTest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertEquals('abc', match_pattern('a*c', 'abc')); - $this->assertFalse(match_pattern('a*c', 'abcd')); - } - - public function testFExec(): void - { - $this->assertEquals('abc', f_exec('echo abc', $out, $ret)); - $this->assertEquals(0, $ret); - $this->assertEquals(['abc'], $out); - } - - public function testPatchPointInterrupt(): void - { - $except = patch_point_interrupt(0); - $this->assertInstanceOf(InterruptException::class, $except); - } -} diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php deleted file mode 100644 index b4b6258fd..000000000 --- a/tests/SPC/builder/BuilderTest.php +++ /dev/null @@ -1,246 +0,0 @@ -builder = BuilderProvider::makeBuilderByInput(new ArgvInput()); - [$extensions, $libs] = DependencyUtil::getExtsAndLibs(['mbregex']); - $this->builder->proveLibs($libs); - foreach ($extensions as $extension) { - $class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class; - $ext = new $class($extension, $this->builder); - $this->builder->addExt($ext); - } - foreach ($this->builder->getExts() as $ext) { - $ext->checkDependency(); - } - } - - public function testMakeBuilderByInput(): void - { - $this->assertInstanceOf(BuilderBase::class, BuilderProvider::makeBuilderByInput(new ArgvInput())); - $this->assertInstanceOf(BuilderBase::class, BuilderProvider::getBuilder()); - } - - public function testGetLibAndGetLibs() - { - $this->assertIsArray($this->builder->getLibs()); - $this->assertInstanceOf(LibraryBase::class, $this->builder->getLib('onig')); - } - - public function testGetExtAndGetExts() - { - $this->assertIsArray($this->builder->getExts()); - $this->assertInstanceOf(Extension::class, $this->builder->getExt('mbregex')); - } - - public function testMakeExtensionArgs() - { - $this->assertStringContainsString('--enable-mbstring', $this->builder->makeStaticExtensionArgs()); - } - - public function testIsLibsOnly() - { - // mbregex is not libs only - $this->assertFalse($this->builder->isLibsOnly()); - } - - public function testGetPHPVersionID() - { - if (file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - $file = SOURCE_PATH . '/php-src/main/php_version.h'; - $cnt = preg_match('/PHP_VERSION_ID (\d+)/m', file_get_contents($file), $match); - if ($cnt !== 0) { - $this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID()); - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersionID(); - } - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersionID(); - } - } - - public function testGetPHPVersion() - { - if (file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - $file = SOURCE_PATH . '/php-src/main/php_version.h'; - $cnt = preg_match('/PHP_VERSION "(\d+\.\d+\.\d+(?:-[^"]+)?)/', file_get_contents($file), $match); - if ($cnt !== 0) { - $this->assertEquals($match[1], $this->builder->getPHPVersion()); - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersion(); - } - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersion(); - } - } - - public function testGetPHPVersionFromArchive() - { - $lock = file_exists(LockFile::LOCK_FILE) ? file_get_contents(LockFile::LOCK_FILE) : false; - if ($lock === false) { - $this->assertFalse($this->builder->getPHPVersionFromArchive()); - } else { - $lock = json_decode($lock, true); - $file = $lock['php-src']['filename'] ?? null; - if ($file === null) { - $this->assertFalse($this->builder->getPHPVersionFromArchive()); - } else { - $cnt = preg_match('/php-(\d+\.\d+\.\d+)/', $file, $match); - if ($cnt !== 0) { - $this->assertEquals($match[1], $this->builder->getPHPVersionFromArchive()); - } else { - $this->assertFalse($this->builder->getPHPVersionFromArchive()); - } - } - } - } - - public function testGetMicroVersion() - { - $file = FileSystem::convertPath(SOURCE_PATH . '/php-src/sapi/micro/php_micro.h'); - if (!file_exists($file)) { - $this->assertFalse($this->builder->getMicroVersion()); - } else { - $content = file_get_contents($file); - $ver = ''; - preg_match('/#define PHP_MICRO_VER_MAJ (\d)/m', $content, $match); - $ver .= $match[1] . '.'; - preg_match('/#define PHP_MICRO_VER_MIN (\d)/m', $content, $match); - $ver .= $match[1] . '.'; - preg_match('/#define PHP_MICRO_VER_PAT (\d)/m', $content, $match); - $ver .= $match[1]; - $this->assertEquals($ver, $this->builder->getMicroVersion()); - } - } - - public static function providerGetBuildTypeName(): array - { - return [ - [BUILD_TARGET_CLI, 'cli'], - [BUILD_TARGET_FPM, 'fpm'], - [BUILD_TARGET_MICRO, 'micro'], - [BUILD_TARGET_EMBED, 'embed'], - [BUILD_TARGET_FRANKENPHP, 'frankenphp'], - [BUILD_TARGET_ALL, 'cli, micro, fpm, embed, frankenphp, cgi'], - [BUILD_TARGET_CLI | BUILD_TARGET_EMBED, 'cli, embed'], - ]; - } - - /** - * @dataProvider providerGetBuildTypeName - */ - public function testGetBuildTypeName(int $target, string $name): void - { - $this->assertEquals($name, $this->builder->getBuildTypeName($target)); - } - - public function testGetOption() - { - // we cannot assure the option exists, so just tests default value - $this->assertEquals('foo', $this->builder->getOption('bar', 'foo')); - } - - public function testGetOptions() - { - $this->assertIsArray($this->builder->getOptions()); - } - - public function testSetOptionIfNotExist() - { - $this->assertEquals(null, $this->builder->getOption('bar')); - $this->builder->setOptionIfNotExist('bar', 'foo'); - $this->assertEquals('foo', $this->builder->getOption('bar')); - } - - public function testSetOption() - { - $this->assertEquals(null, $this->builder->getOption('bar')); - $this->builder->setOption('bar', 'foo'); - $this->assertEquals('foo', $this->builder->getOption('bar')); - } - - public function testGetEnvString() - { - $this->assertIsString($this->builder->getEnvString()); - putenv('TEST_SPC_BUILDER=foo'); - $this->assertStringContainsString('TEST_SPC_BUILDER=foo', $this->builder->getEnvString(['TEST_SPC_BUILDER'])); - } - - public function testValidateLibsAndExts() - { - $this->builder->validateLibsAndExts(); - $this->assertTrue(true); - } - - public static function providerEmitPatchPoint(): array - { - return [ - ['before-libs-extract'], - ['after-libs-extract'], - ['before-php-extract'], - ['after-php-extract'], - ['before-micro-extract'], - ['after-micro-extract'], - ['before-exts-extract'], - ['after-exts-extract'], - ['before-php-buildconf'], - ['before-php-configure'], - ['before-php-make'], - ['before-sanity-check'], - ]; - } - - /** - * @dataProvider providerEmitPatchPoint - */ - public function testEmitPatchPoint(string $point) - { - $code = 'builder->setOption('with-added-patch', ['/tmp/patch-point.' . $point . '.php']); - FileSystem::writeFile('/tmp/patch-point.' . $point . '.php', $code); - $this->expectOutputString('GOOD:' . $point); - $this->builder->emitPatchPoint($point); - } - - public function testEmitPatchPointNotExists() - { - $this->expectOutputRegex('/failed to run/'); - $this->expectException(WrongUsageException::class); - $this->builder->setOption('with-added-patch', ['/tmp/patch-point.not_exsssists.php']); - $this->builder->emitPatchPoint('not-exists'); - } -} diff --git a/tests/SPC/builder/ExtensionTest.php b/tests/SPC/builder/ExtensionTest.php deleted file mode 100644 index 89462065c..000000000 --- a/tests/SPC/builder/ExtensionTest.php +++ /dev/null @@ -1,94 +0,0 @@ -proveLibs($libs); - foreach ($extensions as $extension) { - $class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class; - $ext = new $class($extension, $builder); - $builder->addExt($ext); - } - foreach ($builder->getExts() as $ext) { - $ext->checkDependency(); - } - $this->extension = $builder->getExt('mbregex'); - } - - public function testPatches() - { - $this->assertFalse($this->extension->patchBeforeBuildconf()); - $this->assertFalse($this->extension->patchBeforeConfigure()); - $this->assertFalse($this->extension->patchBeforeMake()); - } - - public function testGetExtensionDependency() - { - $this->assertEquals('mbstring', current($this->extension->getExtensionDependency())->getName()); - } - - public function testGetWindowsConfigureArg() - { - $this->assertEquals('', $this->extension->getWindowsConfigureArg()); - } - - public function testGetConfigureArg() - { - $this->assertEquals('', $this->extension->getUnixConfigureArg()); - } - - public function testGetExtVersion() - { - // only swoole has version, we cannot test it - $this->assertEquals(null, $this->extension->getExtVersion()); - } - - public function testGetDistName() - { - $this->assertEquals('mbregex', $this->extension->getName()); - } - - public function testRunCliCheckWindows() - { - if (is_unix()) { - $this->markTestSkipped('This test is for Windows only'); - } else { - $this->extension->runCliCheckWindows(); - $this->assertTrue(true); - } - } - - public function testGetName() - { - $this->assertEquals('mbregex', $this->extension->getName()); - } - - public function testGetUnixConfigureArg() - { - $this->assertEquals('', $this->extension->getUnixConfigureArg()); - } - - public function testGetEnableArg() - { - $this->assertEquals('', $this->extension->getEnableArg()); - } -} diff --git a/tests/SPC/builder/linux/SystemUtilTest.php b/tests/SPC/builder/linux/SystemUtilTest.php deleted file mode 100644 index 01d555d81..000000000 --- a/tests/SPC/builder/linux/SystemUtilTest.php +++ /dev/null @@ -1,60 +0,0 @@ -assertArrayHasKey('dist', $release); - $this->assertArrayHasKey('ver', $release); - $this->assertTrue($release['dist'] === 'alpine' && SystemUtil::isMuslDist() || $release['dist'] !== 'alpine' && !SystemUtil::isMuslDist()); - } - - public function testFindStaticLib() - { - $this->assertIsArray(SystemUtil::findStaticLib('ld-linux-x86-64.so.2')); - } - - public function testGetCpuCount() - { - $this->assertIsInt(SystemUtil::getCpuCount()); - } - - public function testFindHeader() - { - $this->assertIsArray(SystemUtil::findHeader('elf.h')); - } - - public function testGetSupportedDistros() - { - $this->assertIsArray(SystemUtil::getSupportedDistros()); - } - - public function testFindHeaders() - { - $this->assertIsArray(SystemUtil::findHeaders(['elf.h'])); - } - - public function testFindStaticLibs() - { - $this->assertIsArray(SystemUtil::findStaticLibs(['ld-linux-x86-64.so.2'])); - } -} diff --git a/tests/SPC/builder/macos/SystemUtilTest.php b/tests/SPC/builder/macos/SystemUtilTest.php deleted file mode 100644 index 4af764a74..000000000 --- a/tests/SPC/builder/macos/SystemUtilTest.php +++ /dev/null @@ -1,31 +0,0 @@ -assertIsInt(SystemUtil::getCpuCount()); - } - - public function testGetArchCFlags() - { - $this->assertEquals('--target=x86_64-apple-darwin', SystemUtil::getArchCFlags('x86_64')); - } -} diff --git a/tests/SPC/builder/unix/UnixSystemUtilTest.php b/tests/SPC/builder/unix/UnixSystemUtilTest.php deleted file mode 100644 index e17574af6..000000000 --- a/tests/SPC/builder/unix/UnixSystemUtilTest.php +++ /dev/null @@ -1,42 +0,0 @@ - 'SPC\builder\linux\SystemUtil', - 'Darwin' => 'SPC\builder\macos\SystemUtil', - 'FreeBSD' => 'SPC\builder\freebsd\SystemUtil', - default => null, - }; - if ($util_class === null) { - self::markTestSkipped('This test is only for Unix'); - } - $this->util = new $util_class(); - } - - public function testFindCommand() - { - $this->assertIsString($this->util->findCommand('bash')); - } - - public function testMakeEnvVarString() - { - $this->assertEquals("PATH='/usr/bin' PKG_CONFIG='/usr/bin/pkg-config'", $this->util->makeEnvVarString(['PATH' => '/usr/bin', 'PKG_CONFIG' => '/usr/bin/pkg-config'])); - } -} diff --git a/tests/SPC/doctor/CheckListHandlerTest.php b/tests/SPC/doctor/CheckListHandlerTest.php deleted file mode 100644 index c6191e30d..000000000 --- a/tests/SPC/doctor/CheckListHandlerTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getValidCheckList(); - foreach ($id as $item) { - $this->assertInstanceOf('SPC\doctor\AsCheckItem', $item); - } - } -} diff --git a/tests/SPC/globals/GlobalFunctionsTest.php b/tests/SPC/globals/GlobalFunctionsTest.php deleted file mode 100644 index 818f9686d..000000000 --- a/tests/SPC/globals/GlobalFunctionsTest.php +++ /dev/null @@ -1,94 +0,0 @@ -assertTrue(is_assoc_array(['a' => 1, 'b' => 2])); - $this->assertFalse(is_assoc_array([1, 2, 3])); - } - - public function testLogger(): void - { - $this->assertInstanceOf('Psr\Log\LoggerInterface', logger()); - } - - public function testArch2Gnu(): void - { - $this->assertEquals('x86_64', arch2gnu('x86_64')); - $this->assertEquals('x86_64', arch2gnu('x64')); - $this->assertEquals('x86_64', arch2gnu('amd64')); - $this->assertEquals('aarch64', arch2gnu('arm64')); - $this->assertEquals('aarch64', arch2gnu('aarch64')); - $this->expectException('SPC\exception\WrongUsageException'); - arch2gnu('armv7'); - } - - public function testQuote(): void - { - $this->assertEquals('"hello"', quote('hello')); - $this->assertEquals("'hello'", quote('hello', "'")); - } - - public function testFPassthru(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('Windows not support f_passthru'); - } - $this->assertEquals(null, f_passthru('echo ""')); - $this->expectException(ExecutionException::class); - f_passthru('false'); - } - - public function testFPutenv(): void - { - $this->assertTrue(f_putenv('SPC_TEST_ENV=1')); - $this->assertEquals('1', getenv('SPC_TEST_ENV')); - } - - public function testShell(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('Windows not support shell'); - } - $shell = shell(); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->cd('/')); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->exec('echo ""')); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->setEnv(['SPC_TEST_ENV' => '1'])); - - [$code, $out] = $shell->execWithResult('echo "_"'); - $this->assertEquals(0, $code); - $this->assertEquals('_', implode('', $out)); - - $this->expectException('SPC\exception\ExecutionException'); - $shell->exec('false'); - } -} diff --git a/tests/SPC/store/ConfigTest.php b/tests/SPC/store/ConfigTest.php deleted file mode 100644 index 84701cd4c..000000000 --- a/tests/SPC/store/ConfigTest.php +++ /dev/null @@ -1,68 +0,0 @@ -assertTrue(is_assoc_array(Config::getExts())); - } - - public function testGetLib() - { - $this->assertIsArray(Config::getLib('zlib')); - match (PHP_OS_FAMILY) { - 'FreeBSD', 'Darwin', 'Linux' => $this->assertStringEndsWith('.a', Config::getLib('zlib', 'static-libs', [])[0]), - 'Windows' => $this->assertStringEndsWith('.lib', Config::getLib('zlib', 'static-libs', [])[0]), - default => null, - }; - } - - public function testGetExt() - { - $this->assertIsArray(Config::getExt('bcmath')); - $this->assertEquals('builtin', Config::getExt('bcmath', 'type')); - } - - public function testGetSources() - { - $this->assertTrue(is_assoc_array(Config::getSources())); - } - - public function testGetSource() - { - $this->assertIsArray(Config::getSource('php-src')); - } - - public function testGetLibs() - { - $this->assertTrue(is_assoc_array(Config::getLibs())); - } -} diff --git a/tests/SPC/store/CurlHookTest.php b/tests/SPC/store/CurlHookTest.php deleted file mode 100644 index 8b950da64..000000000 --- a/tests/SPC/store/CurlHookTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertEmpty($header); - } else { - $this->assertEquals(['Authorization: Bearer ' . getenv('GITHUB_TOKEN')], $header); - } - $header = []; - putenv('GITHUB_TOKEN=token'); - CurlHook::setupGithubToken('GET', 'https://example.com', $header); - $this->assertEquals(['Authorization: Bearer token'], $header); - } -} diff --git a/tests/SPC/store/DownloaderTest.php b/tests/SPC/store/DownloaderTest.php deleted file mode 100644 index 749fb3856..000000000 --- a/tests/SPC/store/DownloaderTest.php +++ /dev/null @@ -1,113 +0,0 @@ -assertEquals( - 'https://api.github.com/repos/AOMediaCodec/libavif/tarball/v1.1.1', - Downloader::getLatestGithubTarball('libavif', [ - 'type' => 'ghtar', - 'repo' => 'AOMediaCodec/libavif', - ])[0] - ); - } - - public function testDownloadGit() - { - Downloader::downloadGit('setup-static-php', 'https://github.com/static-php/setup-static-php.git', 'main'); - $this->assertTrue(true); - - // test keyboard interrupt - try { - Downloader::downloadGit('setup-static-php', 'https://github.com/static-php/setup-static-php.git', 'SIGINT'); - } catch (InterruptException $e) { - $this->assertStringContainsString('interrupted', $e->getMessage()); - return; - } - $this->fail('Expected exception not thrown'); - } - - public function testDownloadFile() - { - Downloader::downloadFile('fake-file', 'https://fakecmd.com/curlDown', 'curlDown.exe'); - $this->assertTrue(true); - - // test keyboard interrupt - try { - Downloader::downloadFile('fake-file', 'https://fakecmd.com/curlDown', 'SIGINT'); - } catch (InterruptException $e) { - $this->assertStringContainsString('interrupted', $e->getMessage()); - return; - } - $this->fail('Expected exception not thrown'); - } - - public function testLockSource() - { - LockFile::lockSource('fake-file', ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => 'fake-file-name', 'move_path' => 'fake-path', 'lock_as' => 'fake-lock-as']); - $this->assertFileExists(LockFile::LOCK_FILE); - $json = json_decode(file_get_contents(LockFile::LOCK_FILE), true); - $this->assertIsArray($json); - $this->assertArrayHasKey('fake-file', $json); - $this->assertArrayHasKey('source_type', $json['fake-file']); - $this->assertArrayHasKey('filename', $json['fake-file']); - $this->assertArrayHasKey('move_path', $json['fake-file']); - $this->assertArrayHasKey('lock_as', $json['fake-file']); - $this->assertEquals(SPC_SOURCE_ARCHIVE, $json['fake-file']['source_type']); - $this->assertEquals('fake-file-name', $json['fake-file']['filename']); - $this->assertEquals('fake-path', $json['fake-file']['move_path']); - $this->assertEquals('fake-lock-as', $json['fake-file']['lock_as']); - } - - public function testGetLatestBitbucketTag() - { - $this->assertEquals( - 'abc.tar.gz', - Downloader::getLatestBitbucketTag('abc', [ - 'repo' => 'MATCHED/def', - ])[1] - ); - $this->assertEquals( - 'abc-1.0.0.tar.gz', - Downloader::getLatestBitbucketTag('abc', [ - 'repo' => 'abc/def', - ])[1] - ); - } - - public function testGetLatestGithubRelease() - { - $this->assertEquals( - 'ghreltest.tar.gz', - Downloader::getLatestGithubRelease('ghrel', [ - 'type' => 'ghrel', - 'repo' => 'ghreltest/ghrel', - 'match' => 'ghreltest.tar.gz', - ])[1] - ); - } - - public function testGetFromFileList() - { - $filelist = Downloader::getFromFileList('fake-filelist', [ - 'url' => 'https://fakecmd.com/filelist', - 'regex' => '/href="(?filelist-(?[^"]+)\.tar\.xz)"/', - ]); - $this->assertIsArray($filelist); - $this->assertEquals('filelist-4.7.0.tar.xz', $filelist[1]); - } -} diff --git a/tests/SPC/store/FileSystemTest.php b/tests/SPC/store/FileSystemTest.php deleted file mode 100644 index 13d2893f3..000000000 --- a/tests/SPC/store/FileSystemTest.php +++ /dev/null @@ -1,176 +0,0 @@ -assertEquals('he11o', file_get_contents($file)); - - unlink($file); - } - - public function testFindCommandPath() - { - $this->assertNull(FileSystem::findCommandPath('randomtestxxxxx')); - if (PHP_OS_FAMILY === 'Windows') { - $this->assertIsString(FileSystem::findCommandPath('explorer')); - } elseif (in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'FreeBSD'])) { - $this->assertIsString(FileSystem::findCommandPath('uname')); - } - } - - public function testReadFile() - { - $file = WORKING_DIR . '/.testread'; - file_put_contents($file, 'haha'); - $content = FileSystem::readFile($file); - $this->assertEquals('haha', $content); - @unlink($file); - } - - public function testReplaceFileUser() - { - $file = WORKING_DIR . '/.txt1'; - file_put_contents($file, 'hello'); - - FileSystem::replaceFileUser($file, function ($file) { - return str_replace('el', '55', $file); - }); - $this->assertEquals('h55lo', file_get_contents($file)); - - unlink($file); - } - - public function testExtname() - { - $this->assertEquals('exe', FileSystem::extname('/tmp/asd.exe')); - $this->assertEquals('', FileSystem::extname('/tmp/asd.')); - } - - public function testGetClassesPsr4() - { - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\builder\extension'); - foreach ($classes as $class) { - $this->assertIsString($class); - new \ReflectionClass($class); - } - } - - public function testConvertPath() - { - $this->assertEquals('phar://C:/pharfile.phar', FileSystem::convertPath('phar://C:/pharfile.phar')); - if (DIRECTORY_SEPARATOR === '\\') { - $this->assertEquals('C:\Windows\win.ini', FileSystem::convertPath('C:\Windows/win.ini')); - } - } - - public function testCreateDir() - { - FileSystem::createDir(WORKING_DIR . '/.testdir'); - $this->assertDirectoryExists(WORKING_DIR . '/.testdir'); - rmdir(WORKING_DIR . '/.testdir'); - } - - public function testReplaceFileStr() - { - $file = WORKING_DIR . '/.txt1'; - file_put_contents($file, 'hello'); - - FileSystem::replaceFileStr($file, 'el', '55'); - $this->assertEquals('h55lo', file_get_contents($file)); - - unlink($file); - } - - public function testResetDir() - { - // prepare fake git dir to test - FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); - FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }'); - FileSystem::resetDir(WORKING_DIR . '/.fake_down_test'); - $this->assertFileDoesNotExist(WORKING_DIR . '/.fake_down_test/a.c'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); - } - - public function testCopyDir() - { - // prepare fake git dir to test - FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); - FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }'); - FileSystem::copyDir(WORKING_DIR . '/.fake_down_test', WORKING_DIR . '/.fake_down_test2'); - $this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test2'); - $this->assertFileExists(WORKING_DIR . '/.fake_down_test2/a.c'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test2'); - } - - public function testRemoveDir() - { - FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); - $this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); - $this->assertDirectoryDoesNotExist(WORKING_DIR . '/.fake_down_test'); - } - - public function testLoadConfigArray() - { - $arr = FileSystem::loadConfigArray('lib'); - $this->assertArrayHasKey('zlib', $arr); - } - - public function testIsRelativePath() - { - $this->assertTrue(FileSystem::isRelativePath('.')); - $this->assertTrue(FileSystem::isRelativePath('.\sdf')); - if (DIRECTORY_SEPARATOR === '\\') { - $this->assertFalse(FileSystem::isRelativePath('C:\asdasd/fwe\asd')); - } else { - $this->assertFalse(FileSystem::isRelativePath('/fwefwefewf')); - } - } - - public function testScanDirFiles() - { - $this->assertFalse(FileSystem::scanDirFiles('wfwefewfewf')); - $files = FileSystem::scanDirFiles(ROOT_DIR . '/config', true, true); - $this->assertContains('lib.json', $files); - } - - public function testWriteFile() - { - FileSystem::writeFile(WORKING_DIR . '/.txt', 'txt'); - $this->assertFileExists(WORKING_DIR . '/.txt'); - $this->assertEquals('txt', FileSystem::readFile(WORKING_DIR . '/.txt')); - unlink(WORKING_DIR . '/.txt'); - } -} diff --git a/tests/SPC/util/ConfigValidatorTest.php b/tests/SPC/util/ConfigValidatorTest.php deleted file mode 100644 index aba611a42..000000000 --- a/tests/SPC/util/ConfigValidatorTest.php +++ /dev/null @@ -1,767 +0,0 @@ - [ - 'type' => 'filelist', - 'url' => 'https://example.com', - 'regex' => '.*', - ], - 'source2' => [ - 'type' => 'git', - 'url' => 'https://example.com', - 'rev' => 'master', - ], - 'source3' => [ - 'type' => 'ghtagtar', - 'repo' => 'aaaa/bbbb', - ], - 'source4' => [ - 'type' => 'ghtar', - 'repo' => 'aaa/bbb', - 'path' => 'path/to/dir', - ], - 'source5' => [ - 'type' => 'ghrel', - 'repo' => 'aaa/bbb', - 'match' => '.*', - ], - 'source6' => [ - 'type' => 'url', - 'url' => 'https://example.com', - ], - 'source7' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'filename' => 'test.tar.gz', - 'path' => 'test/path', - 'provide-pre-built' => true, - 'license' => [ - 'type' => 'file', - 'path' => 'LICENSE', - ], - ], - 'source8' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'alt' => [ - 'type' => 'url', - 'url' => 'https://alt.example.com', - ], - ], - 'source9' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'alt' => false, - 'license' => [ - 'type' => 'text', - 'text' => 'MIT License', - ], - ], - ]; - try { - ConfigValidator::validateSource($good_source); - $this->assertTrue(true); - } catch (ValidationException $e) { - $this->fail($e->getMessage()); - } - } - - public function testValidateSourceBad(): void - { - $bad_source = [ - 'source1' => [ - 'type' => 'filelist', - 'url' => 'https://example.com', - // no regex - ], - 'source2' => [ - 'type' => 'git', - 'url' => true, // not string - 'rev' => 'master', - ], - 'source3' => [ - 'type' => 'ghtagtar', - 'url' => 'aaaa/bbbb', // not repo - ], - 'source4' => [ - 'type' => 'ghtar', - 'repo' => 'aaa/bbb', - 'path' => true, // not string - ], - 'source5' => [ - 'type' => 'ghrel', - 'repo' => 'aaa/bbb', - 'match' => 1, // not string - ], - 'source6' => [ - 'type' => 'url', // no url - ], - 'source7' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'provide-pre-built' => 'not boolean', // not boolean - ], - 'source8' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'prefer-stable' => 'not boolean', // not boolean - ], - 'source9' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => 'not object', // not object - ], - 'source10' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => [ - 'type' => 'invalid', // invalid type - ], - ], - 'source11' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => [ - 'type' => 'file', // missing path - ], - ], - 'source12' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => [ - 'type' => 'text', // missing text - ], - ], - 'source13' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'alt' => 'not object or boolean', // not object or boolean - ], - ]; - foreach ($bad_source as $name => $src) { - try { - ConfigValidator::validateSource([$name => $src]); - $this->fail("should throw ValidationException for source {$name}"); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - } - - public function testValidateLibsGood(): void - { - $good_libs = [ - 'lib1' => [ - 'source' => 'source1', - ], - 'lib2' => [ - 'source' => 'source2', - 'lib-depends' => [ - 'lib1', - ], - ], - 'lib3' => [ - 'source' => 'source3', - 'lib-suggests' => [ - 'lib1', - ], - ], - 'lib4' => [ - 'source' => 'source4', - 'headers' => [ - 'header1.h', - 'header2.h', - ], - 'headers-windows' => [ - 'windows_header.h', - ], - 'bin-unix' => [ - 'binary1', - 'binary2', - ], - 'frameworks' => [ - 'CoreFoundation', - 'SystemConfiguration', - ], - ], - 'lib5' => [ - 'type' => 'package', - 'source' => 'source5', - 'pkg-configs' => [ - 'pkg1', - 'pkg2', - ], - ], - 'lib6' => [ - 'type' => 'root', - ], - ]; - try { - ConfigValidator::validateLibs($good_libs, ['source1' => [], 'source2' => [], 'source3' => [], 'source4' => [], 'source5' => []]); - $this->assertTrue(true); - } catch (ValidationException $e) { - $this->fail($e->getMessage()); - } - } - - public function testValidateLibsBad(): void - { - // lib.json is broken - try { - ConfigValidator::validateLibs('not array'); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib source not exists - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source3']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json is broken by not assoc array - try { - ConfigValidator::validateLibs(['lib1', 'lib2'], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json lib is not one of "lib", "package", "root", "target" - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'type' => 'not one of']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json lib if it is "lib" or "package", it must have "source" - try { - ConfigValidator::validateLibs(['lib1' => ['type' => 'lib']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json static-libs must be a list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'static-libs-windows' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json frameworks must be a list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'frameworks' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // source must be string - try { - ConfigValidator::validateLibs(['lib1' => ['source' => true]], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib-depends must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-depends' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib-suggests must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-suggests' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // headers must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'headers' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // bin must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'bin-unix' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - - public function testValidateExts(): void - { - // Test valid extensions - $valid_exts = [ - 'ext1' => [ - 'type' => 'builtin', - ], - 'ext2' => [ - 'type' => 'external', - 'source' => 'source1', - ], - 'ext3' => [ - 'type' => 'external', - 'source' => 'source2', - 'arg-type' => 'enable', - 'lib-depends' => ['lib1'], - 'lib-suggests' => ['lib2'], - 'ext-depends-windows' => ['ext1'], - 'support' => [ - 'Windows' => 'wip', - 'BSD' => 'wip', - ], - 'notes' => true, - ], - 'ext4' => [ - 'type' => 'external', - 'source' => 'source3', - 'arg-type-unix' => 'with-path', - 'arg-type-windows' => 'with', - ], - ]; - ConfigValidator::validateExts($valid_exts); - - // Test invalid data - $this->expectException(ValidationException::class); - ConfigValidator::validateExts(null); - } - - public function testValidateExtsBad(): void - { - // Test invalid extension type - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test external extension without source - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'external']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test non-object extension - try { - ConfigValidator::validateExts(['ext1' => 'not object']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid source type - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'external', 'source' => true]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid support - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'support' => 'not object']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid notes - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'notes' => 'not boolean']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid lib-depends - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'lib-depends' => 'not list']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid arg-type - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'arg-type' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid arg-type with suffix - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'arg-type-unix' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - - public function testValidatePkgs(): void - { - // Test valid packages (all supported types) - $valid_pkgs = [ - 'pkg1' => [ - 'type' => 'url', - 'url' => 'https://example.com/file.tar.gz', - ], - 'pkg2' => [ - 'type' => 'ghrel', - 'repo' => 'owner/repo', - 'match' => 'file.+\.tar\.gz', - ], - 'pkg3' => [ - 'type' => 'custom', - ], - 'pkg4' => [ - 'type' => 'url', - 'url' => 'https://example.com/archive.zip', - 'filename' => 'archive.zip', - 'path' => 'extract/path', - 'extract-files' => [ - 'source/file.exe' => '{pkg_root_path}/bin/file.exe', - 'source/lib.dll' => '{pkg_root_path}/lib/lib.dll', - ], - ], - 'pkg5' => [ - 'type' => 'ghrel', - 'repo' => 'owner/repo', - 'match' => 'release.+\.zip', - 'extract-files' => [ - 'binary' => '{pkg_root_path}/bin/binary', - ], - ], - 'pkg6' => [ - 'type' => 'filelist', - 'url' => 'https://example.com/filelist', - 'regex' => '/href="(?.*\.tar\.gz)"/', - ], - 'pkg7' => [ - 'type' => 'git', - 'url' => 'https://github.com/owner/repo.git', - 'rev' => 'main', - ], - 'pkg8' => [ - 'type' => 'git', - 'url' => 'https://github.com/owner/repo.git', - 'rev' => 'v1.0.0', - 'path' => 'subdir/path', - ], - 'pkg9' => [ - 'type' => 'ghtagtar', - 'repo' => 'owner/repo', - ], - 'pkg10' => [ - 'type' => 'ghtar', - 'repo' => 'owner/repo', - 'path' => 'subdir', - ], - ]; - ConfigValidator::validatePkgs($valid_pkgs); - - // Test invalid data - $this->expectException(ValidationException::class); - ConfigValidator::validatePkgs(null); - } - - public function testValidatePkgsBad(): void - { - // Test invalid package type - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test non-object package - try { - ConfigValidator::validatePkgs(['pkg1' => 'not object']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test filelist type without url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'filelist', 'regex' => '.*']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test filelist type without regex - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'filelist', 'url' => 'https://example.com']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test git type without url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'rev' => 'main']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test git type without rev - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'url' => 'https://github.com/owner/repo.git']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghtagtar type without repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghtagtar']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghtar type without repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghtar']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test url type without url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test url type with non-string url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => true]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type without repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'match' => 'pattern']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type without match - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => 'owner/repo']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type with non-string repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => true, 'match' => 'pattern']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type with non-string match - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => 'owner/repo', 'match' => 123]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test git type with non-string path - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'url' => 'https://github.com/owner/repo.git', 'rev' => 'main', 'path' => 123]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test url type with non-string filename - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'filename' => 123]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid extract-files (not object) - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => 'not object']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid extract-files mapping (non-string key) - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => [123 => 'target']]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid extract-files mapping (non-string value) - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => ['source' => 123]]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - - public function testValidatePreBuilt(): void - { - // Test valid pre-built configurations - $valid_prebuilt = [ - 'basic' => [ - 'repo' => 'static-php/static-php-cli-hosted', - 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', - ], - 'full' => [ - 'repo' => 'static-php/static-php-cli-hosted', - 'prefer-stable' => true, - 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', - 'match-pattern-macos' => '{name}-{arch}-{os}.txz', - 'match-pattern-windows' => '{name}-{arch}-{os}.tgz', - ], - 'prefer-stable-false' => [ - 'repo' => 'owner/repo', - 'prefer-stable' => false, - 'match-pattern-macos' => '{name}-{arch}-{os}.tar.gz', - ], - ]; - - foreach ($valid_prebuilt as $name => $config) { - try { - ConfigValidator::validatePreBuilt($config); - $this->assertTrue(true, "Config {$name} should be valid"); - } catch (ValidationException $e) { - $this->fail("Config {$name} should be valid but got: " . $e->getMessage()); - } - } - } - - public function testValidatePreBuiltBad(): void - { - // Test non-array data - try { - ConfigValidator::validatePreBuilt('invalid'); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing repo - try { - ConfigValidator::validatePreBuilt(['match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid repo type - try { - ConfigValidator::validatePreBuilt(['repo' => 123, 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid prefer-stable type - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'prefer-stable' => 'true', 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test no match patterns - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid match pattern type - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => 123]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing {name} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing {arch} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing {os} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test linux pattern missing {libc} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{os}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test linux pattern missing {libcver} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } -} diff --git a/tests/SPC/util/DependencyUtilTest.php b/tests/SPC/util/DependencyUtilTest.php deleted file mode 100644 index 468f6eb2c..000000000 --- a/tests/SPC/util/DependencyUtilTest.php +++ /dev/null @@ -1,113 +0,0 @@ -originalConfig = [ - 'source' => Config::$source, - 'lib' => Config::$lib, - 'ext' => Config::$ext, - ]; - } - - protected function tearDown(): void - { - // Restore original configuration - Config::$source = $this->originalConfig['source']; - Config::$lib = $this->originalConfig['lib']; - Config::$ext = $this->originalConfig['ext']; - } - - public function testGetExtLibsByDeps(): void - { - // Set up test data - Config::$source = [ - 'test1' => [ - 'type' => 'url', - 'url' => 'https://pecl.php.net/get/APCu', - 'filename' => 'apcu.tgz', - 'license' => [ - 'type' => 'file', - 'path' => 'LICENSE', - ], - ], - ]; - Config::$lib = [ - 'lib-base' => ['type' => 'root'], - 'php' => ['type' => 'root'], - 'libaaa' => [ - 'source' => 'test1', - 'static-libs' => ['libaaa.a'], - 'lib-depends' => ['libbbb', 'libccc'], - 'lib-suggests' => ['libeee'], - ], - 'libbbb' => [ - 'source' => 'test1', - 'static-libs' => ['libbbb.a'], - 'lib-suggests' => ['libccc'], - ], - 'libccc' => [ - 'source' => 'test1', - 'static-libs' => ['libccc.a'], - ], - 'libeee' => [ - 'source' => 'test1', - 'static-libs' => ['libeee.a'], - 'lib-suggests' => ['libfff'], - ], - 'libfff' => [ - 'source' => 'test1', - 'static-libs' => ['libfff.a'], - ], - ]; - Config::$ext = [ - 'ext-a' => [ - 'type' => 'builtin', - 'lib-depends' => ['libaaa'], - 'ext-suggests' => ['ext-b'], - ], - 'ext-b' => [ - 'type' => 'builtin', - 'lib-depends' => ['libeee'], - ], - ]; - - // Test dependency resolution - [$exts, $libs, $not_included] = DependencyUtil::getExtsAndLibs(['ext-a'], include_suggested_exts: true); - $this->assertContains('libbbb', $libs); - $this->assertContains('libccc', $libs); - $this->assertContains('ext-b', $exts); - $this->assertContains('ext-b', $not_included); - - // Test dependency order - $this->assertIsInt($b = array_search('libbbb', $libs)); - $this->assertIsInt($c = array_search('libccc', $libs)); - $this->assertIsInt($a = array_search('libaaa', $libs)); - // libbbb, libaaa - $this->assertTrue($b < $a); - $this->assertTrue($c < $a); - $this->assertTrue($c < $b); - } - - public function testNotExistExtException(): void - { - $this->expectException(WrongUsageException::class); - DependencyUtil::getExtsAndLibs(['sdsd']); - } -} diff --git a/tests/SPC/util/GlobalEnvManagerTest.php b/tests/SPC/util/GlobalEnvManagerTest.php deleted file mode 100644 index 1d9ed1083..000000000 --- a/tests/SPC/util/GlobalEnvManagerTest.php +++ /dev/null @@ -1,143 +0,0 @@ -originalEnv = [ - 'BUILD_ROOT_PATH' => getenv('BUILD_ROOT_PATH'), - 'SPC_TARGET' => getenv('SPC_TARGET'), - 'SPC_LIBC' => getenv('SPC_LIBC'), - ]; - // Temporarily set private GlobalEnvManager::$initialized to false (use reflection) - $reflection = new \ReflectionClass(GlobalEnvManager::class); - $property = $reflection->getProperty('initialized'); - $property->setValue(null, false); - } - - protected function tearDown(): void - { - // Restore original environment variables - foreach ($this->originalEnv as $key => $value) { - if ($value === false) { - putenv($key); - } else { - putenv("{$key}={$value}"); - } - } - // Temporarily set private GlobalEnvManager::$initialized to false (use reflection) - $reflection = new \ReflectionClass(GlobalEnvManager::class); - $property = $reflection->getProperty('initialized'); - $property->setValue(null, true); - } - - public function testGetInitializedEnv(): void - { - // Test that getInitializedEnv returns an array - $result = GlobalEnvManager::getInitializedEnv(); - $this->assertIsArray($result); - } - - /** - * @dataProvider envVariableProvider - */ - public function testPutenv(string $envVar): void - { - // Test putenv functionality - GlobalEnvManager::putenv($envVar); - - $env = GlobalEnvManager::getInitializedEnv(); - $this->assertContains($envVar, $env); - $this->assertEquals(explode('=', $envVar, 2)[1], getenv(explode('=', $envVar, 2)[0])); - } - - /** - * @dataProvider pathProvider - */ - public function testAddPathIfNotExistsOnUnix(string $path): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $originalPath = getenv('PATH'); - GlobalEnvManager::addPathIfNotExists($path); - - $newPath = getenv('PATH'); - $this->assertStringContainsString($path, $newPath); - } - - /** - * @dataProvider pathProvider - */ - public function testAddPathIfNotExistsWhenPathAlreadyExists(string $path): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - GlobalEnvManager::addPathIfNotExists($path); - $pathAfterFirstAdd = getenv('PATH'); - - GlobalEnvManager::addPathIfNotExists($path); - $pathAfterSecondAdd = getenv('PATH'); - - // Should not add the same path twice - $this->assertEquals($pathAfterFirstAdd, $pathAfterSecondAdd); - } - - public function testInitWithoutBuildRootPath(): void - { - // Temporarily unset BUILD_ROOT_PATH - putenv('BUILD_ROOT_PATH'); - - $this->expectException(SPCInternalException::class); - GlobalEnvManager::init(); - } - - public function testAfterInit(): void - { - // Set required environment variable - putenv('BUILD_ROOT_PATH=/test/path'); - putenv('SPC_SKIP_TOOLCHAIN_CHECK=true'); - - // Should not throw exception when SPC_SKIP_TOOLCHAIN_CHECK is true - GlobalEnvManager::afterInit(); - - $this->assertTrue(true); // Test passes if no exception is thrown - } - - public function envVariableProvider(): array - { - return [ - 'simple-env' => ['TEST_VAR=test_value'], - 'complex-env' => ['COMPLEX_VAR=complex_value_with_spaces'], - 'numeric-env' => ['NUMERIC_VAR=123'], - 'special-chars-env' => ['SPECIAL_VAR=test@#$%'], - ]; - } - - public function pathProvider(): array - { - return [ - 'simple-path' => ['/test/path'], - 'complex-path' => ['/usr/local/bin'], - 'home-path' => ['/home/user/bin'], - 'root-path' => ['/root/bin'], - ]; - } -} diff --git a/tests/SPC/util/LicenseDumperTest.php b/tests/SPC/util/LicenseDumperTest.php deleted file mode 100644 index ed914296c..000000000 --- a/tests/SPC/util/LicenseDumperTest.php +++ /dev/null @@ -1,110 +0,0 @@ - Config::$source, - 'lib' => Config::$lib, - ]; - Config::$lib = [ - 'lib-base' => ['type' => 'root'], - 'php' => ['type' => 'root'], - 'fake_lib' => [ - 'source' => 'fake_lib', - ], - ]; - Config::$source = [ - 'fake_lib' => [ - 'license' => [ - 'type' => 'text', - 'text' => 'license', - ], - ], - ]; - - $dumper = new LicenseDumper(); - $dumper->addLibs(['fake_lib']); - $dumper->dump(self::DIRECTORY); - - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_0.txt'); - // restore - Config::$source = $bak['source']; - Config::$lib = $bak['lib']; - } - - public function testDumpWithMultipleLicenses(): void - { - $bak = [ - 'source' => Config::$source, - 'lib' => Config::$lib, - ]; - Config::$lib = [ - 'lib-base' => ['type' => 'root'], - 'php' => ['type' => 'root'], - 'fake_lib' => [ - 'source' => 'fake_lib', - ], - ]; - Config::$source = [ - 'fake_lib' => [ - 'license' => [ - [ - 'type' => 'text', - 'text' => 'license', - ], - [ - 'type' => 'text', - 'text' => 'license', - ], - [ - 'type' => 'text', - 'text' => 'license', - ], - ], - ], - ]; - - $dumper = new LicenseDumper(); - $dumper->addLibs(['fake_lib']); - $dumper->dump(self::DIRECTORY); - - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_0.txt'); - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_1.txt'); - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_2.txt'); - - // restore - Config::$source = $bak['source']; - Config::$lib = $bak['lib']; - } -} diff --git a/tests/SPC/util/PkgConfigUtilTest.php b/tests/SPC/util/PkgConfigUtilTest.php deleted file mode 100644 index 41de6d70e..000000000 --- a/tests/SPC/util/PkgConfigUtilTest.php +++ /dev/null @@ -1,210 +0,0 @@ -assertEquals($expectedCflags, $result); - } - - /** - * @dataProvider validPackageProvider - */ - public function testGetLibsArrayWithValidPackage(string $package, string $expectedCflags, array $expectedLibs): void - { - $result = PkgConfigUtil::getLibsArray($package); - $this->assertEquals($expectedLibs, $result); - } - - /** - * @dataProvider invalidPackageProvider - */ - public function testGetCflagsWithInvalidPackage(string $package): void - { - $this->expectException(ExecutionException::class); - PkgConfigUtil::getCflags($package); - } - - /** - * @dataProvider invalidPackageProvider - */ - public function testGetLibsArrayWithInvalidPackage(string $package): void - { - $this->expectException(ExecutionException::class); - PkgConfigUtil::getLibsArray($package); - } - - public static function invalidPackageProvider(): array - { - return [ - 'invalid-package' => ['invalid-package'], - 'empty-string' => [''], - 'non-existent-package' => ['non-existent-package'], - ]; - } - - public static function validPackageProvider(): array - { - return [ - 'libxml2' => ['libxml-2.0', '-I/usr/include/libxml2', ['-lxml2', '']], - 'zlib' => ['zlib', '-I/usr/include', ['-lz', '']], - 'openssl' => ['openssl', '-I/usr/include/openssl', ['-lssl', '-lcrypto', '']], - ]; - } - - /** - * Create a fake pkg-config executable - */ - private static function createFakePkgConfig(): void - { - $pkgConfigScript = self::$fakePkgConfigPath . '/pkg-config'; - - $script = <<<'SCRIPT' -#!/bin/bash - -# Fake pkg-config script for testing -# Shift arguments to get the package name -shift - -case "$1" in - --cflags-only-other) - shift - case "$1" in - libxml-2.0) - echo "-I/usr/include/libxml2" - ;; - zlib) - echo "-I/usr/include" - ;; - openssl) - echo "-I/usr/include/openssl" - ;; - *) - echo "Package '$1' was not found in the pkg-config search path." >&2 - exit 1 - ;; - esac - ;; - --libs-only-l) - shift - case "$1" in - libxml-2.0) - echo "-lxml2" - ;; - zlib) - echo "-lz" - ;; - openssl) - echo "-lssl -lcrypto" - ;; - *) - echo "Package '$1' was not found in the pkg-config search path." >&2 - exit 1 - ;; - esac - ;; - --libs-only-other) - shift - case "$1" in - libxml-2.0) - echo "" - ;; - zlib) - echo "" - ;; - openssl) - echo "" - ;; - *) - echo "Package '$1' was not found in the pkg-config search path." >&2 - exit 1 - ;; - esac - ;; - *) - echo "Usage: pkg-config [OPTION] [PACKAGE]" >&2 - echo "Try 'pkg-config --help' for more information." >&2 - exit 1 - ;; -esac -SCRIPT; - - file_put_contents($pkgConfigScript, $script); - chmod($pkgConfigScript, 0755); - } - - /** - * Remove directory recursively - */ - private static function removeDirectory(string $dir): void - { - if (!is_dir($dir)) { - return; - } - - $files = array_diff(scandir($dir), ['.', '..']); - foreach ($files as $file) { - $path = $dir . '/' . $file; - if (is_dir($path)) { - self::removeDirectory($path); - } else { - unlink($path); - } - } - rmdir($dir); - } -} diff --git a/tests/SPC/util/SPCConfigUtilTest.php b/tests/SPC/util/SPCConfigUtilTest.php deleted file mode 100644 index c4b1427a2..000000000 --- a/tests/SPC/util/SPCConfigUtilTest.php +++ /dev/null @@ -1,74 +0,0 @@ -assertInstanceOf(SPCConfigUtil::class, new SPCConfigUtil()); - $this->assertInstanceOf(SPCConfigUtil::class, new SPCConfigUtil(BuilderProvider::makeBuilderByInput(new ArgvInput()))); - } - - public function testConfig(): void - { - if (PHP_OS_FAMILY !== 'Linux') { - $this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.'); - } - // normal - $result = (new SPCConfigUtil())->config(['bcmath']); - $this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']); - $this->assertStringContainsString(BUILD_ROOT_PATH . '/lib', $result['ldflags']); - $this->assertStringContainsString('-lphp', $result['libs']); - - // has cpp - $result = (new SPCConfigUtil())->config(['rar']); - $this->assertStringContainsString(PHP_OS_FAMILY === 'Darwin' ? '-lc++' : '-lstdc++', $result['libs']); - - // has libmimalloc.a in lib dir - // backup first - if (file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $bak = file_get_contents(BUILD_LIB_PATH . '/libmimalloc.a'); - @unlink(BUILD_LIB_PATH . '/libmimalloc.a'); - } - file_put_contents(BUILD_LIB_PATH . '/libmimalloc.a', ''); - $result = (new SPCConfigUtil())->config(['bcmath'], ['mimalloc']); - $this->assertStringStartsWith(BUILD_LIB_PATH . '/libmimalloc.a', $result['libs']); - @unlink(BUILD_LIB_PATH . '/libmimalloc.a'); - if (isset($bak)) { - file_put_contents(BUILD_LIB_PATH . '/libmimalloc.a', $bak); - } - } -} diff --git a/tests/SPC/util/SPCTargetTest.php b/tests/SPC/util/SPCTargetTest.php deleted file mode 100644 index 665b4045b..000000000 --- a/tests/SPC/util/SPCTargetTest.php +++ /dev/null @@ -1,106 +0,0 @@ -originalEnv = [ - 'SPC_TARGET' => getenv('SPC_TARGET'), - 'SPC_LIBC' => getenv('SPC_LIBC'), - ]; - } - - protected function tearDown(): void - { - // Restore original environment variables - foreach ($this->originalEnv as $key => $value) { - if ($value === false) { - putenv($key); - } else { - putenv("{$key}={$value}"); - } - } - } - - /** - * @dataProvider libcProvider - */ - public function testGetLibc(string $libc, bool $expected): void - { - putenv("SPC_LIBC={$libc}"); - - $result = SPCTarget::getLibc(); - if ($libc === '') { - // When SPC_LIBC is set to empty string, getenv returns empty string, not false - $this->assertEquals('', $result); - } else { - $this->assertEquals($libc, $result); - } - } - - /** - * @dataProvider libcProvider - */ - public function testGetLibcVersion(string $libc): void - { - putenv("SPC_LIBC={$libc}"); - - $result = SPCTarget::getLibcVersion(); - // The actual result depends on the system, but it could be null if libc is not available - $this->assertIsStringOrNull($result); - } - - /** - * @dataProvider targetOSProvider - */ - public function testGetTargetOS(string $target, string $expected): void - { - putenv("SPC_TARGET={$target}"); - - $result = SPCTarget::getTargetOS(); - $this->assertEquals($expected, $result); - } - - public function testLibcListConstant(): void - { - $this->assertIsArray(SPCTarget::LIBC_LIST); - $this->assertContains('musl', SPCTarget::LIBC_LIST); - $this->assertContains('glibc', SPCTarget::LIBC_LIST); - } - - public function libcProvider(): array - { - return [ - 'musl' => ['musl', true], - 'glibc' => ['glibc', false], - 'empty' => ['', false], - ]; - } - - public function targetOSProvider(): array - { - return [ - 'linux-target' => ['native-linux', 'Linux'], - 'macos-target' => ['native-macos', 'Darwin'], - 'windows-target' => ['native-windows', 'Windows'], - 'empty-target' => ['', PHP_OS_FAMILY], - ]; - } - - private function assertIsStringOrNull($value): void - { - $this->assertTrue(is_string($value) || is_null($value), 'Value must be string or null'); - } -} diff --git a/tests/SPC/util/TestBase.php b/tests/SPC/util/TestBase.php deleted file mode 100644 index fd82ccfbf..000000000 --- a/tests/SPC/util/TestBase.php +++ /dev/null @@ -1,100 +0,0 @@ -suppressOutput(); - } - - protected function tearDown(): void - { - $this->restoreOutput(); - parent::tearDown(); - } - - /** - * Suppress output during tests - */ - protected function suppressOutput(): void - { - // Start output buffering to capture PHP output - $this->outputBuffer = ob_start(); - } - - /** - * Restore output after tests - */ - protected function restoreOutput(): void - { - // Clean output buffer - if ($this->outputBuffer) { - ob_end_clean(); - } - } - - /** - * Create a UnixShell instance with debug disabled to suppress logs - */ - protected function createUnixShell(): \SPC\util\shell\UnixShell - { - return new \SPC\util\shell\UnixShell(false); - } - - /** - * Create a WindowsCmd instance with debug disabled to suppress logs - */ - protected function createWindowsCmd(): \SPC\util\shell\WindowsCmd - { - return new \SPC\util\shell\WindowsCmd(false); - } - - /** - * Run a test with output suppression - */ - protected function runWithOutputSuppression(callable $callback) - { - $this->suppressOutput(); - try { - return $callback(); - } finally { - $this->restoreOutput(); - } - } - - /** - * Execute a command with output suppression - */ - protected function execWithSuppression(string $command): array - { - $this->suppressOutput(); - try { - exec($command, $output, $returnCode); - return [$returnCode, $output]; - } finally { - $this->restoreOutput(); - } - } - - /** - * Execute a command with output redirected to /dev/null - */ - protected function execSilently(string $command): array - { - $command .= ' 2>/dev/null 1>/dev/null'; - exec($command, $output, $returnCode); - return [$returnCode, $output]; - } -} diff --git a/tests/SPC/util/UnixShellTest.php b/tests/SPC/util/UnixShellTest.php deleted file mode 100644 index f65e5a406..000000000 --- a/tests/SPC/util/UnixShellTest.php +++ /dev/null @@ -1,184 +0,0 @@ -markTestSkipped('This test is for Windows systems only'); - } - - $this->expectException(EnvironmentException::class); - $this->expectExceptionMessage('Windows cannot use UnixShell'); - - new UnixShell(); - } - - /** - * @dataProvider envProvider - */ - public function testSetEnv(array $env): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $result = $shell->setEnv($env); - - $this->assertSame($shell, $result); - foreach ($env as $item) { - if (trim($item) !== '') { - $this->assertStringContainsString($item, $shell->getEnvString()); - } - } - } - - /** - * @dataProvider envProvider - */ - public function testAppendEnv(array $env): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $shell->setEnv(['CFLAGS' => '-O2']); - - $shell->appendEnv($env); - - $this->assertStringContainsString('-O2', $shell->getEnvString()); - foreach ($env as $value) { - if (trim($value) !== '') { - $this->assertStringContainsString($value, $shell->getEnvString()); - } - } - } - - /** - * @dataProvider envProvider - */ - public function testGetEnvString(array $env): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $shell->setEnv($env); - - $envString = $shell->getEnvString(); - - $hasNonEmptyValues = false; - foreach ($env as $key => $value) { - if (trim($value) !== '') { - $this->assertStringContainsString("{$key}=\"{$value}\"", $envString); - $hasNonEmptyValues = true; - } - } - - // If all values are empty, ensure we still have a test assertion - if (!$hasNonEmptyValues) { - $this->assertIsString($envString); - } - } - - public function testGetEnvStringWithEmptyEnv(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $envString = $shell->getEnvString(); - - $this->assertEquals('', trim($envString)); - } - - /** - * @dataProvider commandProvider - */ - public function testExecWithResult(string $command): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - [$code, $output] = $shell->execWithResult($command); - - $this->assertIsInt($code); - $this->assertIsArray($output); - } - - public function testExecWithResultWithLog(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - [$code, $output] = $shell->execWithResult('echo "test"', false); - - $this->assertIsInt($code); - $this->assertIsArray($output); - $this->assertEquals(0, $code); - $this->assertEquals(['test'], $output); - } - - public function testExecWithResultWithCd(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $shell->cd('/tmp'); - - [$code, $output] = $shell->execWithResult('pwd'); - - $this->assertIsInt($code); - $this->assertEquals(0, $code); - $this->assertIsArray($output); - } - - public static function directoryProvider(): array - { - return [ - 'simple-directory' => ['/test/directory'], - 'home-directory' => ['/home/user'], - 'root-directory' => ['/root'], - 'tmp-directory' => ['/tmp'], - ]; - } - - public static function envProvider(): array - { - return [ - 'simple-env' => [['CFLAGS' => '-O2', 'LDFLAGS' => '-L/usr/lib']], - 'complex-env' => [['CXXFLAGS' => '-std=c++11', 'LIBS' => '-lz -lxml']], - 'empty-env' => [['CFLAGS' => '', 'LDFLAGS' => ' ']], - 'mixed-env' => [['CFLAGS' => '-O2', 'EMPTY_VAR' => '']], - ]; - } - - public static function commandProvider(): array - { - return [ - 'echo-command' => ['echo "test"'], - 'pwd-command' => ['pwd'], - 'ls-command' => ['ls -la'], - ]; - } -} diff --git a/tests/SPC/util/WindowsCmdTest.php b/tests/SPC/util/WindowsCmdTest.php deleted file mode 100644 index fb4ee3f2e..000000000 --- a/tests/SPC/util/WindowsCmdTest.php +++ /dev/null @@ -1,68 +0,0 @@ -markTestSkipped('This test is for Unix systems only'); - } - - $this->expectException(SPCInternalException::class); - $this->expectExceptionMessage('Only windows can use WindowsCmd'); - - new WindowsCmd(); - } - - /** - * @dataProvider commandProvider - */ - public function testExecWithResult(string $command): void - { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('This test is for Windows systems only'); - } - - $cmd = $this->createWindowsCmd(); - [$code, $output] = $cmd->execWithResult($command); - - $this->assertIsInt($code); - $this->assertEquals(0, $code); - $this->assertIsArray($output); - $this->assertNotEmpty($output); - } - - public function testExecWithResultWithLog(): void - { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('This test is for Windows systems only'); - } - - $cmd = $this->createWindowsCmd(); - [$code, $output] = $cmd->execWithResult('echo test', false); - - $this->assertIsInt($code); - $this->assertIsArray($output); - $this->assertEquals(0, $code); - $this->assertEquals(['test'], $output); - } - - public static function commandProvider(): array - { - return [ - 'echo-command' => ['echo test'], - 'dir-command' => ['dir'], - 'cd-command' => ['cd'], - ]; - } -} diff --git a/tests/StaticPHP/Config/ArtifactConfigTest.php b/tests/StaticPHP/Config/ArtifactConfigTest.php new file mode 100644 index 000000000..dc3964881 --- /dev/null +++ b/tests/StaticPHP/Config/ArtifactConfigTest.php @@ -0,0 +1,303 @@ +tempDir = sys_get_temp_dir() . '/artifact_config_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset static state + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue([]); + } + + /** @noinspection PhpExpressionResultUnusedInspection */ + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset static state + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue([]); + } + + public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void + { + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load artifact config.'); + + ArtifactConfig::loadFromDir('/nonexistent/path'); + } + + public function testLoadFromDirWithValidArtifactJson(): void + { + $artifactContent = json_encode([ + 'test-artifact' => [ + 'source' => 'https://example.com/file.tar.gz', + ], + ]); + + file_put_contents($this->tempDir . '/artifact.json', $artifactContent); + + ArtifactConfig::loadFromDir($this->tempDir); + + $config = ArtifactConfig::get('test-artifact'); + $this->assertIsArray($config); + $this->assertArrayHasKey('source', $config); + } + + public function testLoadFromDirWithMultipleArtifactFiles(): void + { + $artifact1Content = json_encode([ + 'artifact-1' => [ + 'source' => 'https://example.com/file1.tar.gz', + ], + ]); + + $artifact2Content = json_encode([ + 'artifact-2' => [ + 'source' => 'https://example.com/file2.tar.gz', + ], + ]); + + file_put_contents($this->tempDir . '/artifact.ext.json', $artifact1Content); + file_put_contents($this->tempDir . '/artifact.lib.json', $artifact2Content); + file_put_contents($this->tempDir . '/artifact.json', json_encode(['artifact-3' => ['source' => 'custom']])); + + ArtifactConfig::loadFromDir($this->tempDir); + + $this->assertNotNull(ArtifactConfig::get('artifact-1')); + $this->assertNotNull(ArtifactConfig::get('artifact-2')); + $this->assertNotNull(ArtifactConfig::get('artifact-3')); + } + + public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void + { + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Failed to read artifact config file:'); + + ArtifactConfig::loadFromFile('/nonexistent/file.json'); + } + + public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void + { + $file = $this->tempDir . '/invalid.json'; + file_put_contents($file, 'not valid json{'); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Invalid JSON format in artifact config file:'); + + ArtifactConfig::loadFromFile($file); + } + + public function testLoadFromFileWithValidJson(): void + { + $file = $this->tempDir . '/valid.json'; + $content = json_encode([ + 'my-artifact' => [ + 'source' => [ + 'type' => 'url', + 'url' => 'https://example.com/file.tar.gz', + ], + ], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $config = ArtifactConfig::get('my-artifact'); + $this->assertIsArray($config); + $this->assertArrayHasKey('source', $config); + } + + public function testGetAllReturnsAllLoadedArtifacts(): void + { + $file = $this->tempDir . '/artifacts.json'; + $content = json_encode([ + 'artifact-a' => ['source' => 'custom'], + 'artifact-b' => ['source' => 'custom'], + 'artifact-c' => ['source' => 'custom'], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $all = ArtifactConfig::getAll(); + $this->assertIsArray($all); + $this->assertCount(3, $all); + $this->assertArrayHasKey('artifact-a', $all); + $this->assertArrayHasKey('artifact-b', $all); + $this->assertArrayHasKey('artifact-c', $all); + } + + public function testGetReturnsNullWhenArtifactNotFound(): void + { + $this->assertNull(ArtifactConfig::get('non-existent-artifact')); + } + + public function testGetReturnsConfigWhenArtifactExists(): void + { + $file = $this->tempDir . '/artifacts.json'; + $content = json_encode([ + 'test-artifact' => [ + 'source' => 'custom', + 'binary' => 'custom', + ], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $config = ArtifactConfig::get('test-artifact'); + $this->assertIsArray($config); + $this->assertEquals('custom', $config['source']); + $this->assertIsArray($config['binary']); + } + + public function testLoadFromFileWithExpandedUrlInSource(): void + { + $file = $this->tempDir . '/artifacts.json'; + $content = json_encode([ + 'test-artifact' => [ + 'source' => 'https://example.com/archive.tar.gz', + ], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $config = ArtifactConfig::get('test-artifact'); + $this->assertIsArray($config); + $this->assertIsArray($config['source']); + $this->assertEquals('url', $config['source']['type']); + $this->assertEquals('https://example.com/archive.tar.gz', $config['source']['url']); + } + + public function testLoadFromFileWithBinaryCustom(): void + { + $file = $this->tempDir . '/artifacts.json'; + $content = json_encode([ + 'test-artifact' => [ + 'source' => 'custom', + 'binary' => 'custom', + ], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $config = ArtifactConfig::get('test-artifact'); + $this->assertIsArray($config['binary']); + $this->assertArrayHasKey('linux-x86_64', $config['binary']); + $this->assertArrayHasKey('macos-aarch64', $config['binary']); + $this->assertEquals('custom', $config['binary']['linux-x86_64']['type']); + } + + public function testLoadFromFileWithBinaryHosted(): void + { + $file = $this->tempDir . '/artifacts.json'; + $content = json_encode([ + 'test-artifact' => [ + 'source' => 'custom', + 'binary' => 'hosted', + ], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $config = ArtifactConfig::get('test-artifact'); + $this->assertIsArray($config['binary']); + $this->assertEquals('hosted', $config['binary']['linux-x86_64']['type']); + $this->assertEquals('hosted', $config['binary']['macos-aarch64']['type']); + } + + public function testLoadFromFileWithBinaryPlatformSpecific(): void + { + $file = $this->tempDir . '/artifacts.json'; + $content = json_encode([ + 'test-artifact' => [ + 'source' => 'custom', + 'binary' => [ + 'linux-x86_64' => 'https://example.com/linux.tar.gz', + 'macos-aarch64' => [ + 'type' => 'url', + 'url' => 'https://example.com/macos.tar.gz', + ], + ], + ], + ]); + file_put_contents($file, $content); + + ArtifactConfig::loadFromFile($file); + + $config = ArtifactConfig::get('test-artifact'); + $this->assertIsArray($config['binary']); + $this->assertEquals('url', $config['binary']['linux-x86_64']['type']); + $this->assertEquals('https://example.com/linux.tar.gz', $config['binary']['linux-x86_64']['url']); + $this->assertEquals('url', $config['binary']['macos-aarch64']['type']); + $this->assertEquals('https://example.com/macos.tar.gz', $config['binary']['macos-aarch64']['url']); + } + + public function testLoadFromDirWithEmptyDirectory(): void + { + // Empty directory should not throw exception + ArtifactConfig::loadFromDir($this->tempDir); + + $this->assertEquals([], ArtifactConfig::getAll()); + } + + public function testMultipleLoadsAppendConfigs(): void + { + $file1 = $this->tempDir . '/artifact1.json'; + $file2 = $this->tempDir . '/artifact2.json'; + + file_put_contents($file1, json_encode(['art1' => ['source' => 'custom']])); + file_put_contents($file2, json_encode(['art2' => ['source' => 'custom']])); + + ArtifactConfig::loadFromFile($file1); + ArtifactConfig::loadFromFile($file2); + + $all = ArtifactConfig::getAll(); + $this->assertCount(2, $all); + $this->assertArrayHasKey('art1', $all); + $this->assertArrayHasKey('art2', $all); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/tests/StaticPHP/Config/ConfigTypeTest.php b/tests/StaticPHP/Config/ConfigTypeTest.php new file mode 100644 index 000000000..0990931ef --- /dev/null +++ b/tests/StaticPHP/Config/ConfigTypeTest.php @@ -0,0 +1,196 @@ +assertEquals('list_array', ConfigType::LIST_ARRAY); + $this->assertEquals('assoc_array', ConfigType::ASSOC_ARRAY); + $this->assertEquals('string', ConfigType::STRING); + $this->assertEquals('bool', ConfigType::BOOL); + } + + public function testPackageTypesConstant(): void + { + $expectedTypes = [ + 'library', + 'php-extension', + 'target', + 'virtual-target', + ]; + + $this->assertEquals($expectedTypes, ConfigType::PACKAGE_TYPES); + } + + public function testValidateLicenseFieldWithValidFileType(): void + { + $license = [ + 'type' => 'file', + 'path' => 'LICENSE', + ]; + + $this->assertTrue(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldWithValidFileTypeArrayPath(): void + { + $license = [ + 'type' => 'file', + 'path' => ['LICENSE', 'COPYING'], + ]; + + $this->assertTrue(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldWithValidTextType(): void + { + $license = [ + 'type' => 'text', + 'text' => 'MIT License', + ]; + + $this->assertTrue(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldWithListOfLicenses(): void + { + $licenses = [ + [ + 'type' => 'file', + 'path' => 'LICENSE', + ], + [ + 'type' => 'text', + 'text' => 'MIT', + ], + ]; + + $this->assertTrue(ConfigType::validateLicenseField($licenses)); + } + + public function testValidateLicenseFieldWithEmptyList(): void + { + $licenses = []; + + $this->assertTrue(ConfigType::validateLicenseField($licenses)); + } + + public function testValidateLicenseFieldReturnsFalseWhenNotAssocArray(): void + { + $this->assertFalse(ConfigType::validateLicenseField('string')); + $this->assertFalse(ConfigType::validateLicenseField(123)); + $this->assertFalse(ConfigType::validateLicenseField(true)); + } + + public function testValidateLicenseFieldReturnsFalseWhenMissingType(): void + { + $license = [ + 'path' => 'LICENSE', + ]; + + $this->assertFalse(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldReturnsFalseWithInvalidType(): void + { + $license = [ + 'type' => 'invalid', + 'data' => 'something', + ]; + + $this->assertFalse(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldReturnsFalseWhenFileTypeMissingPath(): void + { + $license = [ + 'type' => 'file', + ]; + + $this->assertFalse(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldReturnsFalseWhenFileTypePathIsInvalid(): void + { + $license = [ + 'type' => 'file', + 'path' => 123, + ]; + + $this->assertFalse(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldReturnsFalseWhenTextTypeMissingText(): void + { + $license = [ + 'type' => 'text', + ]; + + $this->assertFalse(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldReturnsFalseWhenTextTypeTextIsNotString(): void + { + $license = [ + 'type' => 'text', + 'text' => ['array'], + ]; + + $this->assertFalse(ConfigType::validateLicenseField($license)); + } + + public function testValidateLicenseFieldWithListContainingInvalidItem(): void + { + $licenses = [ + [ + 'type' => 'file', + 'path' => 'LICENSE', + ], + [ + 'type' => 'text', + // missing 'text' field + ], + ]; + + $this->assertFalse(ConfigType::validateLicenseField($licenses)); + } + + public function testValidateLicenseFieldWithNestedListsOfLicenses(): void + { + $licenses = [ + [ + [ + 'type' => 'file', + 'path' => 'LICENSE', + ], + ], + ]; + + $this->assertTrue(ConfigType::validateLicenseField($licenses)); + } + + public function testValidateLicenseFieldWithNestedListContainingInvalidItem(): void + { + $licenses = [ + [ + [ + 'type' => 'file', + 'path' => 'LICENSE', + ], + 'invalid-string-item', + ], + ]; + + $this->assertFalse(ConfigType::validateLicenseField($licenses)); + } +} diff --git a/tests/StaticPHP/Config/ConfigValidatorTest.php b/tests/StaticPHP/Config/ConfigValidatorTest.php new file mode 100644 index 000000000..ae5544ae1 --- /dev/null +++ b/tests/StaticPHP/Config/ConfigValidatorTest.php @@ -0,0 +1,627 @@ +expectException(ValidationException::class); + $this->expectExceptionMessage('test.json is broken'); + + $data = 'not an array'; + ConfigValidator::validateAndLintArtifacts('test.json', $data); + } + + public function testValidateAndLintArtifactsWithCustomSource(): void + { + $data = [ + 'test-artifact' => [ + 'source' => 'custom', + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertEquals('custom', $data['test-artifact']['source']); + } + + public function testValidateAndLintArtifactsExpandsUrlString(): void + { + $data = [ + 'test-artifact' => [ + 'source' => 'https://example.com/file.tar.gz', + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['source']); + $this->assertEquals('url', $data['test-artifact']['source']['type']); + $this->assertEquals('https://example.com/file.tar.gz', $data['test-artifact']['source']['url']); + } + + public function testValidateAndLintArtifactsExpandsHttpUrlString(): void + { + $data = [ + 'test-artifact' => [ + 'source' => 'http://example.com/file.tar.gz', + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['source']); + $this->assertEquals('url', $data['test-artifact']['source']['type']); + $this->assertEquals('http://example.com/file.tar.gz', $data['test-artifact']['source']['url']); + } + + public function testValidateAndLintArtifactsWithSourceObject(): void + { + $data = [ + 'test-artifact' => [ + 'source' => [ + 'type' => 'git', + 'url' => 'https://github.com/example/repo.git', + 'rev' => 'main', + ], + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['source']); + $this->assertEquals('git', $data['test-artifact']['source']['type']); + } + + public function testValidateAndLintArtifactsWithBinaryCustom(): void + { + $data = [ + 'test-artifact' => [ + 'binary' => 'custom', + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['binary']); + $this->assertArrayHasKey('linux-x86_64', $data['test-artifact']['binary']); + $this->assertEquals('custom', $data['test-artifact']['binary']['linux-x86_64']['type']); + } + + public function testValidateAndLintArtifactsWithBinaryHosted(): void + { + $data = [ + 'test-artifact' => [ + 'binary' => 'hosted', + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['binary']); + $this->assertArrayHasKey('macos-aarch64', $data['test-artifact']['binary']); + $this->assertEquals('hosted', $data['test-artifact']['binary']['macos-aarch64']['type']); + } + + public function testValidateAndLintArtifactsWithBinaryPlatformObject(): void + { + $data = [ + 'test-artifact' => [ + 'binary' => [ + 'linux-x86_64' => [ + 'type' => 'url', + 'url' => 'https://example.com/binary.tar.gz', + ], + ], + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertEquals('url', $data['test-artifact']['binary']['linux-x86_64']['type']); + } + + public function testValidateAndLintArtifactsExpandsBinaryPlatformUrlString(): void + { + $data = [ + 'test-artifact' => [ + 'binary' => [ + 'linux-x86_64' => 'https://example.com/binary.tar.gz', + ], + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['binary']['linux-x86_64']); + $this->assertEquals('url', $data['test-artifact']['binary']['linux-x86_64']['type']); + $this->assertEquals('https://example.com/binary.tar.gz', $data['test-artifact']['binary']['linux-x86_64']['url']); + } + + public function testValidateAndLintArtifactsWithSourceMirror(): void + { + $data = [ + 'test-artifact' => [ + 'source-mirror' => 'https://mirror.example.com/file.tar.gz', + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data['test-artifact']['source-mirror']); + $this->assertEquals('url', $data['test-artifact']['source-mirror']['type']); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenDataIsNotArray(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('pkg.json is broken'); + + $data = 'not an array'; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenPackageIsNotAssocArray(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package [test-pkg] in pkg.json is not a valid associative array'); + + $data = [ + 'test-pkg' => ['list', 'array'], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenTypeMissing(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Package [test-pkg] in pkg.json has invalid or missing 'type' field"); + + $data = [ + 'test-pkg' => [ + 'depends' => [], + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenTypeInvalid(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Package [test-pkg] in pkg.json has invalid or missing 'type' field"); + + $data = [ + 'test-pkg' => [ + 'type' => 'invalid-type', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesWithValidLibraryType(): void + { + $data = [ + 'test-lib' => [ + 'type' => 'library', + 'artifact' => 'test-artifact', + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertEquals('library', $data['test-lib']['type']); + } + + public function testValidateAndLintPackagesWithValidPhpExtensionType(): void + { + $data = [ + 'test-ext' => [ + 'type' => 'php-extension', + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertEquals('php-extension', $data['test-ext']['type']); + } + + public function testValidateAndLintPackagesWithValidTargetType(): void + { + $data = [ + 'test-target' => [ + 'type' => 'target', + 'artifact' => 'test-artifact', + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertEquals('target', $data['test-target']['type']); + } + + public function testValidateAndLintPackagesWithValidVirtualTargetType(): void + { + $data = [ + 'test-virtual' => [ + 'type' => 'virtual-target', + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertEquals('virtual-target', $data['test-virtual']['type']); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenLibraryMissingArtifact(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Package [test-lib] in pkg.json of type 'library' must have an 'artifact' field"); + + $data = [ + 'test-lib' => [ + 'type' => 'library', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenTargetMissingArtifact(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Package [test-target] in pkg.json of type 'target' must have an 'artifact' field"); + + $data = [ + 'test-target' => [ + 'type' => 'target', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesWithPhpExtensionFields(): void + { + $data = [ + 'test-ext' => [ + 'type' => 'php-extension', + 'php-extension' => [ + 'zend-extension' => false, + 'build-shared' => true, + ], + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertIsArray($data['test-ext']['php-extension']); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenPhpExtensionIsNotObject(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package test-ext [php-extension] must be an object'); + + $data = [ + 'test-ext' => [ + 'type' => 'php-extension', + 'php-extension' => 'string', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesWithDependsField(): void + { + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends' => ['dep1', 'dep2'], + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertIsArray($data['test-pkg']['depends']); + } + + public function testValidateAndLintPackagesThrowsExceptionWhenDependsIsNotList(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package test-pkg [depends] must be a list'); + + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends' => 'not-a-list', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesWithSuffixFields(): void + { + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends@linux' => ['linux-dep'], + 'depends@windows' => ['windows-dep'], + 'headers@unix' => ['header.h'], + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertIsArray($data['test-pkg']['depends@linux']); + } + + public function testValidateAndLintPackagesThrowsExceptionForInvalidSuffixFieldType(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package test-pkg [headers@linux] must be a list'); + + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'headers@linux' => 'not-a-list', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionForUnknownField(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('package [test-pkg] has invalid field [unknown-field]'); + + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'unknown-field' => 'value', + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionForUnknownPhpExtensionField(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('php-extension [test-ext] has invalid field [unknown]'); + + $data = [ + 'test-ext' => [ + 'type' => 'php-extension', + 'php-extension' => [ + 'unknown' => 'value', + ], + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidatePlatformStringWithValidPlatforms(): void + { + ConfigValidator::validatePlatformString('linux-x86_64'); + ConfigValidator::validatePlatformString('linux-aarch64'); + ConfigValidator::validatePlatformString('windows-x86_64'); + ConfigValidator::validatePlatformString('windows-aarch64'); + ConfigValidator::validatePlatformString('macos-x86_64'); + ConfigValidator::validatePlatformString('macos-aarch64'); + + $this->assertTrue(true); // If no exception thrown, test passes + } + + public function testValidatePlatformStringThrowsExceptionForInvalidFormat(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Invalid platform format 'invalid', expected format 'os-arch'"); + + ConfigValidator::validatePlatformString('invalid'); + } + + public function testValidatePlatformStringThrowsExceptionForTooManyParts(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Invalid platform format 'linux-x86_64-extra', expected format 'os-arch'"); + + ConfigValidator::validatePlatformString('linux-x86_64-extra'); + } + + public function testValidatePlatformStringThrowsExceptionForInvalidOS(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Invalid platform OS 'bsd' in platform 'bsd-x86_64'"); + + ConfigValidator::validatePlatformString('bsd-x86_64'); + } + + public function testValidatePlatformStringThrowsExceptionForInvalidArch(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Invalid platform architecture 'arm' in platform 'linux-arm'"); + + ConfigValidator::validatePlatformString('linux-arm'); + } + + public function testArtifactTypeFieldsConstant(): void + { + $this->assertArrayHasKey('filelist', ConfigValidator::ARTIFACT_TYPE_FIELDS); + $this->assertArrayHasKey('git', ConfigValidator::ARTIFACT_TYPE_FIELDS); + $this->assertArrayHasKey('ghtagtar', ConfigValidator::ARTIFACT_TYPE_FIELDS); + $this->assertArrayHasKey('url', ConfigValidator::ARTIFACT_TYPE_FIELDS); + $this->assertArrayHasKey('custom', ConfigValidator::ARTIFACT_TYPE_FIELDS); + } + + public function testValidateAndLintArtifactsThrowsExceptionForInvalidArtifactType(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact source object has unknown type 'invalid-type'"); + + $data = [ + 'test-artifact' => [ + 'source' => [ + 'type' => 'invalid-type', + ], + ], + ]; + ConfigValidator::validateAndLintArtifacts('test.json', $data); + } + + public function testValidateAndLintArtifactsThrowsExceptionForMissingRequiredField(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact source object of type 'git' must have required field 'url'"); + + $data = [ + 'test-artifact' => [ + 'source' => [ + 'type' => 'git', + 'rev' => 'main', + // missing 'url' + ], + ], + ]; + ConfigValidator::validateAndLintArtifacts('test.json', $data); + } + + public function testValidateAndLintArtifactsThrowsExceptionForMissingTypeInSource(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact source object must have a valid 'type' field"); + + $data = [ + 'test-artifact' => [ + 'source' => [ + 'url' => 'https://example.com', + ], + ], + ]; + ConfigValidator::validateAndLintArtifacts('test.json', $data); + } + + public function testValidateAndLintArtifactsWithAllArtifactTypes(): void + { + $data = [ + 'filelist-artifact' => [ + 'source' => [ + 'type' => 'filelist', + 'url' => 'https://example.com/list', + 'regex' => '/pattern/', + ], + ], + 'git-artifact' => [ + 'source' => [ + 'type' => 'git', + 'url' => 'https://github.com/example/repo.git', + 'rev' => 'main', + ], + ], + 'ghtagtar-artifact' => [ + 'source' => [ + 'type' => 'ghtagtar', + 'repo' => 'example/repo', + ], + ], + 'url-artifact' => [ + 'source' => [ + 'type' => 'url', + 'url' => 'https://example.com/file.tar.gz', + ], + ], + 'custom-artifact' => [ + 'source' => [ + 'type' => 'custom', + ], + ], + ]; + + ConfigValidator::validateAndLintArtifacts('test.json', $data); + + $this->assertIsArray($data); + } + + public function testValidateAndLintPackagesWithAllFieldTypes(): void + { + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test-artifact', + 'depends' => ['dep1'], + 'suggests' => ['sug1'], + 'license' => [ + 'type' => 'file', + 'path' => 'LICENSE', + ], + 'lang' => 'c', + 'frameworks' => ['framework1'], + 'headers' => ['header.h'], + 'static-libs' => ['lib.a'], + 'pkg-configs' => ['pkg.pc'], + 'static-bins' => ['bin'], + ], + ]; + + ConfigValidator::validateAndLintPackages('pkg.json', $data); + + $this->assertEquals('library', $data['test-pkg']['type']); + } + + public function testValidateAndLintPackagesThrowsExceptionForWrongTypeString(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package test-pkg [artifact] must be string'); + + $data = [ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => ['not', 'a', 'string'], + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionForWrongTypeBool(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package test-ext [zend-extension] must be boolean'); + + $data = [ + 'test-ext' => [ + 'type' => 'php-extension', + 'php-extension' => [ + 'zend-extension' => 'not-a-bool', + ], + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } + + public function testValidateAndLintPackagesThrowsExceptionForWrongTypeAssocArray(): void + { + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('Package test-pkg [support] must be an object'); + + $data = [ + 'test-pkg' => [ + 'type' => 'php-extension', + 'php-extension' => [ + 'support' => 'not-an-object', + ], + ], + ]; + ConfigValidator::validateAndLintPackages('pkg.json', $data); + } +} diff --git a/tests/StaticPHP/Config/PackageConfigTest.php b/tests/StaticPHP/Config/PackageConfigTest.php new file mode 100644 index 000000000..4072e39e9 --- /dev/null +++ b/tests/StaticPHP/Config/PackageConfigTest.php @@ -0,0 +1,434 @@ +tempDir = sys_get_temp_dir() . '/package_config_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset static state + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + $property->setAccessible(true); + $property->setValue([]); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset static state + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + $property->setAccessible(true); + $property->setValue([]); + } + + public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void + { + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load pkg.json config.'); + + PackageConfig::loadFromDir('/nonexistent/path'); + } + + public function testLoadFromDirWithValidPkgJson(): void + { + $packageContent = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test-artifact', + ], + ]); + + file_put_contents($this->tempDir . '/pkg.json', $packageContent); + + PackageConfig::loadFromDir($this->tempDir); + + $this->assertTrue(PackageConfig::isPackageExists('test-pkg')); + } + + public function testLoadFromDirWithMultiplePackageFiles(): void + { + $pkg1Content = json_encode([ + 'pkg-1' => [ + 'type' => 'library', + 'artifact' => 'artifact-1', + ], + ]); + + $pkg2Content = json_encode([ + 'pkg-2' => [ + 'type' => 'php-extension', + ], + ]); + + file_put_contents($this->tempDir . '/pkg.ext.json', $pkg1Content); + file_put_contents($this->tempDir . '/pkg.lib.json', $pkg2Content); + file_put_contents($this->tempDir . '/pkg.json', json_encode(['pkg-3' => ['type' => 'virtual-target']])); + + PackageConfig::loadFromDir($this->tempDir); + + $this->assertTrue(PackageConfig::isPackageExists('pkg-1')); + $this->assertTrue(PackageConfig::isPackageExists('pkg-2')); + $this->assertTrue(PackageConfig::isPackageExists('pkg-3')); + } + + public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void + { + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Failed to read package config file:'); + + PackageConfig::loadFromFile('/nonexistent/file.json'); + } + + public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void + { + $file = $this->tempDir . '/invalid.json'; + file_put_contents($file, 'not valid json{'); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Invalid JSON format in package config file:'); + + PackageConfig::loadFromFile($file); + } + + public function testLoadFromFileWithValidJson(): void + { + $file = $this->tempDir . '/valid.json'; + $content = json_encode([ + 'my-pkg' => [ + 'type' => 'library', + 'artifact' => 'my-artifact', + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $this->assertTrue(PackageConfig::isPackageExists('my-pkg')); + } + + public function testIsPackageExistsReturnsFalseWhenPackageNotLoaded(): void + { + $this->assertFalse(PackageConfig::isPackageExists('non-existent')); + } + + public function testIsPackageExistsReturnsTrueWhenPackageLoaded(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $this->assertTrue(PackageConfig::isPackageExists('test-pkg')); + } + + public function testGetAllReturnsAllLoadedPackages(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'pkg-a' => ['type' => 'virtual-target'], + 'pkg-b' => ['type' => 'virtual-target'], + 'pkg-c' => ['type' => 'virtual-target'], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $all = PackageConfig::getAll(); + $this->assertIsArray($all); + $this->assertCount(3, $all); + $this->assertArrayHasKey('pkg-a', $all); + $this->assertArrayHasKey('pkg-b', $all); + $this->assertArrayHasKey('pkg-c', $all); + } + + public function testGetReturnsDefaultWhenPackageNotExists(): void + { + $result = PackageConfig::get('non-existent', 'field', 'default-value'); + + $this->assertEquals('default-value', $result); + } + + public function testGetReturnsWholePackageWhenFieldNameIsNull(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends' => ['dep1'], + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $result = PackageConfig::get('test-pkg'); + $this->assertIsArray($result); + $this->assertEquals('library', $result['type']); + $this->assertEquals('test', $result['artifact']); + } + + public function testGetReturnsFieldValueWhenFieldExists(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test-artifact', + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $result = PackageConfig::get('test-pkg', 'artifact'); + $this->assertEquals('test-artifact', $result); + } + + public function testGetReturnsDefaultWhenFieldNotExists(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $result = PackageConfig::get('test-pkg', 'non-existent-field', 'default'); + $this->assertEquals('default', $result); + } + + public function testGetWithSuffixFieldsOnLinux(): void + { + // Mock SystemTarget to return Linux + $mockTarget = $this->getMockBuilder(SystemTarget::class) + ->disableOriginalConstructor() + ->getMock(); + + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends' => ['base-dep'], + 'depends@linux' => ['linux-dep'], + 'depends@unix' => ['unix-dep'], + 'depends@windows' => ['windows-dep'], + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + // The get method will check SystemTarget::getTargetOS() + // On real Linux systems, it should return 'depends@linux' first + $result = PackageConfig::get('test-pkg', 'depends', []); + + // Result should be one of the suffixed versions or base version + $this->assertIsArray($result); + } + + public function testGetWithSuffixFieldsReturnsBasicFieldWhenNoSuffixMatch(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends' => ['base-dep'], + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $result = PackageConfig::get('test-pkg', 'depends'); + $this->assertEquals(['base-dep'], $result); + } + + public function testGetWithNonSuffixedFieldIgnoresSuffixes(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test-artifact', + 'artifact@linux' => 'linux-artifact', // This should be ignored + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + // 'artifact' is not in SUFFIX_ALLOWED_FIELDS, so it won't check suffixes + $result = PackageConfig::get('test-pkg', 'artifact'); + $this->assertEquals('test-artifact', $result); + } + + public function testGetAllSuffixAllowedFields(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'library', + 'artifact' => 'test', + 'depends@linux' => ['dep1'], + 'suggests@macos' => ['sug1'], + 'headers@unix' => ['header.h'], + 'static-libs@windows' => ['lib.a'], + 'static-bins@linux' => ['bin'], + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + // These are all suffix-allowed fields + $pkg = PackageConfig::get('test-pkg'); + $this->assertArrayHasKey('depends@linux', $pkg); + $this->assertArrayHasKey('suggests@macos', $pkg); + $this->assertArrayHasKey('headers@unix', $pkg); + $this->assertArrayHasKey('static-libs@windows', $pkg); + $this->assertArrayHasKey('static-bins@linux', $pkg); + } + + public function testLoadFromDirWithEmptyDirectory(): void + { + // Empty directory should not throw exception + PackageConfig::loadFromDir($this->tempDir); + + $this->assertEquals([], PackageConfig::getAll()); + } + + public function testMultipleLoadsAppendConfigs(): void + { + $file1 = $this->tempDir . '/pkg1.json'; + $file2 = $this->tempDir . '/pkg2.json'; + + file_put_contents($file1, json_encode(['pkg1' => ['type' => 'virtual-target']])); + file_put_contents($file2, json_encode(['pkg2' => ['type' => 'virtual-target']])); + + PackageConfig::loadFromFile($file1); + PackageConfig::loadFromFile($file2); + + $all = PackageConfig::getAll(); + $this->assertCount(2, $all); + $this->assertArrayHasKey('pkg1', $all); + $this->assertArrayHasKey('pkg2', $all); + } + + public function testGetWithComplexPhpExtensionPackage(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-ext' => [ + 'type' => 'php-extension', + 'depends' => ['dep1'], + 'php-extension' => [ + 'zend-extension' => false, + 'build-shared' => true, + 'build-static' => false, + ], + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $phpExt = PackageConfig::get('test-ext', 'php-extension'); + $this->assertIsArray($phpExt); + $this->assertFalse($phpExt['zend-extension']); + $this->assertTrue($phpExt['build-shared']); + } + + public function testGetReturnsNullAsDefaultWhenNotSpecified(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'test-pkg' => [ + 'type' => 'virtual-target', + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $result = PackageConfig::get('test-pkg', 'non-existent'); + $this->assertNull($result); + } + + public function testLoadFromFileWithAllPackageTypes(): void + { + $file = $this->tempDir . '/pkg.json'; + $content = json_encode([ + 'library-pkg' => [ + 'type' => 'library', + 'artifact' => 'lib-artifact', + ], + 'extension-pkg' => [ + 'type' => 'php-extension', + ], + 'target-pkg' => [ + 'type' => 'target', + 'artifact' => 'target-artifact', + ], + 'virtual-pkg' => [ + 'type' => 'virtual-target', + ], + ]); + file_put_contents($file, $content); + + PackageConfig::loadFromFile($file); + + $this->assertTrue(PackageConfig::isPackageExists('library-pkg')); + $this->assertTrue(PackageConfig::isPackageExists('extension-pkg')); + $this->assertTrue(PackageConfig::isPackageExists('target-pkg')); + $this->assertTrue(PackageConfig::isPackageExists('virtual-pkg')); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/tests/StaticPHP/DI/ApplicationContextTest.php b/tests/StaticPHP/DI/ApplicationContextTest.php new file mode 100644 index 000000000..3da63f47c --- /dev/null +++ b/tests/StaticPHP/DI/ApplicationContextTest.php @@ -0,0 +1,433 @@ +assertInstanceOf(Container::class, $container); + $this->assertSame($container, ApplicationContext::getContainer()); + } + + public function testInitializeWithDebugMode(): void + { + ApplicationContext::initialize(['debug' => true]); + + $this->assertTrue(ApplicationContext::isDebug()); + } + + public function testInitializeWithoutDebugMode(): void + { + ApplicationContext::initialize(['debug' => false]); + + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testInitializeWithCustomDefinitions(): void + { + $customValue = 'test_value'; + ApplicationContext::initialize([ + 'definitions' => [ + 'test.service' => $customValue, + ], + ]); + + $this->assertEquals($customValue, ApplicationContext::get('test.service')); + } + + public function testInitializeThrowsExceptionWhenAlreadyInitialized(): void + { + ApplicationContext::initialize(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('ApplicationContext already initialized'); + ApplicationContext::initialize(); + } + + public function testGetContainerAutoInitializes(): void + { + // Don't call initialize + $container = ApplicationContext::getContainer(); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testGetReturnsServiceFromContainer(): void + { + ApplicationContext::initialize([ + 'definitions' => [ + 'test.key' => 'test_value', + ], + ]); + + $this->assertEquals('test_value', ApplicationContext::get('test.key')); + } + + public function testGetWithClassType(): void + { + ApplicationContext::initialize(); + + $container = ApplicationContext::get(Container::class); + $this->assertInstanceOf(Container::class, $container); + } + + public function testGetContainerInterface(): void + { + ApplicationContext::initialize(); + + $container = ApplicationContext::get(ContainerInterface::class); + $this->assertInstanceOf(ContainerInterface::class, $container); + } + + public function testHasReturnsTrueForExistingService(): void + { + ApplicationContext::initialize([ + 'definitions' => [ + 'test.service' => 'value', + ], + ]); + + $this->assertTrue(ApplicationContext::has('test.service')); + } + + public function testHasReturnsFalseForNonExistingService(): void + { + ApplicationContext::initialize(); + + $this->assertFalse(ApplicationContext::has('non.existing.service')); + } + + public function testSetAddsServiceToContainer(): void + { + ApplicationContext::initialize(); + + ApplicationContext::set('dynamic.service', 'dynamic_value'); + + $this->assertTrue(ApplicationContext::has('dynamic.service')); + $this->assertEquals('dynamic_value', ApplicationContext::get('dynamic.service')); + } + + public function testSetOverridesExistingService(): void + { + ApplicationContext::initialize([ + 'definitions' => [ + 'test.service' => 'original', + ], + ]); + + ApplicationContext::set('test.service', 'updated'); + + $this->assertEquals('updated', ApplicationContext::get('test.service')); + } + + public function testBindCommandContextSetsInputAndOutput(): void + { + ApplicationContext::initialize(); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $output->method('isDebug')->willReturn(false); + + ApplicationContext::bindCommandContext($input, $output); + + $this->assertSame($input, ApplicationContext::get(InputInterface::class)); + $this->assertSame($output, ApplicationContext::get(OutputInterface::class)); + } + + public function testBindCommandContextSetsDebugMode(): void + { + ApplicationContext::initialize(); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $output->method('isDebug')->willReturn(true); + + ApplicationContext::bindCommandContext($input, $output); + + $this->assertTrue(ApplicationContext::isDebug()); + } + + public function testBindCommandContextWithNonDebugOutput(): void + { + ApplicationContext::initialize(); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $output->method('isDebug')->willReturn(false); + + ApplicationContext::bindCommandContext($input, $output); + + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testGetInvokerReturnsCallbackInvoker(): void + { + ApplicationContext::initialize(); + + $invoker = ApplicationContext::getInvoker(); + + $this->assertInstanceOf(CallbackInvoker::class, $invoker); + } + + public function testGetInvokerReturnsSameInstance(): void + { + ApplicationContext::initialize(); + + $invoker1 = ApplicationContext::getInvoker(); + $invoker2 = ApplicationContext::getInvoker(); + + $this->assertSame($invoker1, $invoker2); + } + + public function testGetInvokerAutoInitializesContainer(): void + { + // Don't call initialize + $invoker = ApplicationContext::getInvoker(); + + $this->assertInstanceOf(CallbackInvoker::class, $invoker); + } + + public function testInvokeCallsCallback(): void + { + ApplicationContext::initialize(); + + $called = false; + $callback = function () use (&$called) { + $called = true; + return 'result'; + }; + + $result = ApplicationContext::invoke($callback); + + $this->assertTrue($called); + $this->assertEquals('result', $result); + } + + public function testInvokeWithContext(): void + { + ApplicationContext::initialize(); + + $callback = function (string $param) { + return $param; + }; + + $result = ApplicationContext::invoke($callback, ['param' => 'test_value']); + + $this->assertEquals('test_value', $result); + } + + public function testInvokeWithDependencyInjection(): void + { + ApplicationContext::initialize(); + + $callback = function (Container $container) { + return $container; + }; + + $result = ApplicationContext::invoke($callback); + + $this->assertInstanceOf(Container::class, $result); + } + + public function testInvokeWithArrayCallback(): void + { + ApplicationContext::initialize(); + + $object = new class { + public function method(): string + { + return 'called'; + } + }; + + $result = ApplicationContext::invoke([$object, 'method']); + + $this->assertEquals('called', $result); + } + + public function testIsDebugDefaultsFalse(): void + { + ApplicationContext::initialize(); + + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testSetDebugChangesDebugMode(): void + { + ApplicationContext::initialize(); + + ApplicationContext::setDebug(true); + $this->assertTrue(ApplicationContext::isDebug()); + + ApplicationContext::setDebug(false); + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testResetClearsContainer(): void + { + ApplicationContext::initialize(); + ApplicationContext::set('test.service', 'value'); + + ApplicationContext::reset(); + + // After reset, container should be reinitialized + $this->assertFalse(ApplicationContext::has('test.service')); + } + + public function testResetClearsInvoker(): void + { + ApplicationContext::initialize(); + $invoker1 = ApplicationContext::getInvoker(); + + ApplicationContext::reset(); + + $invoker2 = ApplicationContext::getInvoker(); + $this->assertNotSame($invoker1, $invoker2); + } + + public function testResetClearsDebugMode(): void + { + ApplicationContext::initialize(['debug' => true]); + $this->assertTrue(ApplicationContext::isDebug()); + + ApplicationContext::reset(); + + // After reset and reinit, debug should be false by default + ApplicationContext::initialize(); + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testResetAllowsReinitialize(): void + { + ApplicationContext::initialize(); + ApplicationContext::reset(); + + // Should not throw exception + $container = ApplicationContext::initialize(['debug' => true]); + + $this->assertInstanceOf(Container::class, $container); + $this->assertTrue(ApplicationContext::isDebug()); + } + + public function testCallbackInvokerIsAvailableInContainer(): void + { + ApplicationContext::initialize(); + + $invoker = ApplicationContext::get(CallbackInvoker::class); + + $this->assertInstanceOf(CallbackInvoker::class, $invoker); + } + + public function testMultipleGetCallsReturnSameContainer(): void + { + $container1 = ApplicationContext::getContainer(); + $container2 = ApplicationContext::getContainer(); + + $this->assertSame($container1, $container2); + } + + public function testInitializeWithEmptyOptions(): void + { + $container = ApplicationContext::initialize([]); + + $this->assertInstanceOf(Container::class, $container); + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testInitializeWithNullDefinitions(): void + { + $container = ApplicationContext::initialize(['definitions' => null]); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testInitializeWithEmptyDefinitions(): void + { + $container = ApplicationContext::initialize(['definitions' => []]); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testSetBeforeInitializeAutoInitializes(): void + { + // Don't call initialize + ApplicationContext::set('test.service', 'value'); + + $this->assertEquals('value', ApplicationContext::get('test.service')); + } + + public function testHasBeforeInitializeAutoInitializes(): void + { + // Don't call initialize, should auto-initialize + $result = ApplicationContext::has(Container::class); + + $this->assertTrue($result); + } + + public function testGetBeforeInitializeAutoInitializes(): void + { + // Don't call initialize + $container = ApplicationContext::get(Container::class); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testInvokerSingletonConsistency(): void + { + // Test fix for issue #3 and #4 - Invoker instance consistency + ApplicationContext::initialize(); + + $invoker1 = ApplicationContext::getInvoker(); + $invoker2 = ApplicationContext::get(CallbackInvoker::class); + + // Both should return the same instance + $this->assertSame($invoker1, $invoker2); + } + + public function testInvokerSingletonConsistencyAfterReset(): void + { + ApplicationContext::initialize(); + $invoker1 = ApplicationContext::getInvoker(); + + ApplicationContext::reset(); + ApplicationContext::initialize(); + + $invoker2 = ApplicationContext::getInvoker(); + $invoker3 = ApplicationContext::get(CallbackInvoker::class); + + // After reset, should be new instance + $this->assertNotSame($invoker1, $invoker2); + // But getInvoker() and container should still be consistent + $this->assertSame($invoker2, $invoker3); + } +} diff --git a/tests/StaticPHP/DI/CallbackInvokerTest.php b/tests/StaticPHP/DI/CallbackInvokerTest.php new file mode 100644 index 000000000..751a70eb9 --- /dev/null +++ b/tests/StaticPHP/DI/CallbackInvokerTest.php @@ -0,0 +1,629 @@ +container = new Container(); + $this->invoker = new CallbackInvoker($this->container); + } + + public function testInvokeSimpleCallbackWithoutParameters(): void + { + $callback = function () { + return 'result'; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('result', $result); + } + + public function testInvokeCallbackWithContextByTypeName(): void + { + $callback = function (string $param) { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['string' => 'test_value']); + + $this->assertEquals('test_value', $result); + } + + public function testInvokeCallbackWithContextByParameterName(): void + { + $callback = function (string $myParam) { + return $myParam; + }; + + $result = $this->invoker->invoke($callback, ['myParam' => 'test_value']); + + $this->assertEquals('test_value', $result); + } + + public function testInvokeCallbackWithContextByTypeNameTakesPrecedence(): void + { + $callback = function (string $myParam) { + return $myParam; + }; + + // Type name should take precedence over parameter name + $result = $this->invoker->invoke($callback, [ + 'string' => 'by_type', + 'myParam' => 'by_name', + ]); + + $this->assertEquals('by_type', $result); + } + + public function testInvokeCallbackWithContainerResolution(): void + { + $this->container->set('test.service', 'service_value'); + + $callback = function (string $testService) { + return $testService; + }; + + // Should not resolve from container as 'test.service' is not a type + // Will try default value or null + $this->expectException(\RuntimeException::class); + $this->invoker->invoke($callback); + } + + public function testInvokeCallbackWithClassTypeFromContainer(): void + { + $testObject = new \stdClass(); + $testObject->value = 'test'; + $this->container->set(\stdClass::class, $testObject); + + $callback = function (\stdClass $obj) { + return $obj->value; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('test', $result); + } + + public function testInvokeCallbackWithDefaultValue(): void + { + $callback = function (string $param = 'default_value') { + return $param; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('default_value', $result); + } + + public function testInvokeCallbackWithNullableParameter(): void + { + $callback = function (?string $param) { + return $param ?? 'was_null'; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('was_null', $result); + } + + public function testInvokeCallbackThrowsExceptionForUnresolvableParameter(): void + { + $callback = function (string $required) { + return $required; + }; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("Cannot resolve parameter 'required' of type 'string'"); + $this->invoker->invoke($callback); + } + + public function testInvokeCallbackThrowsExceptionForNonExistentClass(): void + { + // This test uses UnresolvableTestClass which has required constructor params + // Container.has() will return true but get() will throw InvalidDefinition + // So we test that container exceptions bubble up + $callback = function (UnresolvableTestClass $obj) { + return $obj; + }; + + $this->expectException(\Throwable::class); + $this->invoker->invoke($callback); + } + + public function testInvokeCallbackWithMultipleParameters(): void + { + $callback = function (string $first, int $second, bool $third) { + return [$first, $second, $third]; + }; + + $result = $this->invoker->invoke($callback, [ + 'first' => 'value1', + 'second' => 42, + 'third' => true, + ]); + + $this->assertEquals(['value1', 42, true], $result); + } + + public function testInvokeCallbackWithMixedResolutionSources(): void + { + $this->container->set(\stdClass::class, new \stdClass()); + + $callback = function ( + \stdClass $fromContainer, + string $fromContext, + int $withDefault = 100 + ) { + return [$fromContainer, $fromContext, $withDefault]; + }; + + $result = $this->invoker->invoke($callback, ['fromContext' => 'context_value']); + + $this->assertInstanceOf(\stdClass::class, $result[0]); + $this->assertEquals('context_value', $result[1]); + $this->assertEquals(100, $result[2]); + } + + public function testExpandContextHierarchyWithObject(): void + { + // Create a simple parent-child relationship + $childClass = new \ArrayObject(['key' => 'value']); + + $callback = function (\ArrayObject $obj) { + return $obj; + }; + + $result = $this->invoker->invoke($callback, [get_class($childClass) => $childClass]); + + $this->assertSame($childClass, $result); + } + + public function testExpandContextHierarchyWithInterface(): void + { + $object = new class implements \Countable { + public function count(): int + { + return 42; + } + }; + + $callback = function (\Countable $countable) { + return $countable->count(); + }; + + $result = $this->invoker->invoke($callback, [get_class($object) => $object]); + + $this->assertEquals(42, $result); + } + + public function testExpandContextHierarchyWithMultipleInterfaces(): void + { + $object = new class implements \Countable, \IteratorAggregate { + public function count(): int + { + return 5; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } + }; + + $callback = function (\Countable $c, \IteratorAggregate $i) { + return [$c->count(), $i]; + }; + + $result = $this->invoker->invoke($callback, ['obj' => $object]); + + $this->assertEquals(5, $result[0]); + $this->assertInstanceOf(\IteratorAggregate::class, $result[1]); + } + + public function testInvokeWithArrayCallback(): void + { + $testClass = new class { + public function method(string $param): string + { + return 'called_' . $param; + } + }; + + $result = $this->invoker->invoke([$testClass, 'method'], ['param' => 'test']); + + $this->assertEquals('called_test', $result); + } + + public function testInvokeWithStaticMethod(): void + { + $testClass = new class { + public static function staticMethod(string $param): string + { + return 'static_' . $param; + } + }; + + $className = get_class($testClass); + $result = $this->invoker->invoke([$className, 'staticMethod'], ['param' => 'value']); + + $this->assertEquals('static_value', $result); + } + + public function testInvokeWithCallableString(): void + { + $callback = 'Tests\StaticPHP\DI\testFunction'; + + if (!function_exists($callback)) { + eval('namespace Tests\StaticPHP\DI; function testFunction(string $param) { return "func_" . $param; }'); + } + + $result = $this->invoker->invoke($callback, ['param' => 'test']); + + $this->assertEquals('func_test', $result); + } + + public function testInvokeWithNoTypeHintedParameter(): void + { + $callback = function ($param) { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['param' => 'value']); + + $this->assertEquals('value', $result); + } + + public function testInvokeWithNoTypeHintedParameterReturnsNull(): void + { + // Parameters without type hints are implicitly nullable in PHP + $callback = function ($param) { + return $param; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertNull($result); + } + + public function testInvokeWithNoTypeHintAndValueInContext(): void + { + $callback = function ($param) { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['param' => 'value']); + + $this->assertEquals('value', $result); + } + + public function testInvokeWithBuiltinTypes(): void + { + $callback = function ( + string $str, + int $num, + float $decimal, + bool $flag, + array $arr + ) { + return compact('str', 'num', 'decimal', 'flag', 'arr'); + }; + + $result = $this->invoker->invoke($callback, [ + 'str' => 'test', + 'num' => 42, + 'decimal' => 3.14, + 'flag' => true, + 'arr' => [1, 2, 3], + ]); + + $this->assertEquals([ + 'str' => 'test', + 'num' => 42, + 'decimal' => 3.14, + 'flag' => true, + 'arr' => [1, 2, 3], + ], $result); + } + + public function testInvokeWithEmptyContext(): void + { + $callback = function () { + return 'no_params'; + }; + + $result = $this->invoker->invoke($callback, []); + + $this->assertEquals('no_params', $result); + } + + public function testInvokePreservesCallbackReturnValue(): void + { + $callback = function () { + return ['key' => 'value', 'number' => 123]; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals(['key' => 'value', 'number' => 123], $result); + } + + public function testInvokeWithNullReturnValue(): void + { + $callback = function () { + return null; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertNull($result); + } + + public function testInvokeWithObjectInContext(): void + { + $obj = new \stdClass(); + $obj->value = 'test'; + + $callback = function (\stdClass $param) { + return $param->value; + }; + + $result = $this->invoker->invoke($callback, ['param' => $obj]); + + $this->assertEquals('test', $result); + } + + public function testInvokeWithInheritanceInContext(): void + { + $exception = new \RuntimeException('test message'); + + $callback = function (\Exception $e) { + return $e->getMessage(); + }; + + // RuntimeException should be resolved as Exception via hierarchy expansion + $result = $this->invoker->invoke($callback, ['exc' => $exception]); + + $this->assertEquals('test message', $result); + } + + public function testInvokeContextValueOverridesContainer(): void + { + $containerObj = new \stdClass(); + $containerObj->source = 'container'; + $this->container->set(\stdClass::class, $containerObj); + + $contextObj = new \stdClass(); + $contextObj->source = 'context'; + + $callback = function (\stdClass $obj) { + return $obj->source; + }; + + // Context should override container + $result = $this->invoker->invoke($callback, [\stdClass::class => $contextObj]); + + $this->assertEquals('context', $result); + } + + public function testInvokeWithDefaultValueNotUsedWhenContextProvided(): void + { + $callback = function (string $param = 'default') { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['param' => 'from_context']); + + $this->assertEquals('from_context', $result); + } + + public function testInvokeWithMixedNullableAndRequired(): void + { + $callback = function (string $required, ?string $optional) { + return [$required, $optional]; + }; + + $result = $this->invoker->invoke($callback, ['required' => 'value']); + + $this->assertEquals(['value', null], $result); + } + + public function testInvokeWithComplexObjectHierarchy(): void + { + // Use built-in PHP classes with inheritance + // ArrayIterator extends IteratorIterator implements ArrayAccess, SeekableIterator, Countable, Serializable + $arrayIterator = new \ArrayIterator(['test' => 'value']); + + // Test that the object can be resolved via interface (Countable) + $callback1 = function (\Countable $test) { + return $test->count(); + }; + + $result1 = $this->invoker->invoke($callback1, ['obj' => $arrayIterator]); + $this->assertEquals(1, $result1); + + // Test that the object can be resolved via another interface (Iterator) + $callback2 = function (\Iterator $test) { + return $test; + }; + + $result2 = $this->invoker->invoke($callback2, ['obj' => $arrayIterator]); + $this->assertInstanceOf(\ArrayIterator::class, $result2); + + // Test that the object can be resolved via concrete class + $callback3 = function (\ArrayIterator $test) { + return $test; + }; + + $result3 = $this->invoker->invoke($callback3, ['obj' => $arrayIterator]); + $this->assertSame($arrayIterator, $result3); + } + + public function testInvokeWithNonObjectContextValues(): void + { + $callback = function (string $str, int $num, array $arr, bool $flag) { + return compact('str', 'num', 'arr', 'flag'); + }; + + $context = [ + 'str' => 'hello', + 'num' => 999, + 'arr' => ['a', 'b'], + 'flag' => false, + ]; + + $result = $this->invoker->invoke($callback, $context); + + $this->assertEquals($context, $result); + } + + public function testInvokeParameterOrderMatters(): void + { + $callback = function (string $first, string $second, string $third) { + return [$first, $second, $third]; + }; + + $result = $this->invoker->invoke($callback, [ + 'first' => 'A', + 'second' => 'B', + 'third' => 'C', + ]); + + $this->assertEquals(['A', 'B', 'C'], $result); + } + + public function testInvokeWithUnionTypeThrowsException(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Union types require PHP 8.0+'); + } + + $callback = eval('return function (string|int $param) { return $param; };'); + + // Union types are not ReflectionNamedType, should not be resolved from container + $this->expectException(\RuntimeException::class); + $this->invoker->invoke($callback); + } + + public function testInvokeWithCallableType(): void + { + $callback = function (callable $fn) { + return $fn(); + }; + + $result = $this->invoker->invoke($callback, [ + 'fn' => fn () => 'called', + ]); + + $this->assertEquals('called', $result); + } + + public function testInvokeWithIterableType(): void + { + $callback = function (iterable $items) { + $result = []; + foreach ($items as $item) { + $result[] = $item; + } + return $result; + }; + + $result = $this->invoker->invoke($callback, [ + 'items' => [1, 2, 3], + ]); + + $this->assertEquals([1, 2, 3], $result); + } + + public function testInvokeWithObjectType(): void + { + $callback = function (object $obj) { + return get_class($obj); + }; + + $testObj = new \stdClass(); + $result = $this->invoker->invoke($callback, ['obj' => $testObj]); + + $this->assertEquals('stdClass', $result); + } + + public function testInvokeWithContainerExceptionFallsThrough(): void + { + // Test fix for issue #1 - Container exceptions should be caught + // and fall through to other resolution strategies + $callback = function (?UnresolvableTestClass $obj = null) { + return $obj; + }; + + // Should use default value (null) instead of throwing container exception + $result = $this->invoker->invoke($callback); + + $this->assertNull($result); + } + + public function testInvokeWithContainerExceptionAndNoFallback(): void + { + // When there's no fallback (no default, not nullable), should throw RuntimeException + $callback = function (UnresolvableTestClass $obj) { + return $obj; + }; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("Cannot resolve parameter 'obj'"); + + $this->invoker->invoke($callback); + } + + public function testExpandContextHierarchyPerformance(): void + { + // Test fix for issue #2 - Should not create duplicate ReflectionClass + // This is more of a code quality test, ensuring the fix doesn't break functionality + $obj = new \ArrayIterator(['a', 'b', 'c']); + + $callback = function ( + \ArrayIterator $asArrayIterator, + \Traversable $asTraversable, + \Countable $asCountable + ) { + return [ + get_class($asArrayIterator), + get_class($asTraversable), + get_class($asCountable), + ]; + }; + + $result = $this->invoker->invoke($callback, ['obj' => $obj]); + + $this->assertEquals([ + 'ArrayIterator', + 'ArrayIterator', + 'ArrayIterator', + ], $result); + } +} diff --git a/tests/StaticPHP/Registry/ArtifactLoaderTest.php b/tests/StaticPHP/Registry/ArtifactLoaderTest.php new file mode 100644 index 000000000..75370dfe8 --- /dev/null +++ b/tests/StaticPHP/Registry/ArtifactLoaderTest.php @@ -0,0 +1,440 @@ +tempDir = sys_get_temp_dir() . '/artifact_loader_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset ArtifactLoader and ArtifactConfig state + $reflection = new \ReflectionClass(ArtifactLoader::class); + $property = $reflection->getProperty('artifacts'); + $property->setAccessible(true); + $property->setValue(null, null); + + $configReflection = new \ReflectionClass(ArtifactConfig::class); + $configProperty = $configReflection->getProperty('artifact_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset ArtifactLoader and ArtifactConfig state + $reflection = new \ReflectionClass(ArtifactLoader::class); + $property = $reflection->getProperty('artifacts'); + $property->setAccessible(true); + $property->setValue(null, null); + + $configReflection = new \ReflectionClass(ArtifactConfig::class); + $configProperty = $configReflection->getProperty('artifact_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + public function testInitArtifactInstancesOnlyRunsOnce(): void + { + $this->createTestArtifactConfig('test-artifact'); + + ArtifactLoader::initArtifactInstances(); + ArtifactLoader::initArtifactInstances(); + + // Should only initialize once + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertInstanceOf(Artifact::class, $artifact); + } + + public function testGetArtifactInstanceReturnsNullForNonExistent(): void + { + ArtifactLoader::initArtifactInstances(); + $artifact = ArtifactLoader::getArtifactInstance('non-existent-artifact'); + $this->assertNull($artifact); + } + + public function testGetArtifactInstanceReturnsArtifact(): void + { + $this->createTestArtifactConfig('test-artifact'); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertInstanceOf(Artifact::class, $artifact); + } + + public function testLoadFromClassWithCustomSourceAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('test-artifact')] + public function customSource(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidCustomSourceArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('non-existent-artifact')] + public function customSource(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomSource]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithCustomBinaryAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomBinary('test-artifact', ['linux-x86_64', 'macos-aarch64'])] + public function customBinary(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidCustomBinaryArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomBinary('non-existent-artifact', ['linux-x86_64'])] + public function customBinary(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomBinary]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithSourceExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[SourceExtract('test-artifact')] + public function sourceExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidSourceExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[SourceExtract('non-existent-artifact')] + public function sourceExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[SourceExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithBinaryExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[BinaryExtract('test-artifact')] + public function binaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassWithBinaryExtractAttributeAndPlatforms(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[BinaryExtract('test-artifact', platforms: ['linux-x86_64', 'darwin-aarch64'])] + public function binaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidBinaryExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[BinaryExtract('non-existent-artifact')] + public function binaryExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[BinaryExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithAfterSourceExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterSourceExtract('test-artifact')] + public function afterSourceExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidAfterSourceExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterSourceExtract('non-existent-artifact')] + public function afterSourceExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterSourceExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithAfterBinaryExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterBinaryExtract('test-artifact')] + public function afterBinaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassWithAfterBinaryExtractAttributeAndPlatforms(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterBinaryExtract('test-artifact', platforms: ['linux-x86_64'])] + public function afterBinaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidAfterBinaryExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterBinaryExtract('non-existent-artifact')] + public function afterBinaryExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterBinaryExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithMultipleAttributes(): void + { + $this->createTestArtifactConfig('test-artifact-1'); + $this->createTestArtifactConfig('test-artifact-2'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('test-artifact-1')] + public function customSource(): void {} + + #[CustomBinary('test-artifact-2', ['linux-x86_64'])] + public function customBinary(): void {} + + #[SourceExtract('test-artifact-1')] + public function sourceExtract(): void {} + + #[AfterSourceExtract('test-artifact-2')] + public function afterSourceExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact1 = ArtifactLoader::getArtifactInstance('test-artifact-1'); + $artifact2 = ArtifactLoader::getArtifactInstance('test-artifact-2'); + $this->assertNotNull($artifact1); + $this->assertNotNull($artifact2); + } + + public function testLoadFromClassIgnoresNonPublicMethods(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('test-artifact')] + public function publicCustomSource(): void {} + + #[CustomSource('test-artifact')] + private function privateCustomSource(): void {} + + #[CustomSource('test-artifact')] + protected function protectedCustomSource(): void {} + }; + + // Should only process public method + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromPsr4DirLoadsAllClasses(): void + { + $this->createTestArtifactConfig('test-artifact'); + + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/ArtifactClasses'; + mkdir($psr4Dir, 0755, true); + + // Create test class file + $classContent = 'assertNotNull($artifact); + } + + public function testLoadFromClassWithNoAttributes(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + public function regularMethod(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + // Verify no side effects + $this->assertTrue(true); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } + + private function createTestArtifactConfig(string $name): void + { + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $configs = $property->getValue(); + $configs[$name] = [ + 'type' => 'source', + 'url' => 'https://example.com/test.tar.gz', + ]; + $property->setValue(null, $configs); + } +} diff --git a/tests/StaticPHP/Registry/DoctorLoaderTest.php b/tests/StaticPHP/Registry/DoctorLoaderTest.php new file mode 100644 index 000000000..7817a880b --- /dev/null +++ b/tests/StaticPHP/Registry/DoctorLoaderTest.php @@ -0,0 +1,374 @@ +tempDir = sys_get_temp_dir() . '/doctor_loader_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset DoctorLoader state + $reflection = new \ReflectionClass(DoctorLoader::class); + $property = $reflection->getProperty('doctor_items'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('fix_items'); + $property->setAccessible(true); + $property->setValue(null, []); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset DoctorLoader state + $reflection = new \ReflectionClass(DoctorLoader::class); + $property = $reflection->getProperty('doctor_items'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('fix_items'); + $property->setAccessible(true); + $property->setValue(null, []); + } + + public function testGetDoctorItemsReturnsEmptyArrayInitially(): void + { + $this->assertEmpty(DoctorLoader::getDoctorItems()); + } + + public function testLoadFromClassWithCheckItemAttribute(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + $this->assertInstanceOf(CheckItem::class, $items[0][0]); + $this->assertEquals('test-check', $items[0][0]->item_name); + $this->assertEquals(1, $items[0][0]->level); + } + + public function testLoadFromClassWithMultipleCheckItems(): void + { + $class = new class { + #[CheckItem('check-1', level: 2)] + public function check1(): void {} + + #[CheckItem('check-2', level: 1)] + public function check2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(2, $items); + } + + public function testLoadFromClassSortsByLevelDescending(): void + { + $class = new class { + #[CheckItem('low-priority', level: 1)] + public function lowCheck(): void {} + + #[CheckItem('high-priority', level: 5)] + public function highCheck(): void {} + + #[CheckItem('medium-priority', level: 3)] + public function mediumCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(3, $items); + // Should be sorted by level descending: 5, 3, 1 + $this->assertEquals(5, $items[0][0]->level); + $this->assertEquals(3, $items[1][0]->level); + $this->assertEquals(1, $items[2][0]->level); + } + + public function testLoadFromClassWithoutSorting(): void + { + $class = new class { + #[CheckItem('check-1', level: 1)] + public function check1(): void {} + + #[CheckItem('check-2', level: 5)] + public function check2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class), false); + + $items = DoctorLoader::getDoctorItems(); + // Without sorting, items should be in order they were added + $this->assertCount(2, $items); + } + + public function testLoadFromClassWithFixItemAttribute(): void + { + $class = new class { + #[FixItem('test-fix')] + public function testFix(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $fixItem = DoctorLoader::getFixItem('test-fix'); + $this->assertNotNull($fixItem); + $this->assertTrue(is_callable($fixItem)); + } + + public function testLoadFromClassWithMultipleFixItems(): void + { + $class = new class { + #[FixItem('fix-1')] + public function fix1(): void {} + + #[FixItem('fix-2')] + public function fix2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $this->assertNotNull(DoctorLoader::getFixItem('fix-1')); + $this->assertNotNull(DoctorLoader::getFixItem('fix-2')); + } + + public function testGetFixItemReturnsNullForNonExistent(): void + { + $this->assertNull(DoctorLoader::getFixItem('non-existent-fix')); + } + + public function testLoadFromClassWithOptionalCheckOnClass(): void + { + // Note: OptionalCheck expects an array, not a callable directly + // This test verifies the structure even though we can't easily test with anonymous classes + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + // Second element is the optional check callback (null if not set) + $this->assertIsArray($items[0]); + } + + public function testLoadFromClassWithOptionalCheckOnMethod(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + } + + public function testLoadFromClassSetsCallbackCorrectly(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): string + { + return 'test-result'; + } + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + + // Test that the callback is set correctly + $callback = $items[0][0]->callback; + $this->assertIsCallable($callback); + $this->assertEquals('test-result', call_user_func($callback)); + } + + public function testLoadFromClassWithBothCheckAndFixItems(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + + #[FixItem('test-fix')] + public function testFix(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $checkItems = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $checkItems); + + $fixItem = DoctorLoader::getFixItem('test-fix'); + $this->assertNotNull($fixItem); + } + + public function testLoadFromClassMultipleTimesAccumulatesItems(): void + { + $class1 = new class { + #[CheckItem('check-1', level: 1)] + public function check1(): void {} + }; + + $class2 = new class { + #[CheckItem('check-2', level: 2)] + public function check2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class1)); + DoctorLoader::loadFromClass(get_class($class2)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(2, $items); + } + + public function testLoadFromPsr4DirLoadsAllClasses(): void + { + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/DoctorClasses'; + mkdir($psr4Dir, 0755, true); + + // Create test class file 1 + $classContent1 = 'assertGreaterThanOrEqual(0, count($items)); + } + + public function testLoadFromPsr4DirSortsItemsByLevel(): void + { + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/DoctorClasses'; + mkdir($psr4Dir, 0755, true); + + $classContent = '= 2) { + $this->assertGreaterThanOrEqual($items[1][0]->level, $items[0][0]->level); + } + } + + public function testLoadFromClassIgnoresNonPublicMethods(): void + { + $class = new class { + #[CheckItem('public-check', level: 1)] + public function publicCheck(): void {} + + #[CheckItem('private-check', level: 1)] + private function privateCheck(): void {} + + #[CheckItem('protected-check', level: 1)] + protected function protectedCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + // Should only load public methods + $this->assertCount(1, $items); + $this->assertEquals('public-check', $items[0][0]->item_name); + } + + public function testLoadFromClassWithNoAttributes(): void + { + $class = new class { + public function regularMethod(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + // Should not add any items + $items = DoctorLoader::getDoctorItems(); + $this->assertEmpty($items); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/tests/StaticPHP/Registry/PackageLoaderTest.php b/tests/StaticPHP/Registry/PackageLoaderTest.php new file mode 100644 index 000000000..7228b5ae8 --- /dev/null +++ b/tests/StaticPHP/Registry/PackageLoaderTest.php @@ -0,0 +1,550 @@ +tempDir = sys_get_temp_dir() . '/package_loader_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset PackageLoader state + $reflection = new \ReflectionClass(PackageLoader::class); + + $property = $reflection->getProperty('packages'); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('after_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('loaded_classes'); + $property->setAccessible(true); + $property->setValue(null, []); + + // Reset PackageConfig state + $configReflection = new \ReflectionClass(PackageConfig::class); + $configProperty = $configReflection->getProperty('package_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset PackageLoader state + $reflection = new \ReflectionClass(PackageLoader::class); + + $property = $reflection->getProperty('packages'); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('after_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('loaded_classes'); + $property->setAccessible(true); + $property->setValue(null, []); + + // Reset PackageConfig state + $configReflection = new \ReflectionClass(PackageConfig::class); + $configProperty = $configReflection->getProperty('package_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + public function testInitPackageInstancesOnlyRunsOnce(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + + PackageLoader::initPackageInstances(); + PackageLoader::initPackageInstances(); + + // Should only initialize once + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testInitPackageInstancesCreatesLibraryPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-lib'); + $this->assertInstanceOf(LibraryPackage::class, $package); + } + + public function testInitPackageInstancesCreatesPhpExtensionPackage(): void + { + $this->createTestPackageConfig('test-ext', 'php-extension'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-ext'); + $this->assertInstanceOf(PhpExtensionPackage::class, $package); + } + + public function testInitPackageInstancesCreatesTargetPackage(): void + { + $this->createTestPackageConfig('test-target', 'target'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-target'); + $this->assertInstanceOf(TargetPackage::class, $package); + } + + public function testInitPackageInstancesCreatesVirtualTargetPackage(): void + { + $this->createTestPackageConfig('test-virtual-target', 'virtual-target'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-virtual-target'); + $this->assertInstanceOf(TargetPackage::class, $package); + } + + public function testInitPackageInstancesThrowsExceptionForUnknownType(): void + { + $this->createTestPackageConfig('test-unknown', 'unknown-type'); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('has unknown type'); + + PackageLoader::initPackageInstances(); + } + + public function testHasPackageReturnsTrueForExistingPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testHasPackageReturnsFalseForNonExistingPackage(): void + { + PackageLoader::initPackageInstances(); + + $this->assertFalse(PackageLoader::hasPackage('non-existent')); + } + + public function testGetPackageReturnsPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-lib'); + $this->assertInstanceOf(LibraryPackage::class, $package); + } + + public function testGetPackageThrowsExceptionForNonExistingPackage(): void + { + PackageLoader::initPackageInstances(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('not found'); + + PackageLoader::getPackage('non-existent'); + } + + public function testGetTargetPackageReturnsTargetPackage(): void + { + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getTargetPackage('test-target'); + $this->assertInstanceOf(TargetPackage::class, $package); + } + + public function testGetTargetPackageThrowsExceptionForNonTargetPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('is not a TargetPackage'); + + PackageLoader::getTargetPackage('test-lib'); + } + + public function testGetLibraryPackageReturnsLibraryPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getLibraryPackage('test-lib'); + $this->assertInstanceOf(LibraryPackage::class, $package); + } + + public function testGetLibraryPackageThrowsExceptionForNonLibraryPackage(): void + { + $this->createTestPackageConfig('ext-test-ext', 'php-extension'); + PackageLoader::initPackageInstances(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('is not a LibraryPackage'); + + PackageLoader::getLibraryPackage('ext-test-ext'); + } + + public function testGetPackagesReturnsAllPackages(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + $this->createTestPackageConfig('test-ext', 'php-extension'); + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $packages = iterator_to_array(PackageLoader::getPackages()); + $this->assertCount(3, $packages); + } + + public function testGetPackagesWithTypeFilterReturnsFilteredPackages(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + $this->createTestPackageConfig('test-ext', 'php-extension'); + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $packages = iterator_to_array(PackageLoader::getPackages('library')); + $this->assertCount(1, $packages); + $this->assertArrayHasKey('test-lib', $packages); + } + + public function testGetPackagesWithArrayTypeFilterReturnsFilteredPackages(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + $this->createTestPackageConfig('test-ext', 'php-extension'); + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $packages = iterator_to_array(PackageLoader::getPackages(['library', 'target'])); + $this->assertCount(2, $packages); + $this->assertArrayHasKey('test-lib', $packages); + $this->assertArrayHasKey('test-target', $packages); + } + + public function testLoadFromClassWithLibraryAttribute(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $class = new #[Library('test-lib')] class {}; + + PackageLoader::loadFromClass(get_class($class)); + + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testLoadFromClassWithExtensionAttribute(): void + { + $this->createTestPackageConfig('ext-test-ext', 'php-extension'); + PackageLoader::initPackageInstances(); + + $class = new #[Extension('ext-test-ext')] class {}; + + PackageLoader::loadFromClass(get_class($class)); + + $this->assertTrue(PackageLoader::hasPackage('ext-test-ext')); + } + + public function testLoadFromClassWithTargetAttribute(): void + { + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $class = new #[Target('test-target')] class {}; + + PackageLoader::loadFromClass(get_class($class)); + + $this->assertTrue(PackageLoader::hasPackage('test-target')); + } + + public function testLoadFromClassThrowsExceptionForUndefinedPackage(): void + { + PackageLoader::initPackageInstances(); + + $class = new #[Library('undefined-lib')] class {}; + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('not defined in config'); + + PackageLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassThrowsExceptionForTypeMismatch(): void + { + $this->createTestPackageConfig('ext-test-lib', 'library'); + PackageLoader::initPackageInstances(); + + // Try to load with Extension attribute but config says library + $class = new #[Extension('ext-test-lib')] class {}; + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('type mismatch'); + + PackageLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassSkipsDuplicateClasses(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $className = get_class(new #[Library('test-lib')] class {}); + + // Load twice + PackageLoader::loadFromClass($className); + PackageLoader::loadFromClass($className); + + // Should not throw exception + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testLoadFromClassWithNoPackageAttribute(): void + { + PackageLoader::initPackageInstances(); + + $class = new class { + public function regularMethod(): void {} + }; + + // Should not throw exception + PackageLoader::loadFromClass(get_class($class)); + + // Verify no side effects + $this->assertTrue(true); + } + + public function testCheckLoadedStageEventsThrowsExceptionForUnknownPackage(): void + { + PackageLoader::initPackageInstances(); + + // Manually add a before_stage for non-existent package + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'non-existent-package' => [ + 'stage-name' => [[fn () => null, null]], + ], + ]); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('unknown package'); + + PackageLoader::checkLoadedStageEvents(); + } + + public function testCheckLoadedStageEventsThrowsExceptionForUnknownStage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + // Manually add a before_stage for non-existent stage + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-lib' => [ + 'non-existent-stage' => [[fn () => null, null]], + ], + ]); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('is not registered'); + + PackageLoader::checkLoadedStageEvents(); + } + + public function testCheckLoadedStageEventsThrowsExceptionForUnknownOnlyWhenPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-lib'); + $package->addStage('test-stage', fn () => null); + + // Manually add a before_stage with unknown only_when_package_resolved + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-lib' => [ + 'test-stage' => [[fn () => null, 'non-existent-package']], + ], + ]); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('unknown only_when_package_resolved package'); + + PackageLoader::checkLoadedStageEvents(); + } + + public function testGetBeforeStageCallbacksReturnsCallbacks(): void + { + PackageLoader::initPackageInstances(); + + // Manually add some before_stage callbacks + $callback1 = fn () => 'callback1'; + $callback2 = fn () => 'callback2'; + + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-package' => [ + 'test-stage' => [ + [$callback1, null], + [$callback2, null], + ], + ], + ]); + + $callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('test-package', 'test-stage')); + $this->assertCount(2, $callbacks); + } + + public function testGetAfterStageCallbacksReturnsCallbacks(): void + { + PackageLoader::initPackageInstances(); + + // Manually add some after_stage callbacks + $callback1 = fn () => 'callback1'; + $callback2 = fn () => 'callback2'; + + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('after_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-package' => [ + 'test-stage' => [ + [$callback1, null], + [$callback2, null], + ], + ], + ]); + + $callbacks = PackageLoader::getAfterStageCallbacks('test-package', 'test-stage'); + $this->assertCount(2, $callbacks); + } + + public function testGetBeforeStageCallbacksReturnsEmptyForNonExistentPackage(): void + { + PackageLoader::initPackageInstances(); + + $callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('non-existent', 'stage')); + $this->assertEmpty($callbacks); + } + + public function testGetAfterStageCallbacksReturnsEmptyForNonExistentPackage(): void + { + PackageLoader::initPackageInstances(); + + $callbacks = PackageLoader::getAfterStageCallbacks('non-existent', 'stage'); + $this->assertEmpty($callbacks); + } + + public function testRegisterAllDefaultStagesRegistersForPhpExtensions(): void + { + $this->createTestPackageConfig('test-ext', 'php-extension'); + PackageLoader::initPackageInstances(); + + PackageLoader::registerAllDefaultStages(); + + $package = PackageLoader::getPackage('test-ext'); + $this->assertInstanceOf(PhpExtensionPackage::class, $package); + // Default stages should be registered (we can't easily verify this without accessing internal state) + } + + public function testLoadFromPsr4DirLoadsAllClasses(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/PackageClasses'; + mkdir($psr4Dir, 0755, true); + + // Create test class file + $classContent = 'assertTrue(PackageLoader::hasPackage('test-lib')); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } + + private function createTestPackageConfig(string $name, string $type): void + { + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + $property->setAccessible(true); + $configs = $property->getValue(); + $configs[$name] = [ + 'type' => $type, + 'deps' => [], + ]; + $property->setValue(null, $configs); + } +} diff --git a/tests/StaticPHP/Registry/RegistryTest.php b/tests/StaticPHP/Registry/RegistryTest.php new file mode 100644 index 000000000..a7dbd8761 --- /dev/null +++ b/tests/StaticPHP/Registry/RegistryTest.php @@ -0,0 +1,378 @@ +tempDir = sys_get_temp_dir() . '/registry_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset Registry state + Registry::reset(); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset Registry state + Registry::reset(); + } + + public function testLoadRegistryWithValidJsonFile(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadRegistry($registryFile); + + $this->assertContains('test-registry', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithValidYamlFile(): void + { + $registryFile = $this->tempDir . '/test-registry.yaml'; + $registryContent = "name: test-registry-yaml\npackage:\n config: []"; + file_put_contents($registryFile, $registryContent); + + Registry::loadRegistry($registryFile); + + $this->assertContains('test-registry-yaml', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithValidYmlFile(): void + { + $registryFile = $this->tempDir . '/test-registry.yml'; + $registryContent = "name: test-registry-yml\npackage:\n config: []"; + file_put_contents($registryFile, $registryContent); + + Registry::loadRegistry($registryFile); + + $this->assertContains('test-registry-yml', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryThrowsExceptionForNonExistentFile(): void + { + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Failed to read registry file'); + + Registry::loadRegistry($this->tempDir . '/non-existent.json'); + } + + public function testLoadRegistryThrowsExceptionForUnsupportedFormat(): void + { + $registryFile = $this->tempDir . '/test-registry.txt'; + file_put_contents($registryFile, 'invalid content'); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Unsupported registry file format'); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForInvalidJson(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + file_put_contents($registryFile, 'invalid json content'); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Invalid registry format'); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForMissingName(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'package' => [], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage("Registry 'name' is missing or invalid"); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForEmptyName(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => '', + 'package' => [], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage("Registry 'name' is missing or invalid"); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForNonStringName(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 123, + 'package' => [], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage("Registry 'name' is missing or invalid"); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistrySkipsDuplicateRegistry(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'duplicate-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Load first time + Registry::loadRegistry($registryFile); + $this->assertCount(1, Registry::getLoadedRegistries()); + + // Load second time - should skip + Registry::loadRegistry($registryFile); + $this->assertCount(1, Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithNullRegistries(): void + { + // Should not throw exception when null is passed and env is not set + Registry::loadFromEnvOrOption(null); + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithEmptyString(): void + { + Registry::loadFromEnvOrOption(''); + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithSingleRegistry(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'env-test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadFromEnvOrOption($registryFile); + + $this->assertContains('env-test-registry', Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithMultipleRegistries(): void + { + $registryFile1 = $this->tempDir . '/test-registry-1.json'; + $registryData1 = [ + 'name' => 'env-test-registry-1', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile1, json_encode($registryData1)); + + $registryFile2 = $this->tempDir . '/test-registry-2.json'; + $registryData2 = [ + 'name' => 'env-test-registry-2', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile2, json_encode($registryData2)); + + Registry::loadFromEnvOrOption($registryFile1 . ':' . $registryFile2); + + $this->assertContains('env-test-registry-1', Registry::getLoadedRegistries()); + $this->assertContains('env-test-registry-2', Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionIgnoresNonExistentFiles(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'env-test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Mix existing and non-existing files + Registry::loadFromEnvOrOption($registryFile . ':' . $this->tempDir . '/non-existent.json'); + + // Should only load the existing one + $this->assertCount(1, Registry::getLoadedRegistries()); + $this->assertContains('env-test-registry', Registry::getLoadedRegistries()); + } + + public function testGetLoadedRegistriesReturnsEmptyArrayInitially(): void + { + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testGetLoadedRegistriesReturnsCorrectList(): void + { + $registryFile1 = $this->tempDir . '/test-registry-1.json'; + $registryData1 = [ + 'name' => 'registry-1', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile1, json_encode($registryData1)); + + $registryFile2 = $this->tempDir . '/test-registry-2.json'; + $registryData2 = [ + 'name' => 'registry-2', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile2, json_encode($registryData2)); + + Registry::loadRegistry($registryFile1); + Registry::loadRegistry($registryFile2); + + $loaded = Registry::getLoadedRegistries(); + $this->assertCount(2, $loaded); + $this->assertContains('registry-1', $loaded); + $this->assertContains('registry-2', $loaded); + } + + public function testResetClearsLoadedRegistries(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadRegistry($registryFile); + $this->assertNotEmpty(Registry::getLoadedRegistries()); + + Registry::reset(); + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithAutoloadPath(): void + { + // Create a test autoload file + $autoloadFile = $this->tempDir . '/vendor/autoload.php'; + mkdir(dirname($autoloadFile), 0755, true); + file_put_contents($autoloadFile, 'tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'autoload-test-registry', + 'autoload' => 'vendor/autoload.php', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Should not throw exception + Registry::loadRegistry($registryFile); + + $this->assertContains('autoload-test-registry', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithNonExistentAutoloadPath(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'autoload-missing-test-registry', + 'autoload' => 'vendor/non-existent-autoload.php', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Should throw exception when path doesn't exist + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Path does not exist'); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryWithAbsoluteAutoloadPath(): void + { + // Create a test autoload file with absolute path + $autoloadFile = $this->tempDir . '/vendor/autoload.php'; + mkdir(dirname($autoloadFile), 0755, true); + file_put_contents($autoloadFile, 'tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'absolute-autoload-test-registry', + 'autoload' => $autoloadFile, + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadRegistry($registryFile); + + $this->assertContains('absolute-autoload-test-registry', Registry::getLoadedRegistries()); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/tests/assets/filelist.gz b/tests/assets/filelist.gz deleted file mode 100644 index fb566ebf7cea669b993f6bccd2e0dd962e150dde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2375 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 kP=x3IDXJmFEg6m)Fe1{H-W&i*H diff --git a/tests/bootstrap.php b/tests/bootstrap.php index dcb4316eb..ba9173323 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,13 +1,9 @@ setLevel(LogLevel::ERROR); From 794d92c9d804fbbaedd9a09df833bd40ef9aef83 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 10 Dec 2025 12:54:04 +0800 Subject: [PATCH 071/682] Add early validation for package build and installation requirements --- src/StaticPHP/Package/PackageInstaller.php | 23 ++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 1e5e27e8a..530003970 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -12,6 +12,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; @@ -133,6 +134,9 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): echo PHP_EOL; } + // Early validation: check if packages can be built or installed before downloading + $this->validatePackagesBeforeBuild(); + // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); @@ -199,8 +203,6 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): if ($interactive) { InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } - } elseif ($package->getType() === 'library') { - throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS."); } } } @@ -535,6 +537,23 @@ private function printArrayInfo(array $info): void } } + private function validatePackagesBeforeBuild(): void + { + foreach ($this->packages as $package) { + if ($package->getType() !== 'library') { + continue; + } + $is_to_build = $this->isBuildPackage($package); + $has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build'); + $should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false); + + // Check if package can neither be built nor installed + if (!$is_to_build && !$should_use_binary && !$has_build_stage) { + throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS: " . SystemTarget::getCurrentPlatformString()); + } + } + } + private function performAfterInstallActions(Package $package): void { // ----------- perform post-install actions from extracted .package.{pkg_name}.postinstall.json ----------- From 2901d32ba77a6d1d36f8c4187409b1c44ac66d1e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 10 Dec 2025 13:17:15 +0800 Subject: [PATCH 072/682] Update ApplicationContext and InteractiveTerm to handle null outputs gracefully --- src/StaticPHP/DI/ApplicationContext.php | 2 +- src/StaticPHP/Util/InteractiveTerm.php | 31 +++++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index a845fba27..73ff9f2d4 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -83,7 +83,7 @@ public static function getContainer(): Container * * @param class-string $id Service identifier * - * @return T + * @return null|T */ public static function get(string $id): mixed { diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 479327633..ede87a643 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -7,6 +7,7 @@ use StaticPHP\DI\ApplicationContext; use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use ZM\Logger\ConsoleColor; @@ -16,8 +17,8 @@ class InteractiveTerm public static function notice(string $message, bool $indent = false): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); if ($output->isVerbose()) { logger()->notice(strip_ansi_colors($message)); } else { @@ -27,8 +28,8 @@ public static function notice(string $message, bool $indent = false): void public static function success(string $message, bool $indent = false): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class); + $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)); } else { @@ -38,8 +39,8 @@ public static function success(string $message, bool $indent = false): void public static function plain(string $message): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class); + $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)); } else { @@ -49,8 +50,8 @@ public static function plain(string $message): void public static function info(string $message): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); if (!$output->isVerbose()) { $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green('▶ ') . $message)); } @@ -59,8 +60,8 @@ public static function info(string $message): void public static function error(string $message, bool $indent = true): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); if ($output->isVerbose()) { logger()->error(strip_ansi_colors($message)); } else { @@ -75,15 +76,15 @@ public static function advance(): void public static function setMessage(string $message): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } public static function finish(string $message, bool $status = true): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; $message = $no_ansi ? strip_ansi_colors($message) : $message; - $output = ApplicationContext::get(OutputInterface::class); + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); if ($output->isVerbose()) { if ($status) { logger()->info($message); @@ -104,8 +105,8 @@ public static function finish(string $message, bool $status = true): void public static function indicateProgress(string $message): void { - $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class); + $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)); return; From 458af6ac783bb6dab4d4ab57c8a03e8800e4f1e7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 10 Dec 2025 13:38:25 +0800 Subject: [PATCH 073/682] 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 074/682] 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 a54021bf1966bd9bd92014916c0cb88cc4dd5a1d Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 10 Dec 2025 08:42:28 +0100 Subject: [PATCH 075/682] Apply suggestion from @henderkes --- src/SPC/builder/Extension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index bc475d115..08b403e61 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -576,7 +576,7 @@ protected function addExtensionDependency(string $name, bool $optional = false): protected function getExtraEnv(): array { - return ['CFLAGS' => '']; + return []; } /** From f0b5e4f59eb595a8b9a7d6663932060cba91e64e Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 10 Dec 2025 15:43:24 +0800 Subject: [PATCH 076/682] Fix typo in ncurses.php enable-symlinks option (#994) --- src/SPC/builder/unix/library/ncurses.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/library/ncurses.php b/src/SPC/builder/unix/library/ncurses.php index 27725c3d5..fa63eb3f8 100644 --- a/src/SPC/builder/unix/library/ncurses.php +++ b/src/SPC/builder/unix/library/ncurses.php @@ -29,7 +29,7 @@ protected function build(): void '--without-tests', '--without-dlsym', '--without-debug', - '-enable-symlinks', + '--enable-symlinks', "--bindir={$this->getBinDir()}", "--includedir={$this->getIncludeDir()}", "--libdir={$this->getLibDir()}", From 3c89ce6c7f834d66c65e47c24724d6619c41a0fd Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 10 Dec 2025 17:14:27 +0800 Subject: [PATCH 077/682] Update version to 2.7.10 (#997) --- src/SPC/ConsoleApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 1b1d3e199..7fc56ae3c 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.7.9'; + public const string VERSION = '2.7.10'; public function __construct() { From 2080407283be1a314174065571b58a505ce81386 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 11:35:12 +0800 Subject: [PATCH 078/682] 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 079/682] 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 080/682] 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 081/682] 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 082/682] 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 083/682] 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 084/682] 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 085/682] 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 086/682] 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 087/682] 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 088/682] 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 089/682] 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 090/682] 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 091/682] 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 092/682] 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 093/682] 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 094/682] 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 095/682] 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 096/682] 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'])) { From acd0e2b23a2ad29bb7cb77e25fc0f1e4b6f99698 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 15 Dec 2025 17:00:20 +0800 Subject: [PATCH 097/682] Prepare for skeleton --- skeleton-test.php | 21 ++ src/Package/Artifact/zig.php | 1 + src/StaticPHP/Config/ArtifactConfig.php | 10 +- src/StaticPHP/Config/PackageConfig.php | 10 +- src/StaticPHP/ConsoleApplication.php | 8 +- src/StaticPHP/Registry/Registry.php | 57 +++- src/StaticPHP/Skeleton/ArtifactGenerator.php | 57 ++++ src/StaticPHP/Skeleton/ExecutorGenerator.php | 16 ++ src/StaticPHP/Skeleton/PackageGenerator.php | 283 +++++++++++++++++++ tests/bootstrap.php | 2 +- 10 files changed, 447 insertions(+), 18 deletions(-) create mode 100644 skeleton-test.php create mode 100644 src/StaticPHP/Skeleton/ArtifactGenerator.php create mode 100644 src/StaticPHP/Skeleton/ExecutorGenerator.php create mode 100644 src/StaticPHP/Skeleton/PackageGenerator.php diff --git a/skeleton-test.php b/skeleton-test.php new file mode 100644 index 000000000..689d33854 --- /dev/null +++ b/skeleton-test.php @@ -0,0 +1,21 @@ +addDependency('bar') + ->addStaticLib('libfoo.a', 'unix') + ->addStaticLib('libfoo.a', 'unix') + ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz'])); + +$pkg_config = $package_generator->generateConfig(); +$artifact_config = $artifact_generator->generateConfig(); + +echo "===== pkg.json =====" . PHP_EOL; +echo json_encode($pkg_config, 64|128|256) . PHP_EOL; +echo "===== artifact.json =====" . PHP_EOL; +echo json_encode($artifact_config, 64|128|256) . PHP_EOL; diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 2ac7b454b..a73473954 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -8,6 +8,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomSource; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index d25c6dd1a..e840c0bf7 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -5,12 +5,13 @@ namespace StaticPHP\Config; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\Registry; class ArtifactConfig { private static array $artifact_configs = []; - public static function loadFromDir(string $dir): void + public static function loadFromDir(string $dir, string $registry_name): void { if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config."); @@ -18,18 +19,18 @@ public static function loadFromDir(string $dir): void $files = glob("{$dir}/artifact.*.json"); if (is_array($files)) { foreach ($files as $file) { - self::loadFromFile($file); + self::loadFromFile($file, $registry_name); } } if (file_exists("{$dir}/artifact.json")) { - self::loadFromFile("{$dir}/artifact.json"); + self::loadFromFile("{$dir}/artifact.json", $registry_name); } } /** * Load artifact configurations from a specified JSON file. */ - public static function loadFromFile(string $file): void + public static function loadFromFile(string $file, string $registry_name): void { $content = file_get_contents($file); if ($content === false) { @@ -42,6 +43,7 @@ public static function loadFromFile(string $file): void ConfigValidator::validateAndLintArtifacts(basename($file), $data); foreach ($data as $artifact_name => $config) { self::$artifact_configs[$artifact_name] = $config; + Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $file); } } diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index dc0b3d546..3342dfc78 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -5,6 +5,7 @@ namespace StaticPHP\Config; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\Registry; use StaticPHP\Runtime\SystemTarget; class PackageConfig @@ -15,7 +16,7 @@ class PackageConfig * Load package configurations from a specified directory. * It will look for files matching the pattern 'pkg.*.json' and 'pkg.json'. */ - public static function loadFromDir(string $dir): void + public static function loadFromDir(string $dir, string $registry_name): void { if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); @@ -23,11 +24,11 @@ public static function loadFromDir(string $dir): void $files = glob("{$dir}/pkg.*.json"); if (is_array($files)) { foreach ($files as $file) { - self::loadFromFile($file); + self::loadFromFile($file, $registry_name); } } if (file_exists("{$dir}/pkg.json")) { - self::loadFromFile("{$dir}/pkg.json"); + self::loadFromFile("{$dir}/pkg.json", $registry_name); } } @@ -36,7 +37,7 @@ public static function loadFromDir(string $dir): void * * @param string $file the path to the json package configuration file */ - public static function loadFromFile(string $file): void + public static function loadFromFile(string $file, string $registry_name): void { $content = file_get_contents($file); if ($content === false) { @@ -49,6 +50,7 @@ public static function loadFromFile(string $file): void ConfigValidator::validateAndLintPackages(basename($file), $data); foreach ($data as $pkg_name => $config) { self::$package_configs[$pkg_name] = $config; + Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file); } } diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 0e5371ac4..bd0cfd60a 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -9,6 +9,7 @@ use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\ShellCommand; +use StaticPHP\Command\Dev\SkeletonCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\ExtractCommand; @@ -27,12 +28,12 @@ class ConsoleApplication extends Application public function __construct() { - parent::__construct('static-php-cli', self::VERSION); + parent::__construct('StaticPHP', self::VERSION); require_once ROOT_DIR . '/src/bootstrap.php'; - // check registry - Registry::checkLoadedRegistries(); + // resolve registry + Registry::resolve(); /** * @var string $name @@ -59,6 +60,7 @@ public function __construct() new ShellCommand(), new IsInstalledCommand(), new EnvCommand(), + new SkeletonCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 4ae5df4f4..909de021f 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -13,9 +13,13 @@ class Registry { - /** @var string[] List of loaded registry names */ + /** @var array List of loaded registries */ private static array $loaded_registries = []; + /** @var array Maps of package and artifact names to their registry config file paths (for reverse lookup) */ + private static array $package_reversed_registry_files = []; + private static array $artifact_reversed_registry_files = []; + /** * Load a registry from file path. * This method handles external registries that may not be in composer autoload. @@ -85,9 +89,9 @@ public static function loadRegistry(string $registry_file, bool $auto_require = foreach ($data['package']['config'] as $path) { $path = self::fullpath($path, dirname($registry_file)); if (is_file($path)) { - PackageConfig::loadFromFile($path); + PackageConfig::loadFromFile($path, $registry_name); } elseif (is_dir($path)) { - PackageConfig::loadFromDir($path); + PackageConfig::loadFromDir($path, $registry_name); } } } @@ -97,9 +101,9 @@ public static function loadRegistry(string $registry_file, bool $auto_require = foreach ($data['artifact']['config'] as $path) { $path = self::fullpath($path, dirname($registry_file)); if (is_file($path)) { - ArtifactConfig::loadFromFile($path); + ArtifactConfig::loadFromFile($path, $registry_name); } elseif (is_dir($path)) { - ArtifactConfig::loadFromDir($path); + ArtifactConfig::loadFromDir($path, $registry_name); } } } @@ -187,7 +191,12 @@ public static function loadFromEnvOrOption(?string $registries = null): void } } - public static function checkLoadedRegistries(): void + /** + * Resolve loaded registries. + * This method finalizes the loading process by registering default stages + * and validating stage events. + */ + public static function resolve(): void { // Register default stages for all PhpExtensionPackage instances // This must be done after all registries are loaded to ensure custom stages take precedence @@ -217,6 +226,42 @@ public static function reset(): void self::$loaded_registries = []; } + /** + * Bind a package name to its registry config file for reverse lookup. + * + * @internal + */ + public static function _bindPackageConfigFile(string $package_name, string $registry_name, string $config_file): void + { + self::$package_reversed_registry_files[$package_name] = [ + 'registry' => $registry_name, + 'config' => $config_file, + ]; + } + + /** + * Bind an artifact name to its registry config file for reverse lookup. + * + * @internal + */ + public static function _bindArtifactConfigFile(string $artifact_name, string $registry_name, string $config_file): void + { + self::$artifact_reversed_registry_files[$artifact_name] = [ + 'registry' => $registry_name, + 'config' => $config_file, + ]; + } + + public static function getPackageConfigInfo(string $package_name): ?array + { + return self::$package_reversed_registry_files[$package_name] ?? null; + } + + public static function getArtifactConfigInfo(string $artifact_name): ?array + { + return self::$artifact_reversed_registry_files[$artifact_name] ?? null; + } + /** * Parse a class entry from the classes array. * Supports two formats: diff --git a/src/StaticPHP/Skeleton/ArtifactGenerator.php b/src/StaticPHP/Skeleton/ArtifactGenerator.php new file mode 100644 index 000000000..031d095bb --- /dev/null +++ b/src/StaticPHP/Skeleton/ArtifactGenerator.php @@ -0,0 +1,57 @@ +name; + } + + public function setSource(array $source): static + { + $clone = clone $this; + $clone->source = $source; + return $clone; + } + + public function setCustomSource(): static + { + $clone = clone $this; + $clone->source = ['type' => 'custom']; + $clone->generate_class = true; + $clone->generate_custom_source_func = true; + return $clone; + } + + public function getSource(): ?array + { + return $this->source; + } + + public function generateConfig(): array + { + $config = []; + + if ($this->source) { + $config['source'] = $this->source; + } + return $config; + } +} diff --git a/src/StaticPHP/Skeleton/ExecutorGenerator.php b/src/StaticPHP/Skeleton/ExecutorGenerator.php new file mode 100644 index 000000000..02edf2ebd --- /dev/null +++ b/src/StaticPHP/Skeleton/ExecutorGenerator.php @@ -0,0 +1,16 @@ + $depends An array of dependencies required by the package, categorized by operating system. */ + protected array $depends = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */ + protected array $suggests = []; + + /** @var array $frameworks An array of macOS frameworks for the package */ + protected array $frameworks = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */ + protected array $static_libs = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $headers An array of header files required by the package, categorized by operating system. */ + protected array $headers = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */ + protected array $static_bins = []; + + /** @var ArtifactGenerator|null $artifact Artifact */ + protected ?ArtifactGenerator $artifact = null; + + /** @var array $licenses Licenses */ + protected array $licenses = []; + + /** @var array<'Darwin'|'Linux'|'Windows', null|string> $build_for_enables Enable build function generating */ + protected array $build_for_enables = [ + 'Darwin' => null, + 'Linux' => null, + 'Windows' => null, + ]; + + /** @var array */ + protected array $func_executor_binding = []; + + /** + * @param string $package_name Package name + * @param 'library'|'target'|'virtual-target'|'php-extension' $type Package type ('library', 'target', 'virtual-target', etc.) + */ + public function __construct(protected string $package_name, protected string $type) {} + + /** + * Add package dependency. + * + * @param string $package Package name + * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos') + */ + public function addDependency(string $package, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->depends[$os])) { + $clone->depends[$os] = []; + } + if (!in_array($package, $clone->depends[$os], true)) { + $clone->depends[$os][] = $package; + } + return $clone; + } + + /** + * Add package suggestion. + * + * @param string $package Package name + * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos') + */ + public function addSuggestion(string $package, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->suggests[$os])) { + $clone->suggests[$os] = []; + } + if (!in_array($package, $clone->suggests[$os], true)) { + $clone->suggests[$os][] = $package; + } + return $clone; + } + + public function addStaticLib(string $lib_a, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + if (!str_ends_with($lib_a, '.lib') && !str_ends_with($lib_a, '.a')) { + throw new ValidationException("Static library must end with .lib or .a, got: {$lib_a}"); + } + if (str_ends_with($lib_a, '.lib') && in_array($os, ['unix', 'linux', 'macos'], true)) { + throw new ValidationException("Static library with .lib extension cannot be added for non-Windows OS: {$lib_a}"); + } + if (str_ends_with($lib_a, '.a') && $os === 'windows') { + throw new ValidationException("Static library with .a extension cannot be added for Windows OS: {$lib_a}"); + } + if (isset($this->static_libs[$os]) && in_array($lib_a, $this->static_libs[$os], true)) { + // already exists + return $this; + } + $clone = clone $this; + if (!isset($clone->static_libs[$os])) { + $clone->static_libs[$os] = []; + } + if (!in_array($lib_a, $clone->static_libs[$os], true)) { + $clone->static_libs[$os][] = $lib_a; + } + return $clone; + } + + public function addHeader(string $header_file, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->headers[$os])) { + $clone->headers[$os] = []; + } + if (!in_array($header_file, $clone->headers[$os], true)) { + $clone->headers[$os][] = $header_file; + } + return $clone; + } + + public function addStaticBin(string $bin_file, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->static_bins[$os])) { + $clone->static_bins[$os] = []; + } + if (!in_array($bin_file, $clone->static_bins[$os], true)) { + $clone->static_bins[$os][] = $bin_file; + } + return $clone; + } + + /** + * Add package artifact. + * + * @param ArtifactGenerator $artifactGenerator Artifact generator + */ + public function addArtifact(ArtifactGenerator $artifactGenerator): static + { + $clone = clone $this; + $clone->artifact = $artifactGenerator; + return $clone; + } + + /** + * Add license from string. + * + * @param string $text License content + */ + public function addLicenseFromString(string $text): static + { + $clone = clone $this; + $clone->licenses[] = [ + 'type' => 'text', + 'text' => $text, + ]; + return $clone; + } + + /** + * Add license from file. + * + * @param string $file_path License file path + */ + public function addLicenseFromFile(string $file_path): static + { + $clone = clone $this; + $clone->licenses[] = [ + 'type' => 'file', + 'path' => $file_path, + ]; + return $clone; + } + + /** + * Enable build for specific OS. + * + * @param 'Windows'|'Linux'|'Darwin'|array<'Windows'|'Linux'|'Darwin'> $build_for Build for OS + */ + public function enableBuild(string|array $build_for, ?string $build_function_name = null): static + { + $clone = clone $this; + if (is_array($build_for)) { + foreach ($build_for as $bf) { + $clone = $clone->enableBuild($bf, $build_function_name ?? 'build'); + } + return $clone; + } + $clone->build_for_enables[$build_for] = $build_function_name ?? "buildFor{$build_for}"; + return $clone; + } + + /** + * Bind function executor. + * + * @param string $func_name Function name + * @param ExecutorGenerator $executor Executor generator + */ + public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator $executor): static + { + $clone = clone $this; + $clone->func_executor_binding[$func_name] = $executor; + return $clone; + } + + /** + * Generate package config + */ + public function generateConfig(): array + { + $config = ['type' => $this->type]; + + // Add dependencies + foreach ($this->depends as $suffix => $depends) { + $k = $suffix !== '' ? "depends@{$suffix}" : 'depends'; + $config[$k] = $depends; + } + + // add suggests + foreach ($this->suggests as $suffix => $suggests) { + $k = $suffix !== '' ? "suggests@{$suffix}" : 'suggests'; + $config[$k] = $suggests; + } + + // Add frameworks + if (!empty($this->frameworks)) { + $config['frameworks'] = $this->frameworks; + } + + // Add static libs + foreach ($this->static_libs as $suffix => $libs) { + $k = $suffix !== '' ? "static-libs@{$suffix}" : 'static-libs'; + $config[$k] = $libs; + } + + // Add headers + foreach ($this->headers as $suffix => $headers) { + $k = $suffix !== '' ? "headers@{$suffix}" : 'headers'; + $config[$k] = $headers; + } + + // Add static bins + foreach ($this->static_bins as $suffix => $bins) { + $k = $suffix !== '' ? "static-bins@{$suffix}" : 'static-bins'; + $config[$k] = $bins; + } + + // Add artifact + if ($this->artifact !== null) { + $config['artifact'] = $this->artifact->getName(); + } + + // Add licenses + if (!empty($this->licenses)) { + if (count($this->licenses) === 1) { + $config['license'] = $this->licenses[0]; + } else { + $config['license'] = $this->licenses; + } + } + + return $config; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ba9173323..14397d038 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,6 +4,6 @@ use Psr\Log\LogLevel; require_once __DIR__ . '/../src/bootstrap.php'; -\StaticPHP\Registry\Registry::checkLoadedRegistries(); +\StaticPHP\Registry\Registry::resolve(); logger()->setLevel(LogLevel::ERROR); From d064e1353cf7dd8e97a769267fc9a804e38169f1 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 15 Dec 2025 18:50:20 +0100 Subject: [PATCH 098/682] the libwebp 1.6.0 bug affects centos 7 too --- src/SPC/builder/extension/memcache.php | 5 +++++ src/SPC/builder/unix/library/libwebp.php | 8 ++++++-- src/globals/test-extensions.php | 14 +++++++------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/SPC/builder/extension/memcache.php b/src/SPC/builder/extension/memcache.php index b63fa47a4..59c6065d4 100644 --- a/src/SPC/builder/extension/memcache.php +++ b/src/SPC/builder/extension/memcache.php @@ -43,4 +43,9 @@ public function patchBeforeBuildconf(): bool ); return true; } + + protected function getExtraEnv(): array + { + return ['CFLAGS' => '-std=c17']; + } } diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 54f9e7847..47fd0078c 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -5,12 +5,16 @@ namespace SPC\builder\unix\library; use SPC\util\executor\UnixCMakeExecutor; -use SPC\util\SPCTarget; trait libwebp { protected function build(): void { + $code = 'int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }'; + $cc = getenv('CC') ?: 'gcc'; + [$ret] = shell()->execWithResult("echo '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1"); + $disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64'; + UnixCMakeExecutor::create($this) ->addConfigureArgs( '-DWEBP_BUILD_EXTRAS=OFF', @@ -23,7 +27,7 @@ protected function build(): void '-DWEBP_BUILD_WEBPINFO=OFF', '-DWEBP_BUILD_WEBPMUX=OFF', '-DWEBP_BUILD_FUZZTEST=OFF', - SPCTarget::getLibcVersion() === '2.31' && GNU_ARCH === 'x86_64' ? '-DWEBP_ENABLE_SIMD=OFF' : '' // fix an edge bug for debian 11 with gcc 10 + $disableAvx2 ? '-DWEBP_ENABLE_SIMD=OFF' : '' ) ->build(); // patch pkgconfig diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 8a30b008e..72e751244 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -16,19 +16,19 @@ // '8.1', // '8.2', // '8.3', - '8.4', + // '8.4', '8.5', // 'git', ]; // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-15-intel', // bin/spc for x86_64 - 'macos-15', // bin/spc for arm64 + // 'macos-15-intel', // bin/spc for x86_64 + // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 // 'windows-2025', @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'bcmath,xsl,xml', + 'Linux', 'Darwin' => 'imagick', 'Windows' => 'bcmath', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => '', + 'Linux', 'Darwin' => 'libwebp', 'Windows' => '', }; From 1707c679e8b8cd3f5face5073b81d43bbff8db08 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 18 Dec 2025 15:32:50 +0800 Subject: [PATCH 099/682] Sort --- config/artifact.json | 276 +++++----- config/pkg.ext.json | 1154 ++++++++++++++++++++-------------------- config/pkg.lib.json | 644 +++++++++++----------- config/pkg.target.json | 124 ++--- 4 files changed, 1099 insertions(+), 1099 deletions(-) diff --git a/config/artifact.json b/config/artifact.json index c8fd66215..ad8507b90 100644 --- a/config/artifact.json +++ b/config/artifact.json @@ -1,135 +1,4 @@ { - "vswhere": { - "binary": { - "windows-x86_64": { - "type": "url", - "url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe", - "extract": "{pkg_root_path}/bin/vswhere.exe" - } - } - }, - "musl-wrapper": { - "source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz" - }, - "php-src": { - "source": { - "type": "php-release" - } - }, - "php-sdk-binary-tools": { - "binary": { - "windows-x86_64": { - "type": "git", - "rev": "master", - "url": "https://github.com/php/php-sdk-binary-tools.git", - "extract": "{php_sdk_path}" - } - } - }, - "go-xcaddy": { - "binary": "custom" - }, - "musl-toolchain": { - "binary": { - "linux-x86_64": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", - "extract": "{pkg_root_path}/musl-toolchain" - }, - "linux-aarch64": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz", - "extract": "{pkg_root_path}/musl-toolchain" - } - } - }, - "pkg-config": { - "source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz", - "binary": { - "linux-x86_64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", - "extract": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "linux-aarch64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", - "extract": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "macos-x86_64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-darwin.txz", - "extract": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "macos-aarch64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-darwin.txz", - "extract": "{pkg_root_path}" - } - } - }, - "strawberry-perl": { - "binary": { - "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": { - "binary": { - "linux-x86_64": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-amd64_linux\\.tar\\.xz", - "extract": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "linux-aarch64": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-arm64_linux\\.tar\\.xz", - "extract": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "windows-x86_64": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-win64\\.zip", - "extract": { - "upx.exe": "{pkg_root_path}/bin/upx.exe" - } - } - } - }, - "zig": { - "binary": "custom" - }, - "nasm": { - "binary": { - "windows-x86_64": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", - "extract": { - "nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" - } - } - } - }, "amqp": { "source": { "type": "url", @@ -395,6 +264,9 @@ "repo": "guanzhi/GmSSL" } }, + "go-xcaddy": { + "binary": "custom" + }, "grpc": { "binary": "hosted", "source": { @@ -733,13 +605,6 @@ "extract": "php-src/ext/memcached" } }, - "mimalloc": { - "source": { - "type": "ghtagtar", - "repo": "microsoft/mimalloc", - "match": "v2\\.\\d\\.[^3].*" - } - }, "micro": { "source": { "type": "git", @@ -748,6 +613,13 @@ "url": "https://github.com/static-php/phpmicro" } }, + "mimalloc": { + "source": { + "type": "ghtagtar", + "repo": "microsoft/mimalloc", + "match": "v2\\.\\d\\.[^3].*" + } + }, "mongodb": { "source": { "type": "ghrel", @@ -765,6 +637,35 @@ "extract": "php-src/ext/msgpack" } }, + "musl-toolchain": { + "binary": { + "linux-x86_64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", + "extract": "{pkg_root_path}/musl-toolchain" + }, + "linux-aarch64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz", + "extract": "{pkg_root_path}/musl-toolchain" + } + } + }, + "musl-wrapper": { + "source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz" + }, + "nasm": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", + "extract": { + "nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + } + } + } + }, "ncurses": { "binary": "hosted", "source": { @@ -850,6 +751,56 @@ "extract": "php-src/ext/pdo_sqlsrv" } }, + "php-sdk-binary-tools": { + "binary": { + "windows-x86_64": { + "type": "git", + "rev": "master", + "url": "https://github.com/php/php-sdk-binary-tools.git", + "extract": "{php_sdk_path}" + } + } + }, + "php-src": { + "source": { + "type": "php-release" + } + }, + "pkg-config": { + "binary": { + "linux-x86_64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "linux-aarch64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "macos-x86_64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-x86_64-darwin.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "macos-aarch64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-aarch64-darwin.txz", + "extract": "{pkg_root_path}" + } + }, + "source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz" + }, "postgresql": { "source": { "type": "ghtagtar", @@ -950,6 +901,15 @@ "extract": "php-src/ext/sqlsrv" } }, + "strawberry-perl": { + "binary": { + "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" + } + } + }, "swoole": { "source": { "type": "ghtar", @@ -982,6 +942,43 @@ "version": "2.3.12" } }, + "upx": { + "binary": { + "linux-x86_64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-amd64_linux\\.tar\\.xz", + "extract": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "linux-aarch64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-arm64_linux\\.tar\\.xz", + "extract": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "windows-x86_64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-win64\\.zip", + "extract": { + "upx.exe": "{pkg_root_path}/bin/upx.exe" + } + } + } + }, + "vswhere": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe", + "extract": "{pkg_root_path}/bin/vswhere.exe" + } + } + }, "watcher": { "source": { "type": "ghtar", @@ -1041,6 +1038,9 @@ "extract": "php-src/ext/yaml" } }, + "zig": { + "binary": "custom" + }, "zlib": { "binary": "hosted", "source": { diff --git a/config/pkg.ext.json b/config/pkg.ext.json index 70fe34e63..b1e819eb3 100644 --- a/config/pkg.ext.json +++ b/config/pkg.ext.json @@ -1,66 +1,66 @@ { "ext-amqp": { - "type": "php-extension", - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - }, + "artifact": "amqp", "depends": [ "librabbitmq" ], "depends@windows": [ "ext-openssl" ], - "artifact": "amqp", "license": { "type": "file", "path": "LICENSE" - } + }, + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "type": "php-extension" }, "ext-apcu": { - "type": "php-extension", "artifact": "apcu", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "php-extension" }, "ext-ast": { - "type": "php-extension", "artifact": "ast", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "php-extension" }, "ext-bcmath": { "type": "php-extension" }, "ext-brotli": { - "type": "php-extension", - "php-extension": { - "arg-type": "enable" - }, + "artifact": "ext-brotli", "depends": [ "brotli" ], - "artifact": "ext-brotli", "license": { "type": "file", "path": "LICENSE" - } + }, + "php-extension": { + "arg-type": "enable" + }, + "type": "php-extension" }, "ext-bz2": { - "type": "php-extension", + "depends": [ + "bzip2" + ], "php-extension": { "arg-type@windows": "with", "arg-type": "with-path" }, - "depends": [ - "bzip2" - ] + "type": "php-extension" }, "ext-calendar": { "type": "php-extension" @@ -69,68 +69,67 @@ "type": "php-extension" }, "ext-curl": { - "type": "php-extension", - "php-extension": { - "arg-type": "with", - "notes": true - }, "depends": [ "curl" ], "depends@windows": [ "ext-zlib", "ext-openssl" - ] + ], + "php-extension": { + "arg-type": "with", + "notes": true + }, + "type": "php-extension" }, "ext-dba": { - "type": "php-extension", "php-extension": { "arg-type": "custom" }, "suggests": [ "qdbm" - ] + ], + "type": "php-extension" }, "ext-dio": { - "type": "php-extension", - "php-extension": { - "support": { - "BSD": "wip" - } - }, "artifact": "dio", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-dom": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" - }, - "arg-type": "custom", - "arg-type@windows": "with" + } }, + "type": "php-extension" + }, + "ext-dom": { "depends": [ "libxml2", "zlib" ], "depends@windows": [ "ext-xml" - ] + ], + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with" + }, + "type": "php-extension" }, "ext-ds": { - "type": "php-extension", "artifact": "ext-ds", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "php-extension" }, "ext-enchant": { - "type": "php-extension", "php-extension": { "support": { "Windows": "wip", @@ -138,24 +137,33 @@ "Darwin": "wip", "Linux": "wip" } - } + }, + "type": "php-extension" }, "ext-ev": { - "type": "php-extension", - "php-extension": { - "arg-type@windows": "with" - }, + "artifact": "ev", "depends": [ "ext-sockets" ], - "artifact": "ev", "license": { "type": "file", "path": "LICENSE" - } + }, + "php-extension": { + "arg-type@windows": "with" + }, + "type": "php-extension" }, "ext-event": { - "type": "php-extension", + "artifact": "ext-event", + "depends": [ + "libevent", + "ext-openssl" + ], + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", @@ -164,24 +172,21 @@ "arg-type": "custom", "notes": true }, - "depends": [ - "libevent", - "ext-openssl" - ], "suggests": [ "ext-sockets" ], - "artifact": "ext-event", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-exif": { "type": "php-extension" }, "ext-ffi": { - "type": "php-extension", + "depends": [ + "libffi" + ], + "depends@windows": [ + "libffi-win" + ], "php-extension": { "support": { "Linux": "partial", @@ -190,12 +195,7 @@ "arg-type": "custom", "notes": true }, - "depends@windows": [ - "libffi-win" - ], - "depends": [ - "libffi" - ] + "type": "php-extension" }, "ext-fileinfo": { "type": "php-extension" @@ -204,13 +204,17 @@ "type": "php-extension" }, "ext-ftp": { - "type": "php-extension", "suggests": [ "openssl" - ] + ], + "type": "php-extension" }, "ext-gd": { - "type": "php-extension", + "depends": [ + "zlib", + "libpng", + "ext-zlib" + ], "php-extension": { "support": { "BSD": "wip" @@ -219,20 +223,18 @@ "arg-type@windows": "with", "notes": true }, - "depends": [ - "zlib", - "libpng", - "ext-zlib" - ], "suggests": [ "libavif", "libwebp", "libjpeg", "freetype" - ] + ], + "type": "php-extension" }, "ext-gettext": { - "type": "php-extension", + "depends": [ + "gettext" + ], "php-extension": { "support": { "Windows": "wip", @@ -240,12 +242,18 @@ }, "arg-type": "with-path" }, - "depends": [ - "gettext" - ] + "type": "php-extension" }, "ext-glfw": { - "type": "php-extension", + "artifact": "ext-glfw", + "depends": [ + "glfw" + ], + "depends@windows": [], + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", @@ -255,18 +263,12 @@ "arg-type": "custom", "notes": true }, - "depends": [ - "glfw" - ], - "depends@windows": [], - "artifact": "ext-glfw", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-gmp": { - "type": "php-extension", + "depends": [ + "gmp" + ], "php-extension": { "support": { "Windows": "wip", @@ -274,47 +276,50 @@ }, "arg-type": "with-path" }, - "depends": [ - "gmp" - ] + "type": "php-extension" }, "ext-gmssl": { - "type": "php-extension", - "php-extension": { - "support": { - "BSD": "wip" - } - }, + "artifact": "ext-gmssl", "depends": [ "gmssl" ], - "artifact": "ext-gmssl", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-grpc": { - "type": "php-extension", + }, "php-extension": { "support": { - "Windows": "wip", "BSD": "wip" - }, - "arg-type": "enable-path" + } }, + "type": "php-extension" + }, + "ext-grpc": { + "artifact": "grpc", "depends": [ "grpc" ], "lang": "cpp", - "artifact": "grpc", "license": { "type": "file", "path": "LICENSE" - } + }, + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "enable-path" + }, + "type": "php-extension" }, "ext-iconv": { - "type": "php-extension", + "depends": [ + "libiconv" + ], + "depends@windows": [ + "libiconv-win" + ], "php-extension": { "support": { "BSD": "wip" @@ -322,15 +327,14 @@ "arg-type": "with-path", "arg-type@windows": "with" }, - "depends@windows": [ - "libiconv-win" - ], - "depends": [ - "libiconv" - ] + "type": "php-extension" }, "ext-igbinary": { - "type": "php-extension", + "artifact": "igbinary", + "license": { + "type": "file", + "path": "COPYING" + }, "php-extension": { "support": { "BSD": "wip" @@ -340,14 +344,17 @@ "ext-session", "ext-apcu" ], - "artifact": "igbinary", - "license": { - "type": "file", - "path": "COPYING" - } + "type": "php-extension" }, "ext-imagick": { - "type": "php-extension", + "artifact": "ext-imagick", + "depends": [ + "imagemagick" + ], + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", @@ -356,17 +363,19 @@ "arg-type": "custom", "notes": true }, + "type": "php-extension" + }, + "ext-imap": { + "artifact": "ext-imap", "depends": [ - "imagemagick" + "imap" ], - "artifact": "ext-imagick", "license": { "type": "file", - "path": "LICENSE" - } - }, - "ext-imap": { - "type": "php-extension", + "path": [ + "LICENSE" + ] + }, "php-extension": { "support": { "Windows": "wip", @@ -375,22 +384,17 @@ "arg-type": "custom", "notes": true }, - "depends": [ - "imap" - ], "suggests": [ "ext-openssl" ], - "artifact": "ext-imap", - "license": { - "type": "file", - "path": [ - "LICENSE" - ] - } + "type": "php-extension" }, "ext-inotify": { - "type": "php-extension", + "artifact": "inotify", + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "no", @@ -398,28 +402,26 @@ "Darwin": "no" } }, - "artifact": "inotify", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-intl": { - "type": "php-extension", - "php-extension": { - "support": { - "BSD": "wip" - } - }, + "depends": [ + "icu" + ], "depends@windows": [ "icu-static-win" ], - "depends": [ - "icu" - ] + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "type": "php-extension" }, "ext-ldap": { - "type": "php-extension", + "depends": [ + "ldap" + ], "php-extension": { "support": { "Windows": "wip", @@ -427,17 +429,17 @@ }, "arg-type": "with-path" }, - "depends": [ - "ldap" - ], "suggests": [ "gmp", "libsodium", "ext-openssl" - ] + ], + "type": "php-extension" }, "ext-libxml": { - "type": "php-extension", + "depends": [ + "ext-xml" + ], "php-extension": { "support": { "BSD": "wip" @@ -447,50 +449,47 @@ "build-static": true, "build-with-php": true }, - "depends": [ - "ext-xml" - ] + "type": "php-extension" }, "ext-lz4": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom" - }, + "artifact": "ext-lz4", "depends": [ "liblz4" ], - "artifact": "ext-lz4", "license": { "type": "file", "path": [ "LICENSE" ] - } + }, + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "type": "php-extension" }, "ext-mbregex": { - "type": "php-extension", + "depends": [ + "onig", + "ext-mbstring" + ], "php-extension": { "arg-type": "custom", "build-shared": false, "build-static": true }, - "depends": [ - "onig", - "ext-mbstring" - ] + "type": "php-extension" }, "ext-mbstring": { - "type": "php-extension", "php-extension": { "arg-type": "custom" - } + }, + "type": "php-extension" }, "ext-mcrypt": { - "type": "php-extension", "php-extension": { "support": { "Windows": "no", @@ -499,10 +498,19 @@ "Linux": "no" }, "notes": true - } + }, + "type": "php-extension" }, "ext-memcache": { - "type": "php-extension", + "artifact": "ext-memcache", + "depends": [ + "ext-zlib", + "ext-session" + ], + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", @@ -511,18 +519,21 @@ "arg-type": "custom", "build-with-php": true }, + "type": "php-extension" + }, + "ext-memcached": { + "artifact": "memcached", "depends": [ - "ext-zlib", - "ext-session" + "libmemcached", + "fastlz", + "ext-session", + "ext-zlib" ], - "artifact": "ext-memcache", + "lang": "cpp", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-memcached": { - "type": "php-extension", + }, "php-extension": { "support": { "Windows": "wip", @@ -530,27 +541,24 @@ }, "arg-type": "custom" }, - "depends": [ - "libmemcached", - "fastlz", - "ext-session", - "ext-zlib" - ], "suggests": [ "zstd", "ext-igbinary", "ext-msgpack", "ext-session" ], - "lang": "cpp", - "artifact": "memcached", + "type": "php-extension" + }, + "ext-mongodb": { + "artifact": "mongodb", + "frameworks": [ + "CoreFoundation", + "Security" + ], "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-mongodb": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip", @@ -564,18 +572,17 @@ "zstd", "zlib" ], - "frameworks": [ - "CoreFoundation", - "Security" + "type": "php-extension" + }, + "ext-msgpack": { + "artifact": "msgpack", + "depends": [ + "ext-session" ], - "artifact": "mongodb", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-msgpack": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" @@ -583,37 +590,29 @@ "arg-type@windows": "enable", "arg-type": "with" }, - "depends": [ - "ext-session" - ], - "artifact": "msgpack", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-mysqli": { - "type": "php-extension", + "depends": [ + "ext-mysqlnd" + ], "php-extension": { "arg-type": "with", "build-with-php": true }, - "depends": [ - "ext-mysqlnd" - ] + "type": "php-extension" }, "ext-mysqlnd": { - "type": "php-extension", + "depends": [ + "zlib" + ], "php-extension": { "arg-type@windows": "with", "build-with-php": true }, - "depends": [ - "zlib" - ] + "type": "php-extension" }, "ext-oci8": { - "type": "php-extension", "php-extension": { "support": { "Windows": "wip", @@ -622,10 +621,13 @@ "Linux": "no" }, "notes": true - } + }, + "type": "php-extension" }, "ext-odbc": { - "type": "php-extension", + "depends": [ + "unixodbc" + ], "php-extension": { "support": { "BSD": "wip", @@ -633,47 +635,52 @@ }, "arg-type": "custom" }, - "depends": [ - "unixodbc" - ] + "type": "php-extension" }, "ext-opcache": { - "type": "php-extension", "php-extension": { "arg-type@windows": "enable", "arg-type": "custom", "zend-extension": true - } + }, + "type": "php-extension" }, "ext-openssl": { - "type": "php-extension", + "depends": [ + "openssl", + "zlib", + "ext-zlib" + ], "php-extension": { "arg-type": "custom", "arg-type@windows": "with", "build-with-php": true, "notes": true }, - "depends": [ - "openssl", - "zlib", - "ext-zlib" - ] + "type": "php-extension" }, "ext-opentelemetry": { - "type": "php-extension", + "artifact": "opentelemetry", + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "BSD": "wip" } }, - "artifact": "opentelemetry", + "type": "php-extension" + }, + "ext-parallel": { + "artifact": "parallel", + "depends@windows": [ + "pthreads4w" + ], "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-parallel": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" @@ -681,17 +688,13 @@ "arg-type@windows": "with", "notes": true }, - "depends@windows": [ - "pthreads4w" - ], - "artifact": "parallel", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-password-argon2": { - "type": "php-extension", + "depends": [ + "libargon2", + "openssl" + ], "php-extension": { "support": { "Windows": "wip", @@ -700,98 +703,100 @@ "arg-type": "custom", "notes": true }, - "depends": [ - "libargon2", - "openssl" - ] + "type": "php-extension" }, "ext-pcntl": { - "type": "php-extension", "php-extension": { "support": { "Windows": "no" } - } + }, + "type": "php-extension" }, "ext-pdo": { "type": "php-extension" }, "ext-pdo_mysql": { - "type": "php-extension", - "php-extension": { - "arg-type": "with" - }, "depends": [ "ext-pdo", "ext-mysqlnd" - ] - }, - "ext-pdo_odbc": { - "type": "php-extension", + ], "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" + "arg-type": "with" }, + "type": "php-extension" + }, + "ext-pdo_odbc": { "depends": [ "unixodbc", "ext-pdo", "ext-odbc" - ] - }, - "ext-pdo_pgsql": { - "type": "php-extension", + ], "php-extension": { "support": { "BSD": "wip" }, - "arg-type": "with-path", - "arg-type@windows": "custom" + "arg-type": "custom" }, - "depends@windows": [ - "postgresql-win" - ], + "type": "php-extension" + }, + "ext-pdo_pgsql": { "depends": [ "postgresql", "ext-pdo", "ext-pgsql" - ] - }, - "ext-pdo_sqlite": { - "type": "php-extension", + ], + "depends@windows": [ + "postgresql-win" + ], "php-extension": { "support": { "BSD": "wip" }, - "arg-type": "with" + "arg-type": "with-path", + "arg-type@windows": "custom" }, + "type": "php-extension" + }, + "ext-pdo_sqlite": { "depends": [ "sqlite", "ext-pdo", "ext-sqlite3" - ] - }, - "ext-pdo_sqlsrv": { - "type": "php-extension", + ], "php-extension": { "support": { "BSD": "wip" }, "arg-type": "with" }, + "type": "php-extension" + }, + "ext-pdo_sqlsrv": { + "artifact": "pdo_sqlsrv", "depends": [ "ext-pdo", "ext-sqlsrv" ], - "artifact": "pdo_sqlsrv", "license": { "type": "file", "path": "LICENSE" - } + }, + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "type": "php-extension" }, "ext-pgsql": { - "type": "php-extension", + "depends": [ + "postgresql" + ], + "depends@windows": [ + "postgresql-win" + ], "php-extension": { "support": { "BSD": "wip" @@ -799,43 +804,43 @@ "arg-type": "custom", "notes": true }, - "depends@windows": [ - "postgresql-win" - ], - "depends": [ - "postgresql" - ] + "type": "php-extension" }, "ext-phar": { - "type": "php-extension", "depends": [ "ext-zlib" - ] + ], + "type": "php-extension" }, "ext-posix": { - "type": "php-extension", "php-extension": { "support": { "Windows": "no" } - } + }, + "type": "php-extension" }, "ext-protobuf": { - "type": "php-extension", + "artifact": "protobuf", + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", "BSD": "wip" } }, - "artifact": "protobuf", + "type": "php-extension" + }, + "ext-rar": { + "artifact": "rar", + "lang": "cpp", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-rar": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip", @@ -843,15 +848,18 @@ }, "notes": true }, + "type": "php-extension" + }, + "ext-rdkafka": { + "artifact": "ext-rdkafka", + "depends": [ + "librdkafka" + ], "lang": "cpp", - "artifact": "rar", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-rdkafka": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip", @@ -859,18 +867,12 @@ }, "arg-type": "custom" }, - "depends": [ - "librdkafka" - ], - "lang": "cpp", - "artifact": "ext-rdkafka", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-readline": { - "type": "php-extension", + "depends": [ + "libedit" + ], "php-extension": { "support": { "Windows": "wip", @@ -880,12 +882,17 @@ "build-shared": false, "build-static": true }, - "depends": [ - "libedit" - ] + "type": "php-extension" }, "ext-redis": { - "type": "php-extension", + "artifact": "redis", + "license": { + "type": "file", + "path": [ + "LICENSE", + "COPYING" + ] + }, "php-extension": { "support": { "BSD": "wip" @@ -899,38 +906,36 @@ "ext-igbinary", "ext-msgpack" ], - "artifact": "redis", - "license": { - "type": "file", - "path": [ - "LICENSE", - "COPYING" - ] - } + "type": "php-extension" }, "ext-session": { - "type": "php-extension", "php-extension": { "build-with-php": true - } + }, + "type": "php-extension" }, "ext-shmop": { - "type": "php-extension", "php-extension": { "build-with-php": true - } + }, + "type": "php-extension" }, "ext-simdjson": { - "type": "php-extension", - "lang": "cpp", "artifact": "ext-simdjson", + "lang": "cpp", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "php-extension" }, "ext-simplexml": { - "type": "php-extension", + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml" + ], "php-extension": { "support": { "BSD": "wip" @@ -938,15 +943,18 @@ "arg-type": "custom", "build-with-php": true }, - "depends": [ - "libxml2" - ], - "depends@windows": [ - "ext-xml" - ] + "type": "php-extension" }, "ext-snappy": { - "type": "php-extension", + "artifact": "ext-snappy", + "depends": [ + "snappy" + ], + "lang": "cpp", + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", @@ -954,21 +962,15 @@ }, "arg-type": "custom" }, - "depends": [ - "snappy" - ], "suggests": [ "ext-apcu" ], - "lang": "cpp", - "artifact": "ext-snappy", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-snmp": { - "type": "php-extension", + "depends": [ + "net-snmp" + ], "php-extension": { "support": { "Windows": "wip", @@ -977,40 +979,45 @@ "arg-type@windows": "with", "arg-type": "with" }, - "depends": [ - "net-snmp" - ] + "type": "php-extension" }, "ext-soap": { - "type": "php-extension", + "depends": [ + "ext-libxml", + "ext-session" + ], "php-extension": { "support": { "BSD": "wip" }, "arg-type": "custom" }, - "depends": [ - "ext-libxml", - "ext-session" - ] + "type": "php-extension" }, "ext-sockets": { "type": "php-extension" }, "ext-sodium": { - "type": "php-extension", + "depends": [ + "libsodium" + ], "php-extension": { "support": { "BSD": "wip" }, "arg-type": "with" }, - "depends": [ - "libsodium" - ] + "type": "php-extension" }, "ext-spx": { - "type": "php-extension", + "artifact": "spx", + "depends": [ + "zlib" + ], + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "BSD": "wip", @@ -1019,17 +1026,12 @@ "arg-type": "custom", "notes": true }, - "depends": [ - "zlib" - ], - "artifact": "spx", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-sqlite3": { - "type": "php-extension", + "depends": [ + "sqlite" + ], "php-extension": { "support": { "BSD": "wip" @@ -1038,17 +1040,10 @@ "arg-type@windows": "with", "build-with-php": true }, - "depends": [ - "sqlite" - ] + "type": "php-extension" }, "ext-sqlsrv": { - "type": "php-extension", - "php-extension": { - "support": { - "BSD": "wip" - } - }, + "artifact": "sqlsrv", "depends": [ "unixodbc" ], @@ -1056,42 +1051,39 @@ "ext-pcntl" ], "lang": "cpp", - "artifact": "sqlsrv", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-ssh2": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" - }, - "arg-type": "with-path", - "arg-type@windows": "with" + } }, + "type": "php-extension" + }, + "ext-ssh2": { + "artifact": "ext-ssh2", "depends": [ "libssh2", "ext-openssl", "ext-zlib" ], - "artifact": "ext-ssh2", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-swoole": { - "type": "php-extension", + }, "php-extension": { "support": { - "Windows": "no", "BSD": "wip" }, - "arg-type": "custom", - "notes": true + "arg-type": "with-path", + "arg-type@windows": "with" }, + "type": "php-extension" + }, + "ext-swoole": { + "artifact": "swoole", "depends": [ "libcares", "brotli", @@ -1100,6 +1092,19 @@ "ext-openssl", "ext-curl" ], + "lang": "cpp", + "license": { + "type": "file", + "path": "LICENSE" + }, + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, "suggests": [ "zstd", "ext-sockets", @@ -1109,18 +1114,18 @@ "ext-swoole-hook-odbc" ], "suggests@linux": [ - "zstd", - "liburing" - ], - "lang": "cpp", - "artifact": "swoole", - "license": { - "type": "file", - "path": "LICENSE" - } + "zstd", + "liburing" + ], + "type": "php-extension" }, "ext-swoole-hook-mysql": { - "type": "php-extension", + "depends": [ + "ext-mysqlnd", + "ext-pdo", + "ext-pdo_mysql", + "ext-swoole" + ], "php-extension": { "support": { "Windows": "no", @@ -1129,18 +1134,17 @@ "arg-type": "none", "notes": true }, - "depends": [ - "ext-mysqlnd", - "ext-pdo", - "ext-pdo_mysql", - "ext-swoole" - ], "suggests": [ "ext-mysqli" - ] + ], + "type": "php-extension" }, "ext-swoole-hook-odbc": { - "type": "php-extension", + "depends": [ + "unixodbc", + "ext-pdo", + "ext-swoole" + ], "php-extension": { "support": { "Windows": "no", @@ -1149,14 +1153,14 @@ "arg-type": "none", "notes": true }, + "type": "php-extension" + }, + "ext-swoole-hook-pgsql": { "depends": [ - "unixodbc", + "ext-pgsql", "ext-pdo", "ext-swoole" - ] - }, - "ext-swoole-hook-pgsql": { - "type": "php-extension", + ], "php-extension": { "support": { "Windows": "no", @@ -1166,14 +1170,14 @@ "arg-type": "none", "notes": true }, + "type": "php-extension" + }, + "ext-swoole-hook-sqlite": { "depends": [ - "ext-pgsql", + "ext-sqlite3", "ext-pdo", "ext-swoole" - ] - }, - "ext-swoole-hook-sqlite": { - "type": "php-extension", + ], "php-extension": { "support": { "Windows": "no", @@ -1182,14 +1186,14 @@ "arg-type": "none", "notes": true }, - "depends": [ - "ext-sqlite3", - "ext-pdo", - "ext-swoole" - ] + "type": "php-extension" }, "ext-swow": { - "type": "php-extension", + "artifact": "swow", + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "BSD": "wip" @@ -1203,40 +1207,38 @@ "ext-openssl", "ext-curl" ], - "artifact": "swow", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-sysvmsg": { - "type": "php-extension", "php-extension": { "support": { "Windows": "no", "BSD": "wip" } - } + }, + "type": "php-extension" }, "ext-sysvsem": { - "type": "php-extension", "php-extension": { "support": { "Windows": "no", "BSD": "wip" } - } + }, + "type": "php-extension" }, "ext-sysvshm": { - "type": "php-extension", "php-extension": { "support": { "BSD": "wip" } - } + }, + "type": "php-extension" }, "ext-tidy": { - "type": "php-extension", + "depends": [ + "tidy" + ], "php-extension": { "support": { "Windows": "wip", @@ -1244,32 +1246,37 @@ }, "arg-type": "with-path" }, - "depends": [ - "tidy" - ] + "type": "php-extension" }, "ext-tokenizer": { - "type": "php-extension", "php-extension": { "build-with-php": true - } + }, + "type": "php-extension" }, "ext-trader": { - "type": "php-extension", + "artifact": "ext-trader", + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "BSD": "wip", "Windows": "wip" } }, - "artifact": "ext-trader", + "type": "php-extension" + }, + "ext-uuid": { + "artifact": "ext-uuid", + "depends": [ + "libuuid" + ], "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-uuid": { - "type": "php-extension", + }, "php-extension": { "support": { "Windows": "wip", @@ -1277,17 +1284,18 @@ }, "arg-type": "with-path" }, + "type": "php-extension" + }, + "ext-uv": { + "artifact": "ext-uv", "depends": [ - "libuuid" + "libuv", + "ext-sockets" ], - "artifact": "ext-uuid", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-uv": { - "type": "php-extension", + }, "php-extension": { "support": { "Windows": "wip", @@ -1295,18 +1303,14 @@ }, "arg-type": "with-path" }, - "depends": [ - "libuv", - "ext-sockets" - ], - "artifact": "ext-uv", + "type": "php-extension" + }, + "ext-xdebug": { + "artifact": "xdebug", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-xdebug": { - "type": "php-extension", + }, "php-extension": { "support": { "Windows": "wip", @@ -1319,14 +1323,17 @@ "notes": true, "zend-extension": true }, - "artifact": "xdebug", + "type": "php-extension" + }, + "ext-xhprof": { + "artifact": "xhprof", + "depends": [ + "ext-ctype" + ], "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-xhprof": { - "type": "php-extension", + }, "php-extension": { "support": { "Windows": "wip", @@ -1335,17 +1342,18 @@ "build-with-php": true, "notes": true }, + "type": "php-extension" + }, + "ext-xlswriter": { + "artifact": "xlswriter", "depends": [ - "ext-ctype" + "ext-zlib", + "ext-zip" ], - "artifact": "xhprof", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-xlswriter": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" @@ -1355,18 +1363,15 @@ "suggests": [ "openssl" ], - "depends": [ - "ext-zlib", - "ext-zip" - ], - "artifact": "xlswriter", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" }, "ext-xml": { - "type": "php-extension", + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-iconv" + ], "php-extension": { "support": { "BSD": "wip" @@ -1376,15 +1381,16 @@ "build-with-php": true, "notes": true }, + "type": "php-extension" + }, + "ext-xmlreader": { "depends": [ "libxml2" ], "depends@windows": [ - "ext-iconv" - ] - }, - "ext-xmlreader": { - "type": "php-extension", + "ext-xml", + "ext-dom" + ], "php-extension": { "support": { "BSD": "wip" @@ -1392,16 +1398,15 @@ "arg-type": "custom", "build-with-php": true }, + "type": "php-extension" + }, + "ext-xmlwriter": { "depends": [ "libxml2" ], "depends@windows": [ - "ext-xml", - "ext-dom" - ] - }, - "ext-xmlwriter": { - "type": "php-extension", + "ext-xml" + ], "php-extension": { "support": { "BSD": "wip" @@ -1409,15 +1414,14 @@ "arg-type": "custom", "build-with-php": true }, - "depends": [ - "libxml2" - ], - "depends@windows": [ - "ext-xml" - ] + "type": "php-extension" }, "ext-xsl": { - "type": "php-extension", + "depends": [ + "libxslt", + "ext-xml", + "ext-dom" + ], "php-extension": { "support": { "Windows": "wip", @@ -1425,71 +1429,63 @@ }, "arg-type": "with-path" }, - "depends": [ - "libxslt", - "ext-xml", - "ext-dom" - ] + "type": "php-extension" }, "ext-xz": { - "type": "php-extension", - "php-extension": { - "arg-type": "with" - }, + "artifact": "ext-xz", "depends": [ "xz" ], - "artifact": "ext-xz", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-yac": { - "type": "php-extension", + }, "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" + "arg-type": "with" }, + "type": "php-extension" + }, + "ext-yac": { + "artifact": "yac", "depends": [ "fastlz", "ext-igbinary" ], - "artifact": "yac", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-yaml": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" }, - "arg-type@windows": "with", - "arg-type": "with-path" + "arg-type": "custom" }, + "type": "php-extension" + }, + "ext-yaml": { + "artifact": "yaml", "depends": [ "libyaml" ], - "artifact": "yaml", "license": { "type": "file", "path": "LICENSE" - } - }, - "ext-zip": { - "type": "php-extension", + }, "php-extension": { "support": { "BSD": "wip" }, - "arg-type": "custom", - "arg-type@windows": "enable" + "arg-type@windows": "with", + "arg-type": "with-path" }, + "type": "php-extension" + }, + "ext-zip": { + "artifact": "ext-zip", + "depends": [ + "libzip" + ], "depends@windows": [ "libzip", "zlib", @@ -1498,17 +1494,23 @@ "ext-zlib", "ext-bz2" ], - "depends": [ - "libzip" - ], - "artifact": "ext-zip", "license": { "type": "file", "path": "LICENSE" - } + }, + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "enable" + }, + "type": "php-extension" }, "ext-zlib": { - "type": "php-extension", + "depends": [ + "zlib" + ], "php-extension": { "arg-type": "custom", "arg-type@windows": "enable", @@ -1516,12 +1518,17 @@ "build-static": true, "build-with-php": true }, - "depends": [ - "zlib" - ] + "type": "php-extension" }, "ext-zstd": { - "type": "php-extension", + "artifact": "ext-zstd", + "depends": [ + "zstd" + ], + "license": { + "type": "file", + "path": "LICENSE" + }, "php-extension": { "support": { "Windows": "wip", @@ -1529,13 +1536,6 @@ }, "arg-type": "custom" }, - "depends": [ - "zstd" - ], - "artifact": "ext-zstd", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "php-extension" } } diff --git a/config/pkg.lib.json b/config/pkg.lib.json index 79e1a8534..6cade9b7f 100644 --- a/config/pkg.lib.json +++ b/config/pkg.lib.json @@ -1,54 +1,62 @@ { "attr": { - "type": "library", "artifact": "attr", "license": { "type": "file", "path": "doc/COPYING.LGPL" - } + }, + "type": "library" }, "brotli": { - "type": "library", + "artifact": "brotli", "headers": [ "brotli" ], + "license": { + "type": "file", + "path": "LICENSE" + }, "pkg-configs": [ "libbrotlicommon", "libbrotlidec", "libbrotlienc" ], - "artifact": "brotli", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "bzip2": { - "type": "library", + "artifact": "bzip2", "headers": [ "bzlib.h" ], - "artifact": "bzip2", "license": { "type": "text", "text": "This program, \"bzip2\", the associated library \"libbzip2\", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\n 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\n 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nJulian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010\n\nPATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement." - } + }, + "type": "library" }, "curl": { - "type": "library", + "artifact": "curl", + "depends": [ + "openssl", + "zlib" + ], "depends@windows": [ "zlib", "libssh2", "nghttp2" ], - "depends": [ - "openssl", - "zlib" + "frameworks": [ + "CoreFoundation", + "CoreServices", + "SystemConfiguration" ], - "suggests@windows": [ - "brotli", - "zstd" + "headers": [ + "curl" ], + "license": { + "type": "file", + "path": "COPYING" + }, "suggests": [ "libssh2", "brotli", @@ -59,150 +67,142 @@ "libcares", "ldap" ], - "headers": [ - "curl" - ], - "frameworks": [ - "CoreFoundation", - "CoreServices", - "SystemConfiguration" + "suggests@windows": [ + "brotli", + "zstd" ], - "artifact": "curl", - "license": { - "type": "file", - "path": "COPYING" - } + "type": "library" }, "fastlz": { - "type": "library", + "artifact": "fastlz", "headers": [ "fastlz/fastlz.h" ], - "artifact": "fastlz", "license": { "type": "file", "path": "LICENSE.MIT" - } + }, + "type": "library" }, "freetype": { - "type": "library", + "artifact": "freetype", "depends": [ "zlib" ], - "suggests": [ - "libpng", - "bzip2", - "brotli" - ], "headers": [ "freetype2/freetype/freetype.h", "freetype2/ft2build.h" ], - "artifact": "freetype", "license": { "type": "file", "path": "LICENSE.TXT" - } + }, + "suggests": [ + "libpng", + "bzip2", + "brotli" + ], + "type": "library" }, "gettext": { - "type": "library", + "artifact": "gettext", "depends": [ "libiconv" ], - "suggests": [ - "ncurses", - "libxml2" - ], "frameworks": [ "CoreFoundation" ], - "artifact": "gettext", "license": { "type": "file", "path": "gettext-runtime/intl/COPYING.LIB" - } + }, + "suggests": [ + "ncurses", + "libxml2" + ], + "type": "library" }, "glfw": { - "type": "library", + "artifact": "ext-glfw", "frameworks": [ "CoreVideo", "OpenGL", "Cocoa", "IOKit" ], - "artifact": "ext-glfw", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "gmp": { - "type": "library", + "artifact": "gmp", "headers": [ "gmp.h" ], - "artifact": "gmp", "license": { "type": "text", "text": "Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs." - } + }, + "type": "library" }, "gmssl": { - "type": "library", + "artifact": "gmssl", "frameworks": [ "Security" ], - "artifact": "gmssl", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "grpc": { - "type": "library", + "artifact": "grpc", "depends": [ "zlib", "openssl", "libcares" ], - "pkg-configs": [ - "grpc" - ], "frameworks": [ "CoreFoundation" ], - "artifact": "grpc", "license": { "type": "file", "path": "LICENSE" - } + }, + "pkg-configs": [ + "grpc" + ], + "type": "library" }, "icu": { - "type": "library", + "artifact": "icu", + "license": { + "type": "file", + "path": "LICENSE" + }, "pkg-configs": [ "icu-uc", "icu-i18n", "icu-io" ], - "artifact": "icu", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "icu-static-win": { - "type": "library", + "artifact": "icu-static-win", "headers@windows": [ "unicode" ], - "artifact": "icu-static-win", "license": { "type": "text", "text": "none" - } + }, + "type": "library" }, "imagemagick": { - "type": "library", + "artifact": "imagemagick", "depends": [ "zlib", "libjpeg", @@ -214,183 +214,183 @@ "libheif", "bzip2" ], + "license": { + "type": "file", + "path": "LICENSE" + }, + "pkg-configs": [ + "Magick++-7.Q16HDRI", + "MagickCore-7.Q16HDRI", + "MagickWand-7.Q16HDRI" + ], "suggests": [ "zstd", "xz", "libzip", "libxml2" ], - "pkg-configs": [ - "Magick++-7.Q16HDRI", - "MagickCore-7.Q16HDRI", - "MagickWand-7.Q16HDRI" - ], - "artifact": "imagemagick", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "imap": { - "type": "library", - "suggests": [ - "openssl" - ], "artifact": "imap", "license": { "type": "file", "path": "LICENSE" - } + }, + "suggests": [ + "openssl" + ], + "type": "library" }, "jbig": { - "type": "library", + "artifact": "jbig", "headers": [ "jbig.h", "jbig85.h", "jbig_ar.h" ], - "artifact": "jbig", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "ldap": { - "type": "library", + "artifact": "ldap", "depends": [ "openssl", "zlib", "gmp", "libsodium" ], + "license": { + "type": "file", + "path": "LICENSE" + }, "pkg-configs": [ "ldap", "lber" ], - "artifact": "ldap", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "lerc": { - "type": "library", "artifact": "lerc", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libacl": { - "type": "library", + "artifact": "libacl", "depends": [ "attr" ], - "artifact": "libacl", "license": { "type": "file", "path": "doc/COPYING.LGPL" - } + }, + "type": "library" }, "libaom": { - "type": "library", "artifact": "libaom", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libargon2": { - "type": "library", "artifact": "libargon2", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libavif": { - "type": "library", "artifact": "libavif", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libcares": { - "type": "library", + "artifact": "libcares", "headers": [ "ares.h", "ares_dns.h", "ares_nameser.h" ], - "artifact": "libcares", "license": { "type": "file", "path": "LICENSE.md" - } + }, + "type": "library" }, "libde265": { - "type": "library", "artifact": "libde265", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "libedit": { - "type": "library", + "artifact": "libedit", "depends": [ "ncurses" ], - "artifact": "libedit", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "libevent": { - "type": "library", + "artifact": "libevent", "depends": [ "openssl" ], - "artifact": "libevent", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libffi": { - "type": "library", - "headers@windows": [ + "artifact": "libffi", + "headers": [ "ffi.h", - "fficonfig.h", "ffitarget.h" ], - "headers": [ + "headers@windows": [ "ffi.h", + "fficonfig.h", "ffitarget.h" ], - "artifact": "libffi", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libffi-win": { - "type": "library", + "artifact": "libffi-win", "headers@windows": [ "ffi.h", "ffitarget.h", "fficonfig.h" ], - "artifact": "libffi-win", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libheif": { - "type": "library", + "artifact": "libheif", "depends": [ "libde265", "libwebp", @@ -398,113 +398,121 @@ "zlib", "brotli" ], - "artifact": "libheif", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "libiconv": { - "type": "library", + "artifact": "libiconv", "headers": [ "iconv.h", "libcharset.h", "localcharset.h" ], - "artifact": "libiconv", "license": { "type": "file", "path": "COPYING.LIB" - } + }, + "type": "library" }, "libiconv-win": { - "type": "library", "artifact": "libiconv-win", "license": { "type": "file", "path": "source/COPYING" - } + }, + "type": "library" }, "libjpeg": { - "type": "library", - "suggests@windows": [ - "zlib" - ], "artifact": "libjpeg", "license": { "type": "file", "path": "LICENSE.md" - } + }, + "suggests@windows": [ + "zlib" + ], + "type": "library" }, "libjxl": { - "type": "library", + "artifact": "libjxl", "depends": [ "brotli", "libjpeg", "libpng", "libwebp" ], + "license": { + "type": "file", + "path": "LICENSE" + }, "pkg-configs": [ "libjxl", "libjxl_cms", "libjxl_threads", "libhwy" ], - "artifact": "libjxl", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "liblz4": { - "type": "library", "artifact": "liblz4", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libmemcached": { - "type": "library", "artifact": "libmemcached", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libpng": { - "type": "library", + "artifact": "libpng", "depends": [ "zlib" ], - "headers@windows": [ - "png.h", - "pngconf.h" - ], "headers": [ "png.h", "pngconf.h", "pnglibconf.h" ], - "artifact": "libpng", + "headers@windows": [ + "png.h", + "pngconf.h" + ], "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "librabbitmq": { - "type": "library", + "artifact": "librabbitmq", "depends": [ "openssl" ], - "artifact": "librabbitmq", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "librdkafka": { - "type": "library", + "artifact": "librdkafka", + "license": { + "type": "file", + "path": "LICENSE" + }, + "pkg-configs": [ + "rdkafka++-static", + "rdkafka-static" + ], "suggests": [ "curl", "liblz4", @@ -512,26 +520,18 @@ "zlib", "zstd" ], - "pkg-configs": [ - "rdkafka++-static", - "rdkafka-static" - ], - "artifact": "librdkafka", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "libsodium": { - "type": "library", "artifact": "libsodium", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "libssh2": { - "type": "library", + "artifact": "libssh2", "depends": [ "openssl" ], @@ -540,18 +540,22 @@ "libssh2_publickey.h", "libssh2_sftp.h" ], - "artifact": "libssh2", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "libtiff": { - "type": "library", + "artifact": "libtiff", "depends": [ "zlib", "libjpeg" ], + "license": { + "type": "file", + "path": "LICENSE.md" + }, "suggests": [ "lerc", "libwebp", @@ -559,41 +563,36 @@ "xz", "zstd" ], - "artifact": "libtiff", - "license": { - "type": "file", - "path": "LICENSE.md" - } + "type": "library" }, "liburing": { - "type": "library", + "artifact": "liburing", "headers@linux": [ "liburing/", "liburing.h" ], + "license": { + "type": "file", + "path": "COPYING" + }, "pkg-configs": [ "liburing", "liburing-ffi" ], - "artifact": "liburing", - "license": { - "type": "file", - "path": "COPYING" - } + "type": "library" }, "libuuid": { - "type": "library", + "artifact": "libuuid", "headers": [ "uuid/uuid.h" ], - "artifact": "libuuid", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "libuv": { - "type": "library", "artifact": "libuv", "license": [ { @@ -604,10 +603,15 @@ "type": "file", "path": "LICENSE-extra" } - ] + ], + "type": "library" }, "libwebp": { - "type": "library", + "artifact": "libwebp", + "license": { + "type": "file", + "path": "COPYING" + }, "pkg-configs": [ "libwebp", "libwebpdecoder", @@ -615,206 +619,202 @@ "libwebpmux", "libsharpyuv" ], - "artifact": "libwebp", - "license": { - "type": "file", - "path": "COPYING" - } + "type": "library" }, "libxml2": { - "type": "library", - "depends@windows": [ - "libiconv-win" - ], + "artifact": "libxml2", "depends": [ "libiconv" ], - "suggests@windows": [ - "zlib" - ], - "suggests": [ - "xz", - "zlib" + "depends@windows": [ + "libiconv-win" ], "headers": [ "libxml2" ], - "pkg-configs": [ - "libxml-2.0" - ], - "artifact": "libxml2", "license": { "type": "file", "path": "Copyright" - } + }, + "pkg-configs": [ + "libxml-2.0" + ], + "suggests": [ + "xz", + "zlib" + ], + "suggests@windows": [ + "zlib" + ], + "type": "library" }, "libxslt": { - "type": "library", + "artifact": "libxslt", "depends": [ "libxml2" ], - "artifact": "libxslt", "license": { "type": "file", "path": "Copyright" - } + }, + "type": "library" }, "libyaml": { - "type": "library", + "artifact": "libyaml", "headers": [ "yaml.h" ], - "artifact": "libyaml", "license": { "type": "file", "path": "License" - } + }, + "type": "library" }, "libzip": { - "type": "library", + "artifact": "libzip", + "depends": [ + "zlib" + ], "depends@windows": [ "zlib", "bzip2", "xz" ], - "depends": [ - "zlib" - ], - "suggests@windows": [ - "zstd", - "openssl" + "headers": [ + "zip.h", + "zipconf.h" ], + "license": { + "type": "file", + "path": "LICENSE" + }, "suggests": [ "bzip2", "xz", "zstd", "openssl" ], - "headers": [ - "zip.h", - "zipconf.h" + "suggests@windows": [ + "zstd", + "openssl" ], - "artifact": "libzip", - "license": { - "type": "file", - "path": "LICENSE" - } + "type": "library" }, "mimalloc": { - "type": "library", "artifact": "mimalloc", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "ncurses": { - "type": "library", "artifact": "ncurses", - "static-libs@unix": [ - "libncurses.a" - ], "license": { "type": "file", "path": "COPYING" - } + }, + "static-libs@unix": [ + "libncurses.a" + ], + "type": "library" }, "net-snmp": { - "type": "library", + "artifact": "net-snmp", "depends": [ "openssl", "zlib" ], + "license": { + "type": "file", + "path": "COPYING" + }, "pkg-configs": [ "netsnmp", "netsnmp-agent" ], - "artifact": "net-snmp", - "license": { - "type": "file", - "path": "COPYING" - } + "type": "library" }, "nghttp2": { - "type": "library", + "artifact": "nghttp2", "depends": [ "zlib", "openssl" ], - "suggests": [ - "libxml2", - "nghttp3", - "ngtcp2" - ], "headers": [ "nghttp2" ], - "artifact": "nghttp2", "license": { "type": "file", "path": "COPYING" - } + }, + "suggests": [ + "libxml2", + "nghttp3", + "ngtcp2" + ], + "type": "library" }, "nghttp3": { - "type": "library", + "artifact": "nghttp3", "depends": [ "openssl" ], "headers": [ "nghttp3" ], - "artifact": "nghttp3", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "ngtcp2": { - "type": "library", + "artifact": "ngtcp2", "depends": [ "openssl" ], - "suggests": [ - "nghttp3", - "brotli" - ], "headers": [ "ngtcp2" ], - "artifact": "ngtcp2", "license": { "type": "file", "path": "COPYING" - } + }, + "suggests": [ + "nghttp3", + "brotli" + ], + "type": "library" }, "onig": { - "type": "library", + "artifact": "onig", "headers": [ "oniggnu.h", "oniguruma.h" ], - "artifact": "onig", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "openssl": { - "type": "library", + "artifact": "openssl", "depends": [ "zlib" ], "headers": [ "openssl" ], - "artifact": "openssl", "license": { "type": "file", "path": "LICENSE.txt" - } + }, + "type": "library" }, "postgresql": { - "type": "library", + "artifact": "postgresql", "depends": [ "libiconv", "libxml2", @@ -822,69 +822,69 @@ "zlib", "libedit" ], + "license": { + "type": "file", + "path": "COPYRIGHT" + }, + "pkg-configs": [ + "libpq" + ], "suggests": [ "icu", "libxslt", "ldap", "zstd" ], - "pkg-configs": [ - "libpq" - ], - "artifact": "postgresql", - "license": { - "type": "file", - "path": "COPYRIGHT" - } + "type": "library" }, "postgresql-win": { - "type": "library", "artifact": "postgresql-win", "license": { "type": "text", "text": "PostgreSQL Database Management System\n(also known as Postgres, formerly as Postgres95)\n\nPortions Copyright (c) 1996-2025, The PostgreSQL Global Development Group\n\nPortions Copyright (c) 1994, The Regents of the University of California\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written\nagreement is hereby granted, provided that the above copyright notice\nand this paragraph and the following two paragraphs appear in all\ncopies.\n\nIN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY\nFOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nINCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\nDOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\nTHE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS\nTO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS." - } + }, + "type": "library" }, "pthreads4w": { - "type": "library", "artifact": "pthreads4w", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "qdbm": { - "type": "library", + "artifact": "qdbm", "headers@windows": [ "depot.h" ], - "artifact": "qdbm", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "re2c": { - "type": "library", "artifact": "re2c", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" }, "readline": { - "type": "library", + "artifact": "readline", "depends": [ "ncurses" ], - "artifact": "readline", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "snappy": { - "type": "library", + "artifact": "snappy", "depends": [ "zlib" ], @@ -894,99 +894,99 @@ "snappy-sinksource.h", "snappy-stubs-public.h" ], - "artifact": "snappy", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "sqlite": { - "type": "library", + "artifact": "sqlite", "headers": [ "sqlite3.h", "sqlite3ext.h" ], - "artifact": "sqlite", "license": { "type": "text", "text": "The author disclaims copyright to this source code. In place of\na legal notice, here is a blessing:\n\n * May you do good and not evil.\n * May you find forgiveness for yourself and forgive others.\n * May you share freely, never taking more than you give." - } + }, + "type": "library" }, "tidy": { - "type": "library", "artifact": "tidy", "license": { "type": "file", "path": "README/LICENSE.md" - } + }, + "type": "library" }, "unixodbc": { - "type": "library", + "artifact": "unixodbc", "depends": [ "libiconv" ], - "artifact": "unixodbc", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "watcher": { - "type": "library", + "artifact": "watcher", "headers": [ "wtr/watcher-c.h" ], - "artifact": "watcher", "license": { "type": "file", "path": "license" - } + }, + "type": "library" }, "xz": { - "type": "library", + "artifact": "xz", "depends": [ "libiconv" ], + "headers": [ + "lzma" + ], "headers@windows": [ "lzma", "lzma.h" ], - "headers": [ - "lzma" - ], - "artifact": "xz", "license": { "type": "file", "path": "COPYING" - } + }, + "type": "library" }, "zlib": { - "type": "library", + "artifact": "zlib", "headers": [ "zlib.h", "zconf.h" ], - "artifact": "zlib", "license": { "type": "text", "text": "(C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly Mark Adler\njloup@gzip.org madler@alumni.caltech.edu" - } + }, + "type": "library" }, "zstd": { - "type": "library", - "headers@windows": [ + "artifact": "zstd", + "headers": [ + "zdict.h", "zstd.h", "zstd_errors.h" ], - "headers": [ - "zdict.h", + "headers@windows": [ "zstd.h", "zstd_errors.h" ], - "artifact": "zstd", "license": { "type": "file", "path": "LICENSE" - } + }, + "type": "library" } } diff --git a/config/pkg.target.json b/config/pkg.target.json index 2ae49f400..8e04df905 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -1,98 +1,98 @@ { - "vswhere": { - "type": "target", - "artifact": "vswhere", - "static-bins@windows": [ - "vswhere.exe" - ] + "frankenphp": { + "artifact": "frankenphp", + "depends": [ + "php-embed", + "go-xcaddy" + ], + "depends@macos": [ + "php-embed", + "go-xcaddy", + "libxml2" + ], + "type": "virtual-target" }, - "pkg-config": { - "type": "target", + "go-xcaddy": { + "artifact": "go-xcaddy", "static-bins": [ - "pkg-config" + "xcaddy" ], - "artifact": "pkg-config" + "type": "target" + }, + "musl-toolchain": { + "artifact": "musl-toolchain", + "type": "target" + }, + "nasm": { + "artifact": "nasm", + "type": "target" }, "php": { - "type": "target", "artifact": "php-src", "depends@macos": [ "libxml2" - ] + ], + "type": "target" }, - "php-cli": { - "type": "virtual-target", + "php-cgi": { "depends": [ "php" - ] + ], + "type": "virtual-target" }, - "php-micro": { - "type": "virtual-target", - "artifact": "micro", + "php-cli": { "depends": [ "php" - ] + ], + "type": "virtual-target" }, - "php-cgi": { - "type": "virtual-target", + "php-embed": { "depends": [ "php" - ] + ], + "type": "virtual-target" }, "php-fpm": { - "type": "virtual-target", "depends": [ "php" - ] + ], + "type": "virtual-target" }, - "php-embed": { - "type": "virtual-target", + "php-micro": { + "artifact": "micro", "depends": [ "php" - ] - }, - "frankenphp": { - "type": "virtual-target", - "artifact": "frankenphp", - "depends": [ - "php-embed", - "go-xcaddy" ], - "depends@macos": [ - "php-embed", - "go-xcaddy", - "libxml2" - ] + "type": "virtual-target" }, - "go-xcaddy": { - "type": "target", - "artifact": "go-xcaddy", - "static-bins": [ - "xcaddy" - ] + "php-sdk-binary-tools": { + "artifact": "php-sdk-binary-tools", + "type": "target" }, - "musl-toolchain": { - "type": "target", - "artifact": "musl-toolchain" + "pkg-config": { + "artifact": "pkg-config", + "static-bins": [ + "pkg-config" + ], + "type": "target" }, "strawberry-perl": { - "type": "target", - "artifact": "strawberry-perl" + "artifact": "strawberry-perl", + "type": "target" }, "upx": { - "type": "target", - "artifact": "upx" - }, - "zig": { - "type": "target", - "artifact": "zig" + "artifact": "upx", + "type": "target" }, - "nasm": { - "type": "target", - "artifact": "nasm" + "vswhere": { + "artifact": "vswhere", + "static-bins@windows": [ + "vswhere.exe" + ], + "type": "target" }, - "php-sdk-binary-tools": { - "type": "target", - "artifact": "php-sdk-binary-tools" + "zig": { + "artifact": "zig", + "type": "target" } } From dd5762fbd399f644b2ecb2f4dc7fc9e34bbed18c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 18 Dec 2025 15:43:58 +0800 Subject: [PATCH 100/682] Add lib skeleton command and sort config, spc_mode suuport, etc... --- phpstan.neon | 1 + skeleton-test.php | 23 +- src/Package/Artifact/zig.php | 1 - src/StaticPHP/Artifact/ArtifactDownloader.php | 3 +- src/StaticPHP/Command/BaseCommand.php | 3 +- src/StaticPHP/Command/Dev/SkeletonCommand.php | 402 ++++++++++++++++++ .../Command/Dev/SortConfigCommand.php | 49 +++ src/StaticPHP/Config/ArtifactConfig.php | 9 +- src/StaticPHP/Config/PackageConfig.php | 9 +- src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Package/PackageInstaller.php | 3 +- src/StaticPHP/Registry/Registry.php | 81 ++-- src/StaticPHP/Skeleton/ArtifactGenerator.php | 60 ++- src/StaticPHP/Skeleton/ExecutorGenerator.php | 20 + src/StaticPHP/Skeleton/PackageGenerator.php | 239 ++++++++--- src/StaticPHP/Util/FileSystem.php | 17 + src/StaticPHP/Util/InteractiveTerm.php | 1 + src/globals/defines.php | 25 +- src/globals/functions.php | 25 ++ 19 files changed, 865 insertions(+), 108 deletions(-) create mode 100644 src/StaticPHP/Command/Dev/SkeletonCommand.php create mode 100644 src/StaticPHP/Command/Dev/SortConfigCommand.php diff --git a/phpstan.neon b/phpstan.neon index cf6e49742..45e512ba0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ parameters: reportUnmatchedIgnoredErrors: false level: 4 + phpVersion: 80400 paths: - ./src/ ignoreErrors: diff --git a/skeleton-test.php b/skeleton-test.php index 689d33854..59fbbb7e1 100644 --- a/skeleton-test.php +++ b/skeleton-test.php @@ -1,21 +1,28 @@ addDependency('bar') ->addStaticLib('libfoo.a', 'unix') ->addStaticLib('libfoo.a', 'unix') - ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz'])); + ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz'])) + ->enableBuild(['Darwin', 'Linux'], 'build') + ->addFunctionExecutorBinding('build', new ExecutorGenerator(UnixCMakeExecutor::class)); -$pkg_config = $package_generator->generateConfig(); -$artifact_config = $artifact_generator->generateConfig(); +$pkg_config = $package_generator->generateConfigArray(); +$artifact_config = $artifact_generator->generateConfigArray(); -echo "===== pkg.json =====" . PHP_EOL; -echo json_encode($pkg_config, 64|128|256) . PHP_EOL; -echo "===== artifact.json =====" . PHP_EOL; -echo json_encode($artifact_config, 64|128|256) . PHP_EOL; +echo '===== pkg.json =====' . PHP_EOL; +echo json_encode($pkg_config, 64 | 128 | 256) . PHP_EOL; +echo '===== artifact.json =====' . PHP_EOL; +echo json_encode($artifact_config, 64 | 128 | 256) . PHP_EOL; +echo '===== php code for package =====' . PHP_EOL; +echo $package_generator->generatePackageClassFile('Package\Library'); diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index a73473954..2ac7b454b 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -8,7 +8,6 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; -use StaticPHP\Attribute\Artifact\CustomSource; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 315cfb11d..b53ddd8a2 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -329,8 +329,7 @@ public function download(bool $interactive = true): void } if ($interactive) { $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; - InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}", true); - echo PHP_EOL; + InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true); } } } catch (SPCException $e) { diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index e416be26d..02f84ffb5 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -23,7 +23,6 @@ abstract class BaseCommand extends Command \___ \| __/ _` | __| |/ __| |_) | |_| | |_) | ___) | || (_| | |_| | (__| __/| _ | __/ |____/ \__\__,_|\__|_|\___|_| |_| |_|_| {version} - '; protected bool $no_motd = false; @@ -71,7 +70,7 @@ public function initialize(InputInterface $input, OutputInterface $output): void $version = $this->getVersionWithCommit(); if (!$this->no_motd) { $str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); - echo $this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str; + $this->output->writeln($this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str); } } diff --git a/src/StaticPHP/Command/Dev/SkeletonCommand.php b/src/StaticPHP/Command/Dev/SkeletonCommand.php new file mode 100644 index 000000000..b19eb4a11 --- /dev/null +++ b/src/StaticPHP/Command/Dev/SkeletonCommand.php @@ -0,0 +1,402 @@ +output->writeln('The dev:skel command is not available in phar mode.'); + return 1; + } + if (SystemTarget::getTargetOS() === 'Windows') { + $this->output->writeln('The dev:skel command is not available on Windows systems.'); + return 1; + } + + $this->runMainMenu(); + + return 0; + } + + public function validatePackageName(string $name): ?string + { + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { + return 'Library name can only contain letters, numbers, underscores, and hyphens.'; + } + // must start with a letter + if (!preg_match('/^[a-zA-Z]/', $name)) { + return 'Library name must start with a letter.'; + } + return null; + } + + private function runMainMenu(): void + { + $main = select('Please select the skeleton option', [ + 'library' => 'Create a new library package', + 'target' => 'Create a new target package', + 'php-extension' => 'Create a new PHP extension', + 'q' => 'Exit', + ]); + $generator = match ($main) { + 'library' => $this->runCreateLib(), + 'target' => $this->runCreateTarget(), + 'php-extension' => $this->runCreateExt(), + 'q' => exit(0), + default => null, + }; + $write = $generator->writeAll(); + $this->output->writeln("Package config in: {$write['package_config']}"); + $this->output->writeln("Artifact config in: {$write['artifact_config']}"); + $this->output->writeln('Package class:'); + $this->output->writeln($write['package_class_content']); + } + + private function runCreateLib(): PackageGenerator + { + // init empty + $static_libs = ''; + $headers = ''; + $static_bins = ''; + $pkg_configs = ''; + + // ask name + $package_name = text('Please enter your library name', placeholder: 'e.g. pcre2', validate: [$this, 'validatePackageName']); + + // ask OS + $os = select("[{$package_name}] On which OS family do you want to build this library?", [ + 'unix' => 'Both Linux and Darwin (unix-like OS)', + 'linux' => 'Linux only', + 'macos' => 'Darwin(macOS) only', + 'windows' => 'Windows only', + 'all' => 'All supported OS (' . implode(', ', SUPPORTED_OS_FAMILY) . ')', + ]); + + $produce = select("[{$package_name}] What does this library produce?", [ + 'static_libs' => 'Static Libraries (.a/.lib)', + 'headers' => 'Header Files (.h)', + 'static_bins' => 'Static Binaries (executables)', + 'pkg_configs' => 'Pkg-Config files (.pc)', + 'all' => 'All of the above', + ]); + + if ($produce === 'all' || $produce === 'static_libs') { + $static_libs = text( + 'Please enter the names of the static libraries produced', + placeholder: 'e.g. libpcre2.a, libbar.a', + default: str_starts_with($package_name, 'lib') ? "{$package_name}.a" : "lib{$package_name}.a", + validate: function ($value) { + $names = array_map('trim', explode(',', $value)); + if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) { + return 'Library names can only contain letters, numbers, underscores, hyphens, and dots.'; + } + return null; + }, + hint: 'Separate multiple names with commas' + ); + } + if ($produce === 'all' || $produce === 'headers') { + $headers = text( + 'Please enter the names of the header files produced', + placeholder: 'e.g. foo.h, bar.h', + default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.h' : $package_name . '.h', + validate: function ($value) { + $names = array_map('trim', explode(',', $value)); + if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) { + return 'Header file names can only contain letters, numbers, underscores, hyphens, and dots.'; + } + return null; + }, + hint: 'Separate multiple names with commas, directories are allowed (e.g. openssl directory)' + ); + } + if ($produce === 'all' || $produce === 'static_bins') { + $static_bins = text( + 'Please enter the names of the static binaries produced', + placeholder: 'e.g. foo, bar', + default: $package_name, + validate: function ($value) { + $names = array_map('trim', explode(',', $value)); + if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) { + return 'Binary names can only contain letters, numbers, underscores, hyphens, and dots.'; + } + return null; + }, + hint: 'Separate multiple names with commas' + ); + } + if ($produce === 'all' || $produce === 'pkg_configs') { + $pkg_configs = text( + 'Please enter the names of the pkg-config files produced', + placeholder: 'e.g. foo.pc, bar.pc', + default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.pc' : $package_name . '.pc', + validate: function ($value) { + if (!str_ends_with($value, '.pc')) { + return 'Pkg-config file names must end with .pc extension.'; + } + return null; + }, + hint: 'Separate multiple names with commas' + ); + } + + if ($headers === '' && $static_bins === '' && $static_libs === '' && $pkg_configs === '') { + $this->output->writeln('You must specify at least one of static libraries, header files, or static binaries produced.'); + exit(1); + } + + // ask source + $artifact_generator = $this->runCreateArtifact($package_name, true, false, null); + $package_generator = new PackageGenerator($package_name, 'library'); + // set artifact + $package_generator = $package_generator->addArtifact($artifact_generator); + // set os + $package_generator = match ($os) { + 'unix' => $package_generator->enableBuild(['Darwin', 'Linux'], 'build'), + 'linux' => $package_generator->enableBuild(['Linux'], 'build'), + 'macos' => $package_generator->enableBuild(['Darwin'], 'build'), + 'windows' => $package_generator->enableBuild(['Windows'], 'build'), + 'all' => $package_generator->enableBuild(SUPPORTED_OS_FAMILY, 'build'), + default => $package_generator, + }; + // set produce + if ($static_libs !== '') { + $lib_names = array_map('trim', explode(',', $static_libs)); + foreach ($lib_names as $lib_name) { + $package_generator = $package_generator->addStaticLib($lib_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); + } + } + if ($headers !== '') { + $header_names = array_map('trim', explode(',', $headers)); + foreach ($header_names as $header_name) { + $package_generator = $package_generator->addHeaderFile($header_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); + } + } + if ($static_bins !== '') { + $bin_names = array_map('trim', explode(',', $static_bins)); + foreach ($bin_names as $bin_name) { + $package_generator = $package_generator->addStaticBin($bin_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); + } + } + if ($pkg_configs !== '') { + $pc_names = array_map('trim', explode(',', $pkg_configs)); + foreach ($pc_names as $pc_name) { + $package_generator = $package_generator->addPkgConfigFile($pc_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); + } + } + // ask for package config writing selection, same as artifact + $package_configs = Registry::getLoadedPackageConfigs(); + $package_config_file = select("[{$package_name}] Please select the package config file to write the package config to", $package_configs); + return $package_generator->setConfigFile($package_config_file); + } + + private function runCreateArtifact( + string $package_name, + ?bool $create_source, + ?bool $create_binary, + string|true|null $default_extract_dir = true + ): ArtifactGenerator { + $artifact = new ArtifactGenerator($package_name); + + if ($create_source === null) { + $create_source = confirm("[{$package_name}] Do you want to create a source artifact?"); + } + + if (!$create_source) { + goto binary; + } + + $source_type = select("[{$package_name}] Where is the source code located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME); + + $source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'source'); + $artifact = $artifact->setSource($source_config); + + binary: + if ($create_binary === null) { + $create_binary = confirm("[{$package_name}] Do you want to create a binary artifact?"); + } + + if (!$create_binary) { + goto end; + } + + $binary_fix = [ + 'macos-x86_64' => null, + 'macos-aarch64' => null, + 'linux-x86_64' => null, + 'linux-aarch64' => null, + 'windows-x86_64' => null, + ]; + while (($os = select("[{$package_name}] Please configure the binary downloading options for OS", [ + 'macos-x86_64' => 'macos-x86_64' . ($binary_fix['macos-x86_64'] ? ' (done)' : ''), + 'macos-aarch64' => 'macos-aarch64' . ($binary_fix['macos-aarch64'] ? ' (done)' : ''), + 'linux-x86_64' => 'linux-x86_64' . ($binary_fix['linux-x86_64'] ? ' (done)' : ''), + 'linux-aarch64' => 'linux-aarch64' . ($binary_fix['linux-aarch64'] ? ' (done)' : ''), + 'windows-x86_64' => 'windows-x86_64' . ($binary_fix['windows-x86_64'] ? ' (done)' : ''), + 'copy' => 'Duplicate from another OS', + 'finish' => 'Submit', + ])) !== 'finish') { + $source_type = select("[{$package_name}] Where is the binary for {$os} located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME); + $source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'binary'); + // set to artifact + $artifact = $artifact->setBinary($os, $source_config); + $binary_fix[$os] = true; + } + + end: + + // generate config files, select existing package config file to write + $artifact_configs = Registry::getLoadedArtifactConfigs(); + $artifact_config_file = select("[{$package_name}] Please select the artifact config file to write the artifact config to", $artifact_configs); + return $artifact->setConfigFile($artifact_config_file); + } + + private function runCreateTarget(): PackageGenerator + { + throw new WrongUsageException('Not implemented'); + } + + private function runCreateExt(): PackageGenerator + { + throw new WrongUsageException('Not implemented'); + } + + private function askDownloadTypeConfig(string $package_name, int|string $source_type, bool|string|null $default_extract_dir, string $config_type): array + { + $source_config = ['type' => $source_type]; + switch ($source_type) { + case 'bitbuckettag': + $source_config['repo'] = text("[{$package_name}] Please enter the BitBucket repository (e.g. user/repo)"); + break; + case 'filelist': + $source_config['url'] = text( + "[{$package_name}] Please enter the file index website URL", + placeholder: 'e.g. https://ftp.gnu.org/pub/gnu/gettext/', + hint: 'Make sure the target url is a directory listing page like ftp.gnu.org.' + ); + $source_config['regex'] = text( + "[{$package_name}] Please enter the regex pattern to match the archive file", + placeholder: 'e.g. /gettext-(\d+\.\d+(\.\d+)?)\.tar\.gz/', + default: "/href=\"(?{$package_name}-(?[^\"]+)\\.tar\\.gz)\"/", + hint: 'Make sure the regex contains a capturing group for the version number.' + ); + break; + case 'git': + $source_config['url'] = text( + "[{$package_name}] Please enter the Git repository URL", + validate: function ($value) { + if (!filter_var($value, FILTER_VALIDATE_URL) && !preg_match('/^(git|ssh|http|https|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/', $value)) { + return 'Please enter a valid Git repository URL.'; + } + return null; + }, + hint: 'e.g. https://github.com/user/repo.git' + ); + $source_config['rev'] = text( + "[{$package_name}] Please enter the Git revision (branch, tag, or commit hash)", + default: 'main', + hint: 'e.g. main, master, v1.0.0, or a commit hash' + ); + break; + case 'ghrel': + $source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)"); + $source_config['match'] = text( + "[{$package_name}] Please enter the regex pattern to match the source archive file", + placeholder: 'e.g. /foo-(\d+\.\d+(\.\d+)?)\.tar\.gz/', + default: "{$package_name}-.+\\.tar\\.gz", + ); + break; + case 'ghtar': + case 'ghtagtar': + $source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)"); + $source_config['prefer-stable'] = confirm("[{$package_name}] Do you want to prefer stable releases?"); + if ($source_type === 'ghtagtar' && confirm('Do you want to match tags with a specific pattern?', default: false)) { + $source_config['match'] = text( + "[{$package_name}] Please enter the regex pattern to match tags", + placeholder: 'e.g. v(\d+\.\d+(\.\d+)?)', + ); + } + break; + case 'local': + $source_config['dirname'] = text( + "[{$package_name}] Please enter the local directory path", + validate: function ($value) { + if (trim($value) === '') { + return 'Local source directory cannot be empty.'; + } + if (!is_dir($value)) { + return 'The specified local source directory does not exist.'; + } + return null; + }, + ); + break; + case 'pie': + $source_config['repo'] = text( + "[{$package_name}] Please enter the PIE repository name", + placeholder: 'e.g. user/repo', + ); + break; + case 'url': + $source_config['url'] = text( + "[{$package_name}] Please enter the file download URL", + validate: function ($value) { + if (!filter_var($value, FILTER_VALIDATE_URL)) { + return 'Please enter a valid URL.'; + } + return null; + }, + ); + break; + case 'custom': + break; + } + // ask extract dir if is true + if ($default_extract_dir === true) { + if (confirm('Do you want to specify a custom extract directory?')) { + $extract_hint = match ($config_type) { + 'source' => 'the source will be from the `source/` dir by default', + 'binary' => 'the binary will be from the `pkgroot/{arch}-{os}/` dir by default', + default => '', + }; + $default_extract_dir = text( + "[{$package_name}] Please enter the source extract directory", + validate: function ($value) { + if (trim($value) === '') { + return 'Extract directory cannot be empty.'; + } + return null; + }, + hint: 'You can use relative path, ' . $extract_hint . '.' + ); + } else { + $default_extract_dir = null; + } + } + if ($default_extract_dir !== null) { + $source_config['extract'] = $default_extract_dir; + } + + // return config + return $source_config; + } +} diff --git a/src/StaticPHP/Command/Dev/SortConfigCommand.php b/src/StaticPHP/Command/Dev/SortConfigCommand.php new file mode 100644 index 000000000..aa3a9ecdb --- /dev/null +++ b/src/StaticPHP/Command/Dev/SortConfigCommand.php @@ -0,0 +1,49 @@ +sortConfigFile($file); + } + $loaded_pkg_configs = Registry::getLoadedPackageConfigs(); + foreach ($loaded_pkg_configs as $file) { + $this->sortConfigFile($file); + } + return static::SUCCESS; + } + + private function sortConfigFile(mixed $file): void + { + $content = file_get_contents($file); + if ($content === false) { + $this->output->writeln("Failed to read artifact config file: {$file}"); + return; + } + $data = json_decode($content, true); + if (!is_array($data)) { + $this->output->writeln("Invalid JSON format in artifact config file: {$file}"); + return; + } + ksort($data); + foreach ($data as $artifact_name => &$config) { + ksort($config); + } + unset($config); + $new_content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + file_put_contents($file, $new_content); + $this->output->writeln("Sorted artifact config file: {$file}"); + } +} diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index e840c0bf7..49abae926 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -11,26 +11,30 @@ class ArtifactConfig { private static array $artifact_configs = []; - public static function loadFromDir(string $dir, string $registry_name): void + public static function loadFromDir(string $dir, string $registry_name): array { if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config."); } + $loaded = []; $files = glob("{$dir}/artifact.*.json"); if (is_array($files)) { foreach ($files as $file) { self::loadFromFile($file, $registry_name); + $loaded[] = $file; } } if (file_exists("{$dir}/artifact.json")) { self::loadFromFile("{$dir}/artifact.json", $registry_name); + $loaded[] = "{$dir}/artifact.json"; } + return $loaded; } /** * Load artifact configurations from a specified JSON file. */ - public static function loadFromFile(string $file, string $registry_name): void + public static function loadFromFile(string $file, string $registry_name): string { $content = file_get_contents($file); if ($content === false) { @@ -45,6 +49,7 @@ public static function loadFromFile(string $file, string $registry_name): void self::$artifact_configs[$artifact_name] = $config; Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $file); } + return $file; } /** diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index 3342dfc78..56ef7ab1c 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -16,20 +16,24 @@ class PackageConfig * Load package configurations from a specified directory. * It will look for files matching the pattern 'pkg.*.json' and 'pkg.json'. */ - public static function loadFromDir(string $dir, string $registry_name): void + public static function loadFromDir(string $dir, string $registry_name): array { if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); } + $loaded = []; $files = glob("{$dir}/pkg.*.json"); if (is_array($files)) { foreach ($files as $file) { self::loadFromFile($file, $registry_name); + $loaded[] = $file; } } if (file_exists("{$dir}/pkg.json")) { self::loadFromFile("{$dir}/pkg.json", $registry_name); + $loaded[] = "{$dir}/pkg.json"; } + return $loaded; } /** @@ -37,7 +41,7 @@ public static function loadFromDir(string $dir, string $registry_name): void * * @param string $file the path to the json package configuration file */ - public static function loadFromFile(string $file, string $registry_name): void + public static function loadFromFile(string $file, string $registry_name): string { $content = file_get_contents($file); if ($content === false) { @@ -52,6 +56,7 @@ public static function loadFromFile(string $file, string $registry_name): void self::$package_configs[$pkg_name] = $config; Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file); } + return $file; } /** diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index bd0cfd60a..fd7650aa8 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -10,6 +10,7 @@ use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\Dev\SkeletonCommand; +use StaticPHP\Command\Dev\SortConfigCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\ExtractCommand; @@ -61,6 +62,7 @@ public function __construct() new IsInstalledCommand(), new EnvCommand(), new SkeletonCommand(), + new SortConfigCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 4c44ce920..ae3b7346a 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -133,9 +133,8 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): // show install or build options in terminal with beautiful output $this->printInstallerInfo(); - InteractiveTerm::notice('Build process will start after 2s ...'); + InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL); sleep(2); - echo PHP_EOL; } // Early validation: check if packages can be built or installed before downloading diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 909de021f..e464ed471 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -13,13 +13,39 @@ class Registry { - /** @var array List of loaded registries */ + /** @var string[] List of loaded registries */ private static array $loaded_registries = []; + /** @var array Loaded registry configs */ + private static array $registry_configs = []; + + private static array $loaded_package_configs = []; + + private static array $loaded_artifact_configs = []; + /** @var array Maps of package and artifact names to their registry config file paths (for reverse lookup) */ private static array $package_reversed_registry_files = []; + private static array $artifact_reversed_registry_files = []; + /** + * Get the current registry configuration. + * "Current" depends on SPC load mode + */ + public static function getRegistryConfig(?string $registry_name = null): array + { + if ($registry_name === null && spc_mode(SPC_MODE_SOURCE)) { + return self::$registry_configs['internal']; + } + if ($registry_name !== null && isset(self::$registry_configs[$registry_name])) { + return self::$registry_configs[$registry_name]; + } + if ($registry_name === null) { + throw new RegistryException('No registry name specified.'); + } + throw new RegistryException("Registry '{$registry_name}' is not loaded."); + } + /** * Load a registry from file path. * This method handles external registries that may not be in composer autoload. @@ -52,12 +78,14 @@ public static function loadRegistry(string $registry_file, bool $auto_require = return; } self::$loaded_registries[] = $registry_name; + self::$registry_configs[$registry_name] = $data; + self::$registry_configs[$registry_name]['_file'] = $registry_file; logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}"); // Load composer autoload if specified (for external registries with their own dependencies) if (isset($data['autoload']) && is_string($data['autoload'])) { - $autoload_path = self::fullpath($data['autoload'], dirname($registry_file)); + $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file)); if (file_exists($autoload_path)) { logger()->debug("Loading external autoload from: {$autoload_path}"); require_once $autoload_path; @@ -69,7 +97,7 @@ public static function loadRegistry(string $registry_file, bool $auto_require = // load doctor items from PSR-4 directories if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { foreach ($data['doctor']['psr-4'] as $namespace => $path) { - $path = self::fullpath($path, dirname($registry_file)); + $path = FileSystem::fullpath($path, dirname($registry_file)); DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); } } @@ -87,11 +115,11 @@ public static function loadRegistry(string $registry_file, bool $auto_require = // load package configs if (isset($data['package']['config']) && is_array($data['package']['config'])) { foreach ($data['package']['config'] as $path) { - $path = self::fullpath($path, dirname($registry_file)); + $path = FileSystem::fullpath($path, dirname($registry_file)); if (is_file($path)) { - PackageConfig::loadFromFile($path, $registry_name); + self::$loaded_package_configs[] = PackageConfig::loadFromFile($path, $registry_name); } elseif (is_dir($path)) { - PackageConfig::loadFromDir($path, $registry_name); + self::$loaded_package_configs = array_merge(self::$loaded_package_configs, PackageConfig::loadFromDir($path, $registry_name)); } } } @@ -99,11 +127,11 @@ public static function loadRegistry(string $registry_file, bool $auto_require = // load artifact configs if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) { foreach ($data['artifact']['config'] as $path) { - $path = self::fullpath($path, dirname($registry_file)); + $path = FileSystem::fullpath($path, dirname($registry_file)); if (is_file($path)) { - ArtifactConfig::loadFromFile($path, $registry_name); + self::$loaded_artifact_configs[] = ArtifactConfig::loadFromFile($path, $registry_name); } elseif (is_dir($path)) { - ArtifactConfig::loadFromDir($path, $registry_name); + self::$loaded_package_configs = array_merge(self::$loaded_package_configs, ArtifactConfig::loadFromDir($path, $registry_name)); } } } @@ -111,7 +139,7 @@ public static function loadRegistry(string $registry_file, bool $auto_require = // load packages from PSR-4 directories if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) { foreach ($data['package']['psr-4'] as $namespace => $path) { - $path = self::fullpath($path, dirname($registry_file)); + $path = FileSystem::fullpath($path, dirname($registry_file)); PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require); } } @@ -129,7 +157,7 @@ public static function loadRegistry(string $registry_file, bool $auto_require = // load artifacts from PSR-4 directories if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) { foreach ($data['artifact']['psr-4'] as $namespace => $path) { - $path = self::fullpath($path, dirname($registry_file)); + $path = FileSystem::fullpath($path, dirname($registry_file)); ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require); } } @@ -147,7 +175,7 @@ public static function loadRegistry(string $registry_file, bool $auto_require = // load additional commands from PSR-4 directories if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) { foreach ($data['command']['psr-4'] as $namespace => $path) { - $path = self::fullpath($path, dirname($registry_file)); + $path = FileSystem::fullpath($path, dirname($registry_file)); $classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require); $instances = array_map(fn ($x) => new $x(), $classes); ConsoleApplication::_addAdditionalCommands($instances); @@ -262,6 +290,16 @@ public static function getArtifactConfigInfo(string $artifact_name): ?array return self::$artifact_reversed_registry_files[$artifact_name] ?? null; } + public static function getLoadedPackageConfigs(): array + { + return self::$loaded_package_configs; + } + + public static function getLoadedArtifactConfigs(): array + { + return self::$loaded_artifact_configs; + } + /** * Parse a class entry from the classes array. * Supports two formats: @@ -298,7 +336,7 @@ private static function requireClassFile(string $class, ?string $file_path, stri // If file path is provided, require it if ($file_path !== null) { - $full_path = self::fullpath($file_path, $base_path); + $full_path = FileSystem::fullpath($file_path, $base_path); require_once $full_path; return; } @@ -311,21 +349,4 @@ private static function requireClassFile(string $class, ?string $file_path, stri " 3. Provide file path in classes map: \"{$class}\": \"path/to/file.php\"" ); } - - /** - * Return full path, resolving relative paths against a base path. - * - * @param string $path Input path (relative or absolute) - * @param string $relative_path_base Base path for relative paths - */ - private static function fullpath(string $path, string $relative_path_base): string - { - if (FileSystem::isRelativePath($path)) { - $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; - } - if (!file_exists($path)) { - throw new RegistryException("Path does not exist: {$path}"); - } - return FileSystem::convertPath($path); - } } diff --git a/src/StaticPHP/Skeleton/ArtifactGenerator.php b/src/StaticPHP/Skeleton/ArtifactGenerator.php index 031d095bb..e05f94d14 100644 --- a/src/StaticPHP/Skeleton/ArtifactGenerator.php +++ b/src/StaticPHP/Skeleton/ArtifactGenerator.php @@ -1,11 +1,21 @@ source; } - public function generateConfig(): array + public function setBinary(string $os, array $config): static + { + $clone = clone $this; + if ($clone->binary === null) { + $clone->binary = [$os => $config]; + } else { + $clone->binary[$os] = $config; + } + return $clone; + } + + public function generateConfigArray(): array { $config = []; if ($this->source) { $config['source'] = $this->source; } + if ($this->binary) { + $config['binary'] = $this->binary; + } return $config; } + + public function setConfigFile(string $file): static + { + $clone = clone $this; + $clone->config_file = $file; + return $clone; + } + + /** + * Write the artifact configuration to the config file. + */ + public function writeConfigFile(): string + { + if ($this->config_file === null) { + throw new ValidationException('Config file path is not set.'); + } + $config_array = $this->generateConfigArray(); + $config_file_json = json_decode(FileSystem::readFile($this->config_file), true); + if (!is_array($config_file_json)) { + throw new ValidationException('Existing config file is not a valid JSON array.'); + } + + $config_file_json[$this->name] = $config_array; + // sort keys + ksort($config_file_json); + $json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($json_content === false) { + throw new ValidationException('Failed to encode config array to JSON.'); + } + if (file_put_contents($this->config_file, $json_content) === false) { + throw new FileSystemException("Failed to write config file: {$this->config_file}"); + } + return $this->config_file; + } } diff --git a/src/StaticPHP/Skeleton/ExecutorGenerator.php b/src/StaticPHP/Skeleton/ExecutorGenerator.php index 02edf2ebd..69d7a9f89 100644 --- a/src/StaticPHP/Skeleton/ExecutorGenerator.php +++ b/src/StaticPHP/Skeleton/ExecutorGenerator.php @@ -1,9 +1,14 @@ class) { + UnixCMakeExecutor::class => [UnixCMakeExecutor::class, 'UnixCMakeExecutor::create($package)->build();'], + UnixAutoconfExecutor::class => [UnixAutoconfExecutor::class, 'UnixAutoconfExecutor::create($package)->build();'], + WindowsCMakeExecutor::class => [WindowsCMakeExecutor::class, 'WindowsCMakeExecutor::create($package)->build();'], + default => throw new ValidationException("Unsupported executor class: {$this->class}"), + }; + } } diff --git a/src/StaticPHP/Skeleton/PackageGenerator.php b/src/StaticPHP/Skeleton/PackageGenerator.php index 95d3c87f1..89802899a 100644 --- a/src/StaticPHP/Skeleton/PackageGenerator.php +++ b/src/StaticPHP/Skeleton/PackageGenerator.php @@ -1,33 +1,49 @@ $depends An array of dependencies required by the package, categorized by operating system. */ + /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $depends An array of dependencies required by the package, categorized by operating system. */ protected array $depends = []; - /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */ + /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */ protected array $suggests = []; /** @var array $frameworks An array of macOS frameworks for the package */ protected array $frameworks = []; - /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */ + /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */ protected array $static_libs = []; - /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $headers An array of header files required by the package, categorized by operating system. */ + /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $headers An array of header files required by the package, categorized by operating system. */ protected array $headers = []; - /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */ + /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */ protected array $static_bins = []; - /** @var ArtifactGenerator|null $artifact Artifact */ + protected ?string $config_file = null; + + /** @var null|ArtifactGenerator $artifact Artifact */ protected ?ArtifactGenerator $artifact = null; /** @var array $licenses Licenses */ @@ -44,28 +60,28 @@ class PackageGenerator protected array $func_executor_binding = []; /** - * @param string $package_name Package name - * @param 'library'|'target'|'virtual-target'|'php-extension' $type Package type ('library', 'target', 'virtual-target', etc.) + * @param string $package_name Package name + * @param 'library'|'php-extension'|'target'|'virtual-target' $type Package type ('library', 'target', 'virtual-target', etc.) */ public function __construct(protected string $package_name, protected string $type) {} /** * Add package dependency. * - * @param string $package Package name - * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos') + * @param string $package Package name + * @param string $os_category Operating system ('' for all OSes, 'unix', 'windows', 'macos') */ - public function addDependency(string $package, string $os = ''): static + public function addDependency(string $package, string $os_category = ''): static { - if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { - throw new ValidationException("Invalid OS suffix: {$os}"); + if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { + throw new ValidationException("Invalid OS suffix: {$os_category}"); } $clone = clone $this; - if (!isset($clone->depends[$os])) { - $clone->depends[$os] = []; + if (!isset($clone->depends[$os_category])) { + $clone->depends[$os_category] = []; } - if (!in_array($package, $clone->depends[$os], true)) { - $clone->depends[$os][] = $package; + if (!in_array($package, $clone->depends[$os_category], true)) { + $clone->depends[$os_category][] = $package; } return $clone; } @@ -73,78 +89,78 @@ public function addDependency(string $package, string $os = ''): static /** * Add package suggestion. * - * @param string $package Package name - * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos') + * @param string $package Package name + * @param string $os_category Operating system ('' for all OSes) */ - public function addSuggestion(string $package, string $os = ''): static + public function addSuggestion(string $package, string $os_category = ''): static { - if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { - throw new ValidationException("Invalid OS suffix: {$os}"); + if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { + throw new ValidationException("Invalid OS suffix: {$os_category}"); } $clone = clone $this; - if (!isset($clone->suggests[$os])) { - $clone->suggests[$os] = []; + if (!isset($clone->suggests[$os_category])) { + $clone->suggests[$os_category] = []; } - if (!in_array($package, $clone->suggests[$os], true)) { - $clone->suggests[$os][] = $package; + if (!in_array($package, $clone->suggests[$os_category], true)) { + $clone->suggests[$os_category][] = $package; } return $clone; } - public function addStaticLib(string $lib_a, string $os = ''): static + public function addStaticLib(string $lib_a, string $os_category = ''): static { - if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { - throw new ValidationException("Invalid OS suffix: {$os}"); + if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { + throw new ValidationException("Invalid OS suffix: {$os_category}"); } if (!str_ends_with($lib_a, '.lib') && !str_ends_with($lib_a, '.a')) { throw new ValidationException("Static library must end with .lib or .a, got: {$lib_a}"); } - if (str_ends_with($lib_a, '.lib') && in_array($os, ['unix', 'linux', 'macos'], true)) { + if (str_ends_with($lib_a, '.lib') && in_array($os_category, ['unix', 'linux', 'macos'], true)) { throw new ValidationException("Static library with .lib extension cannot be added for non-Windows OS: {$lib_a}"); } - if (str_ends_with($lib_a, '.a') && $os === 'windows') { + if (str_ends_with($lib_a, '.a') && $os_category === 'windows') { throw new ValidationException("Static library with .a extension cannot be added for Windows OS: {$lib_a}"); } - if (isset($this->static_libs[$os]) && in_array($lib_a, $this->static_libs[$os], true)) { + if (isset($this->static_libs[$os_category]) && in_array($lib_a, $this->static_libs[$os_category], true)) { // already exists return $this; } $clone = clone $this; - if (!isset($clone->static_libs[$os])) { - $clone->static_libs[$os] = []; + if (!isset($clone->static_libs[$os_category])) { + $clone->static_libs[$os_category] = []; } - if (!in_array($lib_a, $clone->static_libs[$os], true)) { - $clone->static_libs[$os][] = $lib_a; + if (!in_array($lib_a, $clone->static_libs[$os_category], true)) { + $clone->static_libs[$os_category][] = $lib_a; } return $clone; } - public function addHeader(string $header_file, string $os = ''): static + public function addHeader(string $header_file, string $os_category = ''): static { - if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { - throw new ValidationException("Invalid OS suffix: {$os}"); + if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { + throw new ValidationException("Invalid OS suffix: {$os_category}"); } $clone = clone $this; - if (!isset($clone->headers[$os])) { - $clone->headers[$os] = []; + if (!isset($clone->headers[$os_category])) { + $clone->headers[$os_category] = []; } - if (!in_array($header_file, $clone->headers[$os], true)) { - $clone->headers[$os][] = $header_file; + if (!in_array($header_file, $clone->headers[$os_category], true)) { + $clone->headers[$os_category][] = $header_file; } return $clone; } - public function addStaticBin(string $bin_file, string $os = ''): static + public function addStaticBin(string $bin_file, string $os_category = ''): static { - if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { - throw new ValidationException("Invalid OS suffix: {$os}"); + if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { + throw new ValidationException("Invalid OS suffix: {$os_category}"); } $clone = clone $this; - if (!isset($clone->static_bins[$os])) { - $clone->static_bins[$os] = []; + if (!isset($clone->static_bins[$os_category])) { + $clone->static_bins[$os_category] = []; } - if (!in_array($bin_file, $clone->static_bins[$os], true)) { - $clone->static_bins[$os][] = $bin_file; + if (!in_array($bin_file, $clone->static_bins[$os_category], true)) { + $clone->static_bins[$os_category][] = $bin_file; } return $clone; } @@ -194,9 +210,9 @@ public function addLicenseFromFile(string $file_path): static /** * Enable build for specific OS. * - * @param 'Windows'|'Linux'|'Darwin'|array<'Windows'|'Linux'|'Darwin'> $build_for Build for OS + * @param 'Darwin'|'Linux'|'Windows'|array<'Darwin'|'Linux'|'Windows'> $build_for Build for OS */ - public function enableBuild(string|array $build_for, ?string $build_function_name = null): static + public function enableBuild(array|string $build_for, ?string $build_function_name = null): static { $clone = clone $this; if (is_array($build_for)) { @@ -205,6 +221,9 @@ public function enableBuild(string|array $build_for, ?string $build_function_nam } return $clone; } + if (!in_array($build_for, SUPPORTED_OS_FAMILY, true)) { + throw new ValidationException("Unsupported build_for value: {$build_for}"); + } $clone->build_for_enables[$build_for] = $build_function_name ?? "buildFor{$build_for}"; return $clone; } @@ -212,8 +231,8 @@ public function enableBuild(string|array $build_for, ?string $build_function_nam /** * Bind function executor. * - * @param string $func_name Function name - * @param ExecutorGenerator $executor Executor generator + * @param string $func_name Function name + * @param ExecutorGenerator $executor Executor generator */ public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator $executor): static { @@ -222,10 +241,73 @@ public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator return $clone; } + public function generatePackageClassFile(string $namespace, bool $uppercase = false): string + { + $printer = new class extends Printer { + public string $indentation = ' '; + }; + $file = new PhpFile(); + $namespace = $file->setStrictTypes()->addNamespace($namespace); + + $uses = []; + + // class name and package attribute + $class_name = str_replace('-', '_', $uppercase ? ucwords($this->package_name, '-') : $this->package_name); + $class_attribute = match ($this->type) { + 'library' => Library::class, + 'php-extension' => Extension::class, + 'target', 'virtual-target' => Target::class, + }; + $package_class = match ($this->type) { + 'library' => LibraryPackage::class, + 'php-extension' => PhpExtensionPackage::class, + 'target', 'virtual-target' => TargetPackage::class, + }; + $uses[] = $class_attribute; + $uses[] = $package_class; + $uses[] = BuildFor::class; + $uses[] = PackageInstaller::class; + + foreach ($uses as $use) { + $namespace->addUse($use); + } + + // add class attribute + $class = $namespace->addClass($class_name); + $class->addAttribute($class_attribute, [$this->package_name]); + + // add build functions if enabled + $funcs = []; + foreach ($this->build_for_enables as $os_family => $func_name) { + if ($func_name !== null) { + $funcs[$func_name][] = $os_family; + } + } + foreach ($funcs as $name => $oss) { + $method = $class->addMethod(name: $name ?: 'build') + ->setPublic() + ->setReturnType('void'); + // check if function executor is bound + if (isset($this->func_executor_binding[$name])) { + $executor = $this->func_executor_binding[$name]; + [$executor_use, $code] = $executor->generateCode(); + $namespace->addUse($executor_use); + $method->setBody($code); + } + $method->addParameter('package')->setType($package_class); + $method->addParameter('installer')->setType(PackageInstaller::class); + foreach ($oss as $os) { + $method->addAttribute(BuildFor::class, [$os]); + } + } + + return $printer->printFile($file); + } + /** * Generate package config */ - public function generateConfig(): array + public function generateConfigArray(): array { $config = ['type' => $this->type]; @@ -280,4 +362,51 @@ public function generateConfig(): array return $config; } + + public function setConfigFile(string $config_file): static + { + $clone = clone $this; + $clone->config_file = $config_file; + return $clone; + } + + public function writeConfigFile(): string + { + if ($this->config_file === null) { + throw new ValidationException('Config file path is not set.'); + } + $config_array = $this->generateConfigArray(); + $config_file_json = json_decode(FileSystem::readFile($this->config_file), true); + if (!is_array($config_file_json)) { + throw new ValidationException('Existing config file is not a valid JSON array.'); + } + $config_file_json[$this->package_name] = $config_array; + ksort($config_file_json); + $json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($json_content === false) { + throw new ValidationException('Failed to encode package config to JSON.'); + } + if (file_put_contents($this->config_file, $json_content) === false) { + throw new FileSystemException("Failed to write config file: {$this->config_file}"); + } + return $this->config_file; + } + + public function writeAll(): array + { + // write config + $package_config_file = $this->writeConfigFile(); + $artifact_config_file = $this->artifact->writeConfigFile(); + + // write class file + $package_class_file_content = $this->generatePackageClassFile('StaticPHP\Packages'); + $package_class_file_path = str_replace('-', '_', $this->package_name) . '.php'; + // file_put_contents($package_class_file_path, $package_class_file_content); // Uncomment this line to actually write the file + return [ + 'package_config' => $package_config_file, + 'artifact_config' => $artifact_config_file, + 'package_class_file' => $package_class_file_path, + 'package_class_content' => $package_class_file_content, + ]; + } } diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 2f540d70d..1c21a92b5 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -472,6 +472,23 @@ public static function replaceFileLineContainsString(string $file, string $find, return file_put_contents($file, implode('', $lines)); } + /** + * Return full path, resolving relative paths against a base path. + * + * @param string $path Input path (relative or absolute) + * @param string $relative_path_base Base path for relative paths + */ + public static function fullpath(string $path, string $relative_path_base): string + { + if (FileSystem::isRelativePath($path)) { + $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; + } + if (!file_exists($path)) { + throw new FileSystemException("Path does not exist: {$path}"); + } + return FileSystem::convertPath($path); + } + private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int { logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename); diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 1682ed1f6..0570f31c6 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -52,6 +52,7 @@ public static function plain(string $message, string $level = 'info'): void default => logger()->info(strip_ansi_colors($message)), }; } else { + $output = $level === 'error' && $output instanceof ConsoleOutput ? $output->getErrorOutput() : $output; $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } } diff --git a/src/globals/defines.php b/src/globals/defines.php index 3e6d23605..dbcb63f22 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -96,13 +96,32 @@ const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [ 'bitbuckettag' => 'BitBucket', - 'filelist' => 'website', + 'filelist' => 'File index website', 'git' => 'git', 'ghrel' => 'GitHub release', - 'ghtar', 'ghtagtar' => 'GitHub tarball', + 'ghtar' => 'GitHub release tarball', + 'ghtagtar' => 'GitHub tag tarball', 'local' => 'local dir', - 'pie' => 'PHP Installer for Extensions', + 'pie' => 'PHP Installer for Extensions (PIE)', 'url' => 'url', 'php-release' => 'php.net', 'custom' => 'custom downloader', ]; + +const SUPPORTED_OS_CATEGORY = [ + 'unix', + 'windows', + 'linux', + 'macos', +]; + +const SUPPORTED_OS_FAMILY = [ + 'Linux', + 'Darwin', + 'Windows', +]; + +const SPC_MODE_SOURCE = 1; +const SPC_MODE_VENDOR = 2; +const SPC_MODE_PHAR = 4; +const SPC_MODE_VENDOR_PHAR = SPC_MODE_VENDOR | SPC_MODE_PHAR; diff --git a/src/globals/functions.php b/src/globals/functions.php index 93cd1ae09..bb22f3a71 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -10,6 +10,31 @@ use StaticPHP\Runtime\Shell\WindowsCmd; use ZM\Logger\ConsoleLogger; +/** + * Get the current SPC loading mode. If passed a mode to check, will return whether current mode matches the given mode. + */ +function spc_mode(?int $check_mode = null): bool|int +{ + $mode = SPC_MODE_SOURCE; + // if current file is in phar, then it's phar mode + if (str_starts_with(__FILE__, 'phar://') && Phar::running()) { + // judge whether it's vendor mode (inside vendor/) or source mode (inside src/) + if (basename(dirname(__FILE__, 3)) === 'static-php-cli' && basename(dirname(__FILE__, 5)) === 'vendor') { + $mode = SPC_MODE_VENDOR_PHAR; + } else { + $mode = SPC_MODE_PHAR; + } + } elseif (basename(dirname(__FILE__, 3)) === 'static-php-cli' && basename(dirname(__FILE__, 5)) === 'vendor') { + $mode = SPC_MODE_VENDOR; + } + + if ($check_mode === null) { + return $mode; + } + // use bitwise AND to check mode + return ($mode & $check_mode) !== 0; +} + /** * Judge if an array is an associative array */ From 9fdfef50574d6eed60d707e0f0d2a304c7c7825d Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Thu, 18 Dec 2025 21:21:47 +0800 Subject: [PATCH 101/682] macOS don't need to disable avx2 explicitly (#1007) --- src/SPC/ConsoleApplication.php | 2 +- src/SPC/builder/unix/library/libwebp.php | 2 +- src/globals/test-extensions.php | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 7fc56ae3c..9500a231a 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.7.10'; + public const string VERSION = '2.7.11'; public function __construct() { diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 47fd0078c..068185829 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -13,7 +13,7 @@ protected function build(): void $code = 'int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }'; $cc = getenv('CC') ?: 'gcc'; [$ret] = shell()->execWithResult("echo '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1"); - $disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64'; + $disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64' && PHP_OS_FAMILY === 'Linux'; UnixCMakeExecutor::create($this) ->addConfigureArgs( diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 72e751244..cdd27e235 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -23,13 +23,13 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - // 'macos-15-intel', // bin/spc for x86_64 + 'macos-15-intel', // bin/spc for x86_64 // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - 'ubuntu-24.04', // bin/spc for x86_64 - 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - 'ubuntu-24.04-arm', // bin/spc for arm64 + // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + // 'ubuntu-24.04', // bin/spc for x86_64 + // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 // 'windows-2025', ]; From 656a58c3fad9e5c4075ba9fd79c7ec4830e0048f Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 09:58:45 +0100 Subject: [PATCH 102/682] remove source dir after successful build in CI environment --- src/SPC/builder/LibraryBase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index c88e8d96c..0018868fa 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -195,6 +195,9 @@ public function tryBuild(bool $force_build = false): int $this->getBuilder()->emitPatchPoint('before-library[ ' . static::NAME . ']-build'); $this->build(); $this->installLicense(); + if (getenv('CI')) { + FileSystem::removeDir($this->source_dir); + } $this->getBuilder()->emitPatchPoint('after-library[ ' . static::NAME . ']-build'); return LIB_STATUS_OK; } From 024745885321448a70cdfc7e0a9b514a1bae21cf Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 11:06:21 +0100 Subject: [PATCH 103/682] we were installing to wrong dir if source name != lib name --- src/SPC/builder/LibraryBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index 0018868fa..9d676b245 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -357,11 +357,11 @@ protected function installLicense(): void } foreach ($license_files as $index => $license) { if ($license['type'] === 'text') { - FileSystem::writeFile(BUILD_ROOT_PATH . '/source-licenses/' . $this->getName() . "/{$index}.txt", $license['text']); + FileSystem::writeFile(BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt", $license['text']); continue; } if ($license['type'] === 'file') { - copy($this->source_dir . '/' . $license['path'], BUILD_ROOT_PATH . '/source-licenses/' . $this->getName() . "/{$index}.txt"); + copy($this->source_dir . '/' . $license['path'], BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt"); } } } From ce44e00bd40afeb03d7a27671510f1b6846d3a85 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 12:35:06 +0100 Subject: [PATCH 104/682] @crazywhalecc how to use patch points to delete source dirs? --- src/SPC/builder/Extension.php | 3 +++ src/SPC/builder/LibraryBase.php | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 08b403e61..0df87e912 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -385,6 +385,9 @@ public function buildShared(array $visited = []): void logger()->info('Shared extension [' . $this->getName() . '] was already built, skipping (' . $this->getName() . '.so)'); return; } + if (Config::getExt($this->getName(), 'type') === 'addon') { + return; + } logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)'); foreach ($this->dependencies as $dependency) { if (!$dependency instanceof Extension) { diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index 9d676b245..81abceee0 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -184,21 +184,21 @@ public function tryBuild(bool $force_build = false): int // extract first if not exists if (!is_dir($this->source_dir)) { - $this->getBuilder()->emitPatchPoint('before-library[ ' . static::NAME . ']-extract'); + $this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-extract'); SourceManager::initSource(libs: [static::NAME], source_only: true); - $this->getBuilder()->emitPatchPoint('after-library[ ' . static::NAME . ']-extract'); + $this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-extract'); } if (!$this->patched && $this->patchBeforeBuild()) { file_put_contents($this->source_dir . '/.spc.patched', 'PATCHED!!!'); } - $this->getBuilder()->emitPatchPoint('before-library[ ' . static::NAME . ']-build'); + $this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-build'); $this->build(); $this->installLicense(); if (getenv('CI')) { FileSystem::removeDir($this->source_dir); } - $this->getBuilder()->emitPatchPoint('after-library[ ' . static::NAME . ']-build'); + $this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-build'); return LIB_STATUS_OK; } @@ -349,8 +349,8 @@ protected function getSnakeCaseName(): string */ protected function installLicense(): void { - FileSystem::createDir(BUILD_ROOT_PATH . '/source-licenses/' . $this->getName()); $source = Config::getLib($this->getName(), 'source'); + FileSystem::createDir(BUILD_ROOT_PATH . "/source-licenses/{$source}"); $license_files = Config::getSource($source)['license'] ?? []; if (is_assoc_array($license_files)) { $license_files = [$license_files]; From 037d224fd7d6bfbb6b82b314d1d68d5668421d5e Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 12:38:17 +0100 Subject: [PATCH 105/682] why does phpstan think this is necessary? --- src/SPC/builder/Extension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 0df87e912..e79b886bf 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -385,7 +385,7 @@ public function buildShared(array $visited = []): void logger()->info('Shared extension [' . $this->getName() . '] was already built, skipping (' . $this->getName() . '.so)'); return; } - if (Config::getExt($this->getName(), 'type') === 'addon') { + if ((string) Config::getExt($this->getName(), 'type') === 'addon') { return; } logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)'); From e677be74d7e1f85d5503a285f4fd8998e4c8428f Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 13:07:21 +0100 Subject: [PATCH 106/682] remove --- src/SPC/builder/LibraryBase.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index 81abceee0..d34663021 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -195,9 +195,6 @@ public function tryBuild(bool $force_build = false): int $this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-build'); $this->build(); $this->installLicense(); - if (getenv('CI')) { - FileSystem::removeDir($this->source_dir); - } $this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-build'); return LIB_STATUS_OK; } From 9e051c8c8068bb97a2c0533a78fa82cceab70dab Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 14:52:02 +0100 Subject: [PATCH 107/682] fix: check for link first before checking for is_dir --- src/SPC/store/FileSystem.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index f6c538bdf..1d0815ce2 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -408,13 +408,13 @@ public static function removeDir(string $dir): bool continue; } $sub_file = self::convertPath($dir . '/' . $v); - if (is_dir($sub_file)) { - # 如果是 目录 且 递推 , 则递推添加下级文件 - if (!self::removeDir($sub_file)) { + if (is_link($sub_file) || is_file($sub_file)) { + if (!unlink($sub_file)) { return false; } - } elseif (is_link($sub_file) || is_file($sub_file)) { - if (!unlink($sub_file)) { + } elseif (is_dir($sub_file)) { + # 如果是 目录 且 递推 , 则递推添加下级文件 + if (!self::removeDir($sub_file)) { return false; } } From e1a14bbb9fa9522ccfde72f8e69d689b6c158ca6 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 17:39:05 +0100 Subject: [PATCH 108/682] fix implicit include --- src/SPC/builder/unix/library/libwebp.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php index 068185829..015fa73ba 100644 --- a/src/SPC/builder/unix/library/libwebp.php +++ b/src/SPC/builder/unix/library/libwebp.php @@ -10,9 +10,10 @@ trait libwebp { protected function build(): void { - $code = 'int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }'; + $code = '#include +int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }'; $cc = getenv('CC') ?: 'gcc'; - [$ret] = shell()->execWithResult("echo '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1"); + [$ret] = shell()->execWithResult("printf '%s' '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1"); $disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64' && PHP_OS_FAMILY === 'Linux'; UnixCMakeExecutor::create($this) From 53f7cdefe0e791d621d86cc2ce6938d228ec3b19 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 18 Dec 2025 20:12:01 +0100 Subject: [PATCH 109/682] fix swoole compilation with php 8.5.1 --- src/SPC/builder/extension/swoole.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index f6ff5931d..4e292a362 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -17,6 +17,7 @@ class swoole extends Extension public function patchBeforeMake(): bool { $patched = parent::patchBeforeMake(); + FileSystem::replaceFileStr($this->source_dir . '/ext-src/php_swoole_private.h', 'PHP_VERSION_ID > 80500', 'PHP_VERSION_ID >= 80600'); if ($this->builder instanceof MacOSBuilder) { // Fix swoole with event extension conflict bug $util_path = shell()->execWithResult('xcrun --show-sdk-path', false)[1][0] . '/usr/include/util.h'; From 6b5200002e6d744a450038e982c3c88d386ef3e6 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 20 Dec 2025 23:29:25 +0100 Subject: [PATCH 110/682] fix downloader selecting drafts --- composer.lock | 428 ++++++++++++++++++----------------- src/SPC/store/Downloader.php | 3 + 2 files changed, 224 insertions(+), 207 deletions(-) diff --git a/composer.lock b/composer.lock index e8f320c7c..a69833c7c 100644 --- a/composer.lock +++ b/composer.lock @@ -8,7 +8,7 @@ "packages": [ { "name": "illuminate/collections", - "version": "v11.46.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -64,7 +64,7 @@ }, { "name": "illuminate/conditionable", - "version": "v11.46.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -110,16 +110,16 @@ }, { "name": "illuminate/contracts", - "version": "v11.46.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8" + "reference": "4787042340aae19a7ea0fa82f4073c4826204a48" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/4b2a67d1663f50085bc91e6371492697a5d2d4e8", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/4787042340aae19a7ea0fa82f4073c4826204a48", + "reference": "4787042340aae19a7ea0fa82f4073c4826204a48", "shasum": "" }, "require": { @@ -154,11 +154,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-11-27T16:16:07+00:00" }, { "name": "illuminate/macroable", - "version": "v11.46.1", + "version": "v11.47.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -416,16 +416,16 @@ }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", "shasum": "" }, "require": { @@ -433,7 +433,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -447,16 +447,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -490,7 +490,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.1" }, "funding": [ { @@ -510,7 +510,7 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-12-05T15:23:39+00:00" }, { "name": "symfony/deprecation-contracts", @@ -916,16 +916,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -957,7 +957,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.0" }, "funding": [ { @@ -977,7 +977,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/service-contracts", @@ -1068,34 +1068,34 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -1134,7 +1134,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -1154,32 +1154,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -1210,7 +1210,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -1230,7 +1230,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "zhamao/logger", @@ -1615,16 +1615,16 @@ }, { "name": "amphp/parallel", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce" + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/321b45ae771d9c33a068186b24117e3cd1c48dce", - "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce", + "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", "shasum": "" }, "require": { @@ -1687,7 +1687,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.2" + "source": "https://github.com/amphp/parallel/tree/v2.3.3" }, "funding": [ { @@ -1695,7 +1695,7 @@ "type": "github" } ], - "time": "2025-08-27T21:55:40+00:00" + "time": "2025-11-15T06:23:42+00:00" }, { "name": "amphp/parser", @@ -2113,7 +2113,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.25.11", + "version": "5.27.3", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2167,7 +2167,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.25.11" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.27.3" }, "funding": [ { @@ -2864,16 +2864,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.89.2", + "version": "v3.92.3", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf" + "reference": "2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7569658f91e475ec93b99bd5964b059ad1336dcf", - "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8", + "reference": "2ba8f5a60f6f42fb65758cfb3768434fa2d1c7e8", "shasum": "" }, "require": { @@ -2891,17 +2891,17 @@ "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", - "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0", - "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", "symfony/polyfill-php84": "^1.33", - "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", - "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", @@ -2913,8 +2913,9 @@ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", - "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2929,7 +2930,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2955,7 +2956,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.3" }, "funding": [ { @@ -2963,7 +2964,7 @@ "type": "github" } ], - "time": "2025-11-06T21:12:50+00:00" + "time": "2025-12-18T10:45:02+00:00" }, { "name": "humbug/box", @@ -3212,21 +3213,21 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.1", + "version": "6.6.3", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" + "reference": "134e98916fa2f663afa623970af345cd788e8967" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/134e98916fa2f663afa623970af345cd788e8967", + "reference": "134e98916fa2f663afa623970af345cd788e8967", "shasum": "" }, "require": { "ext-json": "*", - "marc-mabe/php-enum": "^4.0", + "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" }, "require-dev": { @@ -3281,9 +3282,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.3" }, - "time": "2025-11-07T18:30:29+00:00" + "time": "2025-12-02T10:21:33+00:00" }, { "name": "kelunik/certificate", @@ -3345,33 +3346,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.7", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3399,6 +3405,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -3411,9 +3418,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -3423,7 +3432,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.7.0" }, "funding": [ { @@ -3431,26 +3440,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-12-07T16:02:06+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -3458,6 +3466,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3482,7 +3491,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -3507,7 +3516,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" }, "funding": [ { @@ -3515,7 +3524,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-12-07T16:03:21+00:00" }, { "name": "marc-mabe/php-enum", @@ -3704,16 +3713,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3756,9 +3765,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/composer-distributor", @@ -4147,16 +4156,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.5", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", "shasum": "" }, "require": { @@ -4205,22 +4214,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-11-27T19:50:05+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { @@ -4263,9 +4272,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-21T15:09:14+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -4690,16 +4699,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.60", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "f2e26f52f80ef77832e359205f216eeac00e320c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f2e26f52f80ef77832e359205f216eeac00e320c", + "reference": "f2e26f52f80ef77832e359205f216eeac00e320c", "shasum": "" }, "require": { @@ -4771,7 +4780,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.60" }, "funding": [ { @@ -4795,7 +4804,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2025-12-06T07:50:42+00:00" }, { "name": "psr/event-dispatcher", @@ -5104,16 +5113,16 @@ }, { "name": "react/dns", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { @@ -5168,7 +5177,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { @@ -5176,20 +5185,20 @@ "type": "open_collective" } ], - "time": "2024-06-13T14:18:03+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { "name": "react/event-loop", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { @@ -5240,7 +5249,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { @@ -5248,7 +5257,7 @@ "type": "open_collective" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { "name": "react/promise", @@ -5325,16 +5334,16 @@ }, { "name": "react/socket", - "version": "v1.16.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { @@ -5393,7 +5402,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { @@ -5401,7 +5410,7 @@ "type": "open_collective" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { "name": "react/stream", @@ -5483,16 +5492,16 @@ }, { "name": "revolt/event-loop", - "version": "v1.0.7", + "version": "v1.0.8", "source": { "type": "git", "url": "https://github.com/revoltphp/event-loop.git", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", "shasum": "" }, "require": { @@ -5549,9 +5558,9 @@ ], "support": { "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" }, - "time": "2025-01-25T19:27:39+00:00" + "time": "2025-08-27T21:33:23+00:00" }, { "name": "sebastian/cli-parser", @@ -6620,24 +6629,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -6646,13 +6655,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -6680,7 +6690,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -6700,7 +6710,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6780,16 +6790,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -6798,7 +6808,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6826,7 +6836,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" }, "funding": [ { @@ -6846,27 +6856,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6894,7 +6904,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -6914,24 +6924,24 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -6965,7 +6975,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -6985,7 +6995,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-iconv", @@ -7153,20 +7163,20 @@ }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -7195,7 +7205,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" }, "funding": [ { @@ -7206,25 +7216,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-24T10:49:57+00:00" + "time": "2025-08-04T07:36:47+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -7236,10 +7250,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -7278,7 +7292,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -7298,7 +7312,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "thecodingmachine/safe", @@ -7441,16 +7455,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -7479,7 +7493,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.0" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -7487,7 +7501,7 @@ "type": "github" } ], - "time": "2025-11-13T13:44:09+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/assert", diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index 63bec8075..a2911e46b 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -108,6 +108,9 @@ public static function getLatestGithubTarball(string $name, array $source, strin if (($rel['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) { continue; } + if (($rel['draft'] ?? false) === true && (($source['prefer-stable'] ?? false) || !$rel['tarball_url'])) { + continue; + } if (!($source['match'] ?? null)) { $url = $rel['tarball_url'] ?? null; break; From f7ca621efec86cd5f24ec69410ac86d7644e0cf8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 26 Dec 2025 15:03:54 +0800 Subject: [PATCH 111/682] Test --- src/globals/test-extensions.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index cdd27e235..0402eaf37 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,10 +13,10 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - // '8.1', + '8.1', // '8.2', // '8.3', - // '8.4', + '8.4', '8.5', // 'git', ]; @@ -24,10 +24,10 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ 'macos-15-intel', // bin/spc for x86_64 - // 'macos-15', // bin/spc for arm64 - // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 + 'macos-15', // bin/spc for arm64 + 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'imagick', + 'Linux', 'Darwin' => 'swoole,imagick', 'Windows' => 'bcmath', }; From 8650ce4f8ff7c61ce47eb80a648b1ea386f0d1c2 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 26 Dec 2025 17:15:45 +0800 Subject: [PATCH 112/682] Add MACOSX_DEPLOYMENT_TARGET to env.ini (#1009) --- config/env.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/env.ini b/config/env.ini index 7448cc373..4b0795f8e 100644 --- a/config/env.ini +++ b/config/env.ini @@ -142,6 +142,8 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable- SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" +; minimum compatible macOS version (LLVM vars, availability not guaranteed) +MACOSX_DEPLOYMENT_TARGET=12.0 [freebsd] ; compiler environments From 9a681a9fa6249130bddcb7d342761f389c8d49ef Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 27 Dec 2025 21:19:31 +0100 Subject: [PATCH 113/682] add mariadb mysqlnd plugins --- config/ext.json | 30 +++++++++++++++++++ config/source.json | 18 +++++++++++ src/SPC/builder/extension/mysqlnd_ed25519.php | 22 ++++++++++++++ src/SPC/builder/extension/mysqlnd_parsec.php | 22 ++++++++++++++ src/globals/test-extensions.php | 10 +++---- 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 src/SPC/builder/extension/mysqlnd_ed25519.php create mode 100644 src/SPC/builder/extension/mysqlnd_parsec.php diff --git a/config/ext.json b/config/ext.json index d3fd2aa27..78a0ea38a 100644 --- a/config/ext.json +++ b/config/ext.json @@ -487,6 +487,36 @@ "zlib" ] }, + "mysqlnd_ed25519": { + "type": "external", + "source": "mysqlnd_ed25519", + "arg-type": "enable", + "target": [ + "shared" + ], + "ext-depends": [ + "mysqlnd" + ], + "lib-depends": [ + "libsodium", + "openssl" + ] + }, + "mysqlnd_parsec": { + "type": "external", + "source": "mysqlnd_parsec", + "arg-type": "enable", + "target": [ + "shared" + ], + "ext-depends": [ + "mysqlnd" + ], + "lib-depends": [ + "libsodium", + "openssl" + ] + }, "oci8": { "type": "wip", "support": { diff --git a/config/source.json b/config/source.json index c96822d6b..255d95848 100644 --- a/config/source.json +++ b/config/source.json @@ -871,6 +871,24 @@ "path": "LICENSE" } }, + "mysqlnd_ed25519": { + "type": "pie", + "repo": "mariadb/mysqlnd_ed25519", + "path": "php-src/ext/mysqlnd_ed25519", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "mysqlnd_parsec": { + "type": "pie", + "repo": "mariadb/mysqlnd_parsec", + "path": "php-src/ext/mysqlnd_parsec", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "ncurses": { "type": "filelist", "url": "https://ftp.gnu.org/pub/gnu/ncurses/", diff --git a/src/SPC/builder/extension/mysqlnd_ed25519.php b/src/SPC/builder/extension/mysqlnd_ed25519.php new file mode 100644 index 000000000..7b2b4abcd --- /dev/null +++ b/src/SPC/builder/extension/mysqlnd_ed25519.php @@ -0,0 +1,22 @@ +getConfigureArg(); + } +} diff --git a/src/SPC/builder/extension/mysqlnd_parsec.php b/src/SPC/builder/extension/mysqlnd_parsec.php new file mode 100644 index 000000000..d044b1c52 --- /dev/null +++ b/src/SPC/builder/extension/mysqlnd_parsec.php @@ -0,0 +1,22 @@ +getConfigureArg(); + } +} diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 0402eaf37..d76153e49 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,9 +13,9 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', - // '8.2', - // '8.3', + // '8.1', + '8.2', + '8.3', '8.4', '8.5', // 'git', @@ -50,13 +50,13 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'swoole,imagick', + 'Linux', 'Darwin' => 'mysqli', 'Windows' => 'bcmath', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => '', + 'Linux' => 'mysqlnd_parsec,mysqlnd_ed25519', 'Darwin' => '', 'Windows' => '', }; From 09b89a30f925510c4cd039a83bb0908390496d81 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 27 Dec 2025 22:20:02 +0100 Subject: [PATCH 114/682] WIP: use system libraries for grpc without building our own grpc lib --- src/SPC/builder/LibraryBase.php | 11 ++++++++- src/SPC/builder/traits/UnixLibraryTrait.php | 2 +- src/SPC/util/SPCConfigUtil.php | 26 +++++++++++++++++---- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index d34663021..383faa41a 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -375,8 +375,17 @@ protected function isLibraryInstalled(): bool return false; } } + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)); foreach (Config::getLib(static::NAME, 'pkg-configs', []) as $name) { - if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$name}.pc")) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$name}.pc")) { + $found = true; + break; + } + } + if (!$found) { return false; } } diff --git a/src/SPC/builder/traits/UnixLibraryTrait.php b/src/SPC/builder/traits/UnixLibraryTrait.php index 868cf6f7b..ec1e7c2be 100644 --- a/src/SPC/builder/traits/UnixLibraryTrait.php +++ b/src/SPC/builder/traits/UnixLibraryTrait.php @@ -34,7 +34,7 @@ public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCON $files = array_map(fn ($x) => "{$x}.pc", $conf_pc); } foreach ($files as $name) { - $realpath = realpath(BUILD_ROOT_PATH . '/lib/pkgconfig/' . $name); + $realpath = realpath(BUILD_LIB_PATH . '/pkgconfig/' . $name); if ($realpath === false) { throw new PatchException('pkg-config prefix patcher', 'Cannot find library [' . static::NAME . '] pkgconfig file [' . $name . '] in ' . BUILD_LIB_PATH . '/pkgconfig/ !'); } diff --git a/src/SPC/util/SPCConfigUtil.php b/src/SPC/util/SPCConfigUtil.php index a74d6a24c..7d2001f52 100644 --- a/src/SPC/util/SPCConfigUtil.php +++ b/src/SPC/util/SPCConfigUtil.php @@ -226,9 +226,17 @@ private function getIncludesString(array $libraries): string // parse pkg-configs foreach ($libraries as $library) { $pc = Config::getLib($library, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)); foreach ($pc as $file) { - if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$file}.pc")) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$file}.pc")) { + $found = true; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist. Please build it first."); } } $pc_cflags = implode(' ', $pc); @@ -257,9 +265,17 @@ private function getLibsString(array $libraries, bool $use_short_libs = true): s foreach ($libraries as $library) { // add pkg-configs libs $pkg_configs = Config::getLib($library, 'pkg-configs', []); - foreach ($pkg_configs as $pkg_config) { - if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$library}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)); + foreach ($pkg_configs as $file) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$file}.pc")) { + $found = true; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist. Please build it first."); } } $pkg_configs = implode(' ', $pkg_configs); From e952f1c76abf73703f0b160306e90c109e75d0eb Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 27 Dec 2025 22:36:24 +0100 Subject: [PATCH 115/682] we don't even need to build grpc library for grpc extension... --- config/ext.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/ext.json b/config/ext.json index 78a0ea38a..e5f4d7706 100644 --- a/config/ext.json +++ b/config/ext.json @@ -236,7 +236,9 @@ "arg-type-unix": "enable-path", "cpp-extension": true, "lib-depends": [ - "grpc" + "zlib", + "openssl", + "libcares" ] }, "iconv": { From 5ef46230513bf6b44634c01752bad1412dfd1238 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 27 Dec 2025 23:05:35 +0100 Subject: [PATCH 116/682] grpc will fail for php 8.5, it's not updated yet --- src/globals/test-extensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index d76153e49..d9202e08d 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -56,7 +56,7 @@ // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'mysqlnd_parsec,mysqlnd_ed25519', + 'Linux' => 'grpc,mysqlnd_parsec,mysqlnd_ed25519', 'Darwin' => '', 'Windows' => '', }; From 93a35908de867c9784cfafe59a257ba0e7b704bb Mon Sep 17 00:00:00 2001 From: henderkes Date: Sun, 28 Dec 2025 12:11:56 +0100 Subject: [PATCH 117/682] factor grpc extension out to ext-grpc, keep library for now, even though unused --- config/ext.json | 2 +- config/source.json | 12 ++++++++++++ src/SPC/builder/extension/grpc.php | 9 --------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/config/ext.json b/config/ext.json index e5f4d7706..62ec48772 100644 --- a/config/ext.json +++ b/config/ext.json @@ -232,7 +232,7 @@ "BSD": "wip" }, "type": "external", - "source": "grpc", + "source": "ext-grpc", "arg-type-unix": "enable-path", "cpp-extension": true, "lib-depends": [ diff --git a/config/source.json b/config/source.json index 255d95848..530e4703d 100644 --- a/config/source.json +++ b/config/source.json @@ -151,6 +151,18 @@ "path": "LICENSE" } }, + "ext-grpc": { + "type": "url", + "url": "https://pecl.php.net/get/grpc", + "path": "php-src/ext/grpc", + "filename": "grpc.tgz", + "license": { + "type": "file", + "path": [ + "LICENSE" + ] + } + }, "ext-imagick": { "type": "url", "url": "https://pecl.php.net/get/imagick", diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index fb31c85fe..ce8243d00 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -21,15 +21,6 @@ public function patchBeforeBuildconf(): bool if ($this->builder instanceof WindowsBuilder) { throw new ValidationException('grpc extension does not support windows yet'); } - if (file_exists(SOURCE_PATH . '/php-src/ext/grpc')) { - return false; - } - // soft link to the grpc source code - if (is_dir($this->source_dir . '/src/php/ext/grpc')) { - shell()->exec('ln -s ' . $this->source_dir . '/src/php/ext/grpc ' . SOURCE_PATH . '/php-src/ext/grpc'); - } else { - throw new ValidationException('Cannot find grpc source code in ' . $this->source_dir . '/src/php/ext/grpc'); - } if (SPCTarget::getTargetOS() === 'Darwin') { FileSystem::replaceFileRegex( SOURCE_PATH . '/php-src/ext/grpc/config.m4', From 2f3122627e054d51ed131c5f80a4972c434e8662 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sun, 28 Dec 2025 12:44:24 +0100 Subject: [PATCH 118/682] make grpc php 8.5 compatible --- src/SPC/builder/extension/grpc.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index ce8243d00..42dcdd5b8 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -21,9 +21,14 @@ public function patchBeforeBuildconf(): bool if ($this->builder instanceof WindowsBuilder) { throw new ValidationException('grpc extension does not support windows yet'); } + FileSystem::replaceFileStr( + $this->source_dir . '/src/php/ext/grpc/call.c', + 'zend_exception_get_default(TSRMLS_C),', + 'zend_ce_exception,', + ); if (SPCTarget::getTargetOS() === 'Darwin') { FileSystem::replaceFileRegex( - SOURCE_PATH . '/php-src/ext/grpc/config.m4', + $this->source_dir . '/config.m4', '/GRPC_LIBDIR=.*$/m', 'GRPC_LIBDIR=' . BUILD_LIB_PATH . "\n" . 'LDFLAGS="$LDFLAGS -framework CoreFoundation"' ); From e7a88f1df76956297d19db09dcd3e22812162aa7 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 29 Dec 2025 21:15:53 +0100 Subject: [PATCH 119/682] enable fat for gmp when next version releases --- src/SPC/builder/unix/library/gmp.php | 4 +++- src/globals/test-extensions.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/unix/library/gmp.php b/src/SPC/builder/unix/library/gmp.php index f625274f6..97a88ba1c 100644 --- a/src/SPC/builder/unix/library/gmp.php +++ b/src/SPC/builder/unix/library/gmp.php @@ -14,7 +14,9 @@ protected function build(): void ->appendEnv([ 'CFLAGS' => '-std=c17', ]) - ->configure() + ->configure( + '--enable-fat' + ) ->make(); $this->patchPkgconfPrefix(['gmp.pc']); } diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index d9202e08d..e2186c8e4 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'mysqli', + 'Linux', 'Darwin' => 'mysqli,gmp', 'Windows' => 'bcmath', }; From 08388c0b153bd9e96b77c543a468ec46cdf99ad8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 29 Dec 2025 22:12:25 +0100 Subject: [PATCH 120/682] force enable tailcall vm with zig --- src/SPC/toolchain/ZigToolchain.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SPC/toolchain/ZigToolchain.php b/src/SPC/toolchain/ZigToolchain.php index bb423db20..212a158f7 100644 --- a/src/SPC/toolchain/ZigToolchain.php +++ b/src/SPC/toolchain/ZigToolchain.php @@ -65,9 +65,11 @@ public function afterInit(): void GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); } $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_preserve_none=yes'); $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); if (!$has_avx512) { - GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no'); + $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no {$extra_vars}"); } } From 7688a556562ed4200255e820d64695cdd601d39a Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 29 Dec 2025 22:16:53 +0100 Subject: [PATCH 121/682] don't get zig master branch --- src/SPC/store/pkg/Zig.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SPC/store/pkg/Zig.php b/src/SPC/store/pkg/Zig.php index e9865dbe3..c2a81c0da 100644 --- a/src/SPC/store/pkg/Zig.php +++ b/src/SPC/store/pkg/Zig.php @@ -72,8 +72,11 @@ public function fetch(string $name, bool $force = false, ?array $config = null): $latest_version = null; foreach ($index_json as $version => $data) { - $latest_version = $version; - break; + // Skip the master branch, get the latest stable release + if ($version !== 'master') { + $latest_version = $version; + break; + } } if (!$latest_version) { From 022fdb2fc5b8ed2d78c0cac972fe4d9cd5f2d7d4 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 29 Dec 2025 23:58:54 +0100 Subject: [PATCH 122/682] fix no-strip --- src/SPC/builder/unix/UnixBuilderBase.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index d98059cb0..e2507f1f4 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -145,11 +145,10 @@ public function deployBinary(string $src, string $dst, bool $executable = true): throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); } - // extract debug info - $this->extractDebugInfo($dst); - - // strip if (!$this->getOption('no-strip')) { + // extract debug info + $this->extractDebugInfo($dst); + // extra strip $this->stripBinary($dst); } From a06cc3249169c3c1e7c76c6c9a8d53c075924882 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 30 Dec 2025 11:58:57 +0100 Subject: [PATCH 123/682] pin libpng to released tags, not git --- config/source.json | 13 +++++++------ src/SPC/store/Downloader.php | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/config/source.json b/config/source.json index 530e4703d..1bae46ce7 100644 --- a/config/source.json +++ b/config/source.json @@ -682,9 +682,10 @@ } }, "libpng": { - "type": "git", - "url": "https://github.com/glennrp/libpng.git", - "rev": "libpng16", + "type": "ghtagtar", + "repo": "pnggroup/libpng", + "match": "v1\\.6\\.\\d+", + "query": "?per_page=150", "provide-pre-built": true, "license": { "type": "file", @@ -692,9 +693,9 @@ } }, "librabbitmq": { - "type": "git", - "url": "https://github.com/alanxz/rabbitmq-c.git", - "rev": "master", + "type": "ghtar", + "repo": "alanxz/rabbitmq-c", + "prefer-stable": true, "license": { "type": "file", "path": "LICENSE" diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index a2911e46b..ccf61dd8d 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -97,8 +97,9 @@ public static function getLatestBitbucketTag(string $name, array $source): array public static function getLatestGithubTarball(string $name, array $source, string $type = 'releases'): array { logger()->debug("finding {$name} source from github {$type} tarball"); + $source['query'] ??= ''; $data = json_decode(self::curlExec( - url: "https://api.github.com/repos/{$source['repo']}/{$type}", + url: "https://api.github.com/repos/{$source['repo']}/{$type}{$source['query']}", hooks: [[CurlHook::class, 'setupGithubToken']], retries: self::getRetryAttempts() ), true, 512, JSON_THROW_ON_ERROR); From 64f7a3553e3fd961045f36842f05bfe2b3f9664b Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 1 Jan 2026 12:33:55 +0100 Subject: [PATCH 124/682] don't need it anymore --- src/SPC/toolchain/ZigToolchain.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SPC/toolchain/ZigToolchain.php b/src/SPC/toolchain/ZigToolchain.php index 212a158f7..1b7cc70dc 100644 --- a/src/SPC/toolchain/ZigToolchain.php +++ b/src/SPC/toolchain/ZigToolchain.php @@ -65,7 +65,6 @@ public function afterInit(): void GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); } $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; - GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_preserve_none=yes'); $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); if (!$has_avx512) { $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; From d1b194999d22f8d6f55b715150fe9e10524fcd3c Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 2 Jan 2026 21:13:22 +0100 Subject: [PATCH 125/682] use OPENSSL_CONF directory for openssl default configuration --- src/SPC/builder/linux/library/openssl.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/linux/library/openssl.php b/src/SPC/builder/linux/library/openssl.php index a78b6a642..8d071f2d9 100644 --- a/src/SPC/builder/linux/library/openssl.php +++ b/src/SPC/builder/linux/library/openssl.php @@ -51,6 +51,8 @@ public function build(): void $zlib_extra = ''; } + $openssl_conf = getenv('OPENSSL_CONF'); + $openssl_dir = $openssl_conf ? dirname($openssl_conf) : '/etc/ssl'; $ex_lib = trim($ex_lib); shell()->cd($this->source_dir)->initializeEnv($this) @@ -58,7 +60,7 @@ public function build(): void "{$env} ./Configure no-shared {$extra} " . '--prefix=' . BUILD_ROOT_PATH . ' ' . '--libdir=' . BUILD_LIB_PATH . ' ' . - '--openssldir=/etc/ssl ' . + "--openssldir={$openssl_dir} " . "{$zlib_extra}" . 'enable-pie ' . 'no-legacy ' . From fff24845298730398bd70cbaaba90be997f7595d Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 2 Jan 2026 22:52:03 +0100 Subject: [PATCH 126/682] postgresql doesn't build under c23 --- src/SPC/builder/unix/library/postgresql.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index 6e0cb606e..a72f3a1a6 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -50,7 +50,7 @@ protected function build(): void $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false)); $env_vars = [ - 'CFLAGS' => $config['cflags'], + 'CFLAGS' => $config['cflags'] . ' -std=c17', 'CPPFLAGS' => '-DPIC', 'LDFLAGS' => $config['ldflags'], 'LIBS' => $config['libs'], From 559a2909a911a5557bee0d97430649e7f9d5daa0 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 2 Jan 2026 23:21:29 +0100 Subject: [PATCH 127/682] use little trick to order libargon2 before libsodium --- config/lib.json | 3 +++ src/SPC/builder/extension/password_argon2.php | 19 ------------------- src/SPC/util/SPCConfigUtil.php | 1 - 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/config/lib.json b/config/lib.json index 47f3c7b89..3be972484 100644 --- a/config/lib.json +++ b/config/lib.json @@ -361,6 +361,9 @@ "source": "libargon2", "static-libs-unix": [ "libargon2.a" + ], + "lib-suggests": [ + "libsodium" ] }, "libavif": { diff --git a/src/SPC/builder/extension/password_argon2.php b/src/SPC/builder/extension/password_argon2.php index 30e6fd2c5..d42fe4e37 100644 --- a/src/SPC/builder/extension/password_argon2.php +++ b/src/SPC/builder/extension/password_argon2.php @@ -24,25 +24,6 @@ public function runCliCheckUnix(): void } } - public function patchBeforeMake(): bool - { - $patched = parent::patchBeforeMake(); - if ($this->builder->getLib('libsodium') !== null) { - $extraLibs = getenv('SPC_EXTRA_LIBS'); - if ($extraLibs !== false) { - $extraLibs = str_replace( - [BUILD_LIB_PATH . '/libargon2.a', BUILD_LIB_PATH . '/libsodium.a'], - ['', BUILD_LIB_PATH . '/libargon2.a ' . BUILD_LIB_PATH . '/libsodium.a'], - $extraLibs, - ); - $extraLibs = trim(preg_replace('/\s+/', ' ', $extraLibs)); // normalize spacing - f_putenv('SPC_EXTRA_LIBS=' . $extraLibs); - return true; - } - } - return $patched; - } - public function getConfigureArg(bool $shared = false): string { if ($this->builder->getLib('openssl') !== null) { diff --git a/src/SPC/util/SPCConfigUtil.php b/src/SPC/util/SPCConfigUtil.php index 7d2001f52..8c4fa9267 100644 --- a/src/SPC/util/SPCConfigUtil.php +++ b/src/SPC/util/SPCConfigUtil.php @@ -80,7 +80,6 @@ public function config(array $extensions = [], array $libraries = [], bool $incl $libs = $this->getLibsString($libraries, !$this->absolute_libs); // additional OS-specific libraries (e.g. macOS -lresolv) - // embed if ($extra_libs = SPCTarget::getRuntimeLibs()) { $libs .= " {$extra_libs}"; } From 890ff475f131c809de2b2e3309b85d33f28f5274 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 14:09:16 +0100 Subject: [PATCH 128/682] our memcache patch prevents shared building --- config/ext.json | 3 +-- src/SPC/builder/extension/memcache.php | 35 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/config/ext.json b/config/ext.json index 62ec48772..419be5a9b 100644 --- a/config/ext.json +++ b/config/ext.json @@ -410,8 +410,7 @@ "ext-depends": [ "zlib", "session" - ], - "build-with-php": true + ] }, "memcached": { "support": { diff --git a/src/SPC/builder/extension/memcache.php b/src/SPC/builder/extension/memcache.php index 59c6065d4..feba0151b 100644 --- a/src/SPC/builder/extension/memcache.php +++ b/src/SPC/builder/extension/memcache.php @@ -18,6 +18,9 @@ public function getUnixConfigureArg(bool $shared = false): string public function patchBeforeBuildconf(): bool { + if (!$this->isBuildStatic()) { + return false; + } FileSystem::replaceFileStr( SOURCE_PATH . '/php-src/ext/memcache/config9.m4', 'if test -d $abs_srcdir/src ; then', @@ -44,6 +47,38 @@ public function patchBeforeBuildconf(): bool return true; } + public function patchBeforeSharedConfigure(): bool + { + if (!$this->isBuildShared()) { + return false; + } + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/memcache/config9.m4', + 'if test -d $abs_srcdir/main ; then', + 'if test -d $abs_srcdir/src ; then', + ); + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/memcache/config9.m4', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + ); + // add for in-tree building + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/memcache/php_memcache.h', + <<<'EOF' +#ifndef PHP_MEMCACHE_H +#define PHP_MEMCACHE_H + +extern zend_module_entry memcache_module_entry; +#define phpext_memcache_ptr &memcache_module_entry + +#endif +EOF, + '' + ); + return true; + } + protected function getExtraEnv(): array { return ['CFLAGS' => '-std=c17']; From 54001ab868b17b4b99b74443a88d5f1244895534 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 16:33:40 +0100 Subject: [PATCH 129/682] simplify logic a bit --- src/SPC/builder/linux/LinuxBuilder.php | 49 +++----------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 0d6f77fba..63abf018b 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -319,7 +319,7 @@ private function getMakeExtraVars(): array return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_LIBS' => $config['libs'], - 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), + 'EXTRA_LDFLAGS' => preg_replace('/-release\s+(\S+)/', '', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS')), 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", ]); } @@ -328,14 +328,12 @@ private function processLibphpSoFile(string $libphpSo): 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}"; + $releaseName = "libphp-{$release}.so"; + $libphpRelease = "{$libDir}/{$releaseName}"; if (!file_exists($libphpRelease) && file_exists($libphpSo)) { rename($libphpSo, $libphpRelease); } @@ -344,52 +342,15 @@ private function processLibphpSoFile(string $libphpSo): void if (file_exists($libphpSo)) { unlink($libphpSo); } - symlink($realLibName, 'libphp.so'); + symlink($releaseName, 'libphp.so'); shell()->exec(sprintf( 'patchelf --set-soname %s %s', - escapeshellarg($realLibName), + escapeshellarg($releaseName), escapeshellarg($libphpRelease) )); } - if (is_dir($modulesDir)) { - chdir($modulesDir); - foreach ($this->getExts() 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) - )); - } - } - } } /** From 1be353fd13051949e301cbaef71f32bd13a57250 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 16:45:39 +0100 Subject: [PATCH 130/682] more concise message --- src/SPC/builder/unix/UnixBuilderBase.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index e2507f1f4..050394f06 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -235,8 +235,10 @@ protected function sanityCheck(int $build_target): void $lens .= ' -static'; } $dynamic_exports = ''; + $embedType = 'static'; // if someone changed to EMBED_TYPE=shared, we need to add LD_LIBRARY_PATH if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $embedType = 'shared'; if (PHP_OS_FAMILY === 'Darwin') { $ext_path = 'DYLD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$DYLD_LIBRARY_PATH '; } else { @@ -255,18 +257,19 @@ protected function sanityCheck(int $build_target): void } } $cc = getenv('CC'); + [$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens} {$dynamic_exports}"); if ($ret !== 0) { throw new ValidationException( - 'embed failed sanity check: build failed. Error message: ' . implode("\n", $out), - validation_module: 'static libphp.a sanity check' + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: $embedType . 'libphp embed build sanity check' ); } [$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed'); if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { throw new ValidationException( - 'embed failed sanity check: run failed. Error message: ' . implode("\n", $output), - validation_module: 'static libphp.a sanity check' + 'embed failed to run. Error message: ' . implode("\n", $output), + validation_module: $embedType . ' libphp embed run sanity check' ); } } From 76025b95c1683b29bcdcbeda30ee4f31513d7365 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 16:46:14 +0100 Subject: [PATCH 131/682] missing space --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 050394f06..464c9b2fc 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -262,7 +262,7 @@ protected function sanityCheck(int $build_target): void if ($ret !== 0) { throw new ValidationException( 'embed failed to build. Error message: ' . implode("\n", $out), - validation_module: $embedType . 'libphp embed build sanity check' + validation_module: $embedType . ' libphp embed build sanity check' ); } [$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed'); From 6bbb3c969ce914c146b96849c7210c9592fe8323 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 17:03:43 +0100 Subject: [PATCH 132/682] remove -release handling functionality --- bin/spc-alpine-docker | 3 +- bin/spc-gnu-docker | 5 ---- src/SPC/builder/linux/LinuxBuilder.php | 33 +--------------------- src/SPC/doctor/item/LinuxToolCheckList.php | 5 +--- 4 files changed, 3 insertions(+), 43 deletions(-) diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker index 2790a5c34..1abf10480 100755 --- a/bin/spc-alpine-docker +++ b/bin/spc-alpine-docker @@ -108,8 +108,7 @@ RUN apk update; \ wget \ xz \ gettext-dev \ - binutils-gold \ - patchelf + binutils-gold RUN curl -#fSL https://dl.static-php.dev/static-php-cli/bulk/php-8.4.4-cli-linux-\$(uname -m).tar.gz | tar -xz -C /usr/local/bin && \ chmod +x /usr/local/bin/php diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker index 68f85109f..286ef9859 100755 --- a/bin/spc-gnu-docker +++ b/bin/spc-gnu-docker @@ -92,11 +92,6 @@ RUN echo "source scl_source enable devtoolset-10" >> /etc/bashrc RUN source /etc/bashrc RUN yum install -y which -RUN curl -fsSL -o patchelf.tgz https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0-$SPC_USE_ARCH.tar.gz && \ - mkdir -p /patchelf && \ - tar -xzf patchelf.tgz -C /patchelf --strip-components=1 && \ - cp /patchelf/bin/patchelf /usr/bin/ - RUN curl -o cmake.tgz -#fSL https://github.com/Kitware/CMake/releases/download/v3.31.4/cmake-3.31.4-linux-$SPC_USE_ARCH.tar.gz && \ mkdir /cmake && \ tar -xzf cmake.tgz -C /cmake --strip-components 1 diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 63abf018b..22c2d47a1 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -284,8 +284,6 @@ protected function buildEmbed(): void // process libphp.so for shared embed $libphpSo = BUILD_LIB_PATH . '/libphp.so'; if (file_exists($libphpSo)) { - // post actions: rename libphp.so to libphp-.so if -release is set in LDFLAGS - $this->processLibphpSoFile($libphpSo); // deploy libphp.so $this->deployBinary($libphpSo, $libphpSo, false); } @@ -319,40 +317,11 @@ private function getMakeExtraVars(): array return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_LIBS' => $config['libs'], - 'EXTRA_LDFLAGS' => preg_replace('/-release\s+(\S+)/', '', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS')), + 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", ]); } - private function processLibphpSoFile(string $libphpSo): void - { - $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; - $libDir = BUILD_LIB_PATH; - $cwd = getcwd(); - - if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) { - $release = $matches[1]; - $releaseName = "libphp-{$release}.so"; - $libphpRelease = "{$libDir}/{$releaseName}"; - if (!file_exists($libphpRelease) && file_exists($libphpSo)) { - rename($libphpSo, $libphpRelease); - } - if (file_exists($libphpRelease)) { - chdir($libDir); - if (file_exists($libphpSo)) { - unlink($libphpSo); - } - symlink($releaseName, 'libphp.so'); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($releaseName), - escapeshellarg($libphpRelease) - )); - } - chdir($cwd); - } - } - /** * Patch micro.sfx after UPX compression. * micro needs special section handling in LinuxBuilder. diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index ab8144a21..53b356ff3 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -22,7 +22,6 @@ class LinuxToolCheckList 'bzip2', 'cmake', 'gcc', 'g++', 'patch', 'binutils-gold', 'libtoolize', 'which', - 'patchelf', ]; public const TOOLS_DEBIAN = [ @@ -31,7 +30,6 @@ class LinuxToolCheckList 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'xz', 'libtoolize', 'which', - 'patchelf', ]; public const TOOLS_RHEL = [ @@ -39,8 +37,7 @@ class LinuxToolCheckList 'git', 'autoconf', 'automake', 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'which', - 'xz', 'libtool', 'gettext-devel', - 'patchelf', 'file', + 'xz', 'libtool', 'gettext-devel', 'file', ]; public const TOOLS_ARCH = [ From f8b0c2c98078d0963ceb6846991825b0760838f3 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 19:08:14 +0100 Subject: [PATCH 133/682] add release thing to extension build too --- src/SPC/builder/Extension.php | 1 + src/SPC/builder/linux/LinuxBuilder.php | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index e79b886bf..040eae026 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -546,6 +546,7 @@ protected function getSharedExtensionEnv(): array 'CFLAGS' => $config['cflags'], 'CXXFLAGS' => $config['cflags'], 'LDFLAGS' => $config['ldflags'], + 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 22c2d47a1..004c37def 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -283,9 +283,14 @@ protected function buildEmbed(): void // process libphp.so for shared embed $libphpSo = BUILD_LIB_PATH . '/libphp.so'; + $libphpSoDest = BUILD_LIB_PATH . '/libphp.so'; if (file_exists($libphpSo)) { // deploy libphp.so - $this->deployBinary($libphpSo, $libphpSo, false); + preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); + if (!empty($matches[1])) { + $libphpSoDest = str_replace('.so', '-' . $matches[1] . '.so', $libphpSo); + } + $this->deployBinary($libphpSo, $libphpSoDest, false); } // process shared extensions build-with-php From 94644d374ff8d10e678ae0cac9fdd236bd187593 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 19:12:16 +0100 Subject: [PATCH 134/682] fix --- src/SPC/builder/extension/memcache.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/SPC/builder/extension/memcache.php b/src/SPC/builder/extension/memcache.php index feba0151b..32cb301c6 100644 --- a/src/SPC/builder/extension/memcache.php +++ b/src/SPC/builder/extension/memcache.php @@ -62,20 +62,6 @@ public function patchBeforeSharedConfigure(): bool 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"', 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', ); - // add for in-tree building - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/memcache/php_memcache.h', - <<<'EOF' -#ifndef PHP_MEMCACHE_H -#define PHP_MEMCACHE_H - -extern zend_module_entry memcache_module_entry; -#define phpext_memcache_ptr &memcache_module_entry - -#endif -EOF, - '' - ); return true; } From 3a17cec52193f01cf9e6354c476546747ae27fac Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 3 Jan 2026 19:15:57 +0100 Subject: [PATCH 135/682] deploy extensions with -release flag too --- src/SPC/builder/Extension.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 040eae026..c0cb152d9 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -455,12 +455,17 @@ public function buildUnixShared(): void // process *.so file $soFile = BUILD_MODULES_PATH . '/' . $this->getName() . '.so'; + $soDest = $soFile; + preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); + if (!empty($matches[1])) { + $soDest = str_replace('.so', '-' . $matches[1] . '.so', $soFile); + } if (!file_exists($soFile)) { throw new ValidationException("extension {$this->getName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getName()} build"); } /** @var UnixBuilderBase $builder */ $builder = $this->builder; - $builder->deployBinary($soFile, $soFile, false); + $builder->deployBinary($soFile, $soDest, false); } /** From 34910d18e959931ea2216c756ee3b42075495627 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sun, 4 Jan 2026 02:31:41 +0100 Subject: [PATCH 136/682] add patch point for shared ext build --- src/SPC/builder/Extension.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index c0cb152d9..816ea8116 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -401,10 +401,12 @@ public function buildShared(array $visited = []): void if (Config::getExt($this->getName(), 'type') === 'addon') { return; } + $this->builder->emitPatchPoint('before-shared-ext[' . $this->getName() . ']-build'); match (PHP_OS_FAMILY) { 'Darwin', 'Linux' => $this->buildUnixShared(), default => throw new WrongUsageException(PHP_OS_FAMILY . ' build shared extensions is not supported yet'), }; + $this->builder->emitPatchPoint('after-shared-ext[' . $this->getName() . ']-build'); } catch (SPCException $e) { $e->bindExtensionInfo(['extension_name' => $this->getName()]); throw $e; From cd2dc5bce40462c11b8a20bb49cf453ee480c6cc Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Tue, 13 Jan 2026 16:51:57 +0800 Subject: [PATCH 137/682] Fix nghttp2 and curl build configurations for static linking (#1014) --- src/SPC/builder/windows/library/curl.php | 5 +++-- src/SPC/builder/windows/library/nghttp2.php | 7 ++++++- src/globals/test-extensions.php | 12 ++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/SPC/builder/windows/library/curl.php b/src/SPC/builder/windows/library/curl.php index 1229dbd69..bba130e1a 100644 --- a/src/SPC/builder/windows/library/curl.php +++ b/src/SPC/builder/windows/library/curl.php @@ -30,7 +30,6 @@ protected function build(): void '-DCMAKE_BUILD_TYPE=Release ' . '-DBUILD_SHARED_LIBS=OFF ' . '-DBUILD_STATIC_LIBS=ON ' . - '-DCURL_STATICLIB=ON ' . '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' . '-DBUILD_CURL_EXE=OFF ' . // disable curl.exe '-DBUILD_TESTING=OFF ' . // disable tests @@ -42,9 +41,9 @@ protected function build(): void '-DCURL_USE_OPENSSL=OFF ' . // disable openssl due to certificate issue '-DCURL_ENABLE_SSL=ON ' . '-DUSE_NGHTTP2=ON ' . // enable nghttp2 + '-DSHARE_LIB_OBJECT=OFF ' . // disable shared lib object '-DCURL_USE_LIBSSH2=ON ' . // enable libssh2 '-DENABLE_IPV6=ON ' . // enable ipv6 - '-DNGHTTP2_CFLAGS="/DNGHTTP2_STATICLIB" ' . $alt ) ->execWithWrapper( @@ -53,5 +52,7 @@ protected function build(): void ); // move libcurl.lib to libcurl_a.lib rename(BUILD_LIB_PATH . '\libcurl.lib', BUILD_LIB_PATH . '\libcurl_a.lib'); + + FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\curl\curl.h', '#ifdef CURL_STATICLIB', '#if 1'); } } diff --git a/src/SPC/builder/windows/library/nghttp2.php b/src/SPC/builder/windows/library/nghttp2.php index 7e6e999dc..5a1c6bf15 100644 --- a/src/SPC/builder/windows/library/nghttp2.php +++ b/src/SPC/builder/windows/library/nghttp2.php @@ -29,11 +29,16 @@ protected function build(): void '-DBUILD_SHARED_LIBS=OFF ' . '-DENABLE_STATIC_CRT=ON ' . '-DENABLE_LIB_ONLY=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' + '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' . + '-DENABLE_STATIC_CRT=ON ' . + '-DENABLE_DOC=OFF ' . + '-DBUILD_TESTING=OFF ' ) ->execWithWrapper( $this->builder->makeSimpleWrapper('cmake'), "--build build --config Release --target install -j{$this->builder->concurrency}" ); + + FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\nghttp2\nghttp2.h', '#ifdef NGHTTP2_STATICLIB', '#if 1'); } } diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index cdd27e235..93f86348b 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,17 +13,17 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - // '8.1', + '8.1', // '8.2', // '8.3', - // '8.4', + '8.4', '8.5', // 'git', ]; // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-15-intel', // bin/spc for x86_64 + // 'macos-15-intel', // bin/spc for x86_64 // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 @@ -31,7 +31,7 @@ // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - // 'windows-2025', + 'windows-2025', ]; // whether enable thread safe @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'imagick', + 'Linux', 'Darwin' => 'curl', 'Windows' => 'bcmath', }; @@ -67,7 +67,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { 'Linux', 'Darwin' => 'libwebp', - 'Windows' => '', + 'Windows' => 'nghttp2', }; // Please change your test base combination. We recommend testing with `common`. From d902e70b4d72867944b553982ed5152dbf58c810 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 16 Jan 2026 12:28:41 +0100 Subject: [PATCH 138/682] fix arm64 builds --- config/source.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/source.json b/config/source.json index 1bae46ce7..33295e87d 100644 --- a/config/source.json +++ b/config/source.json @@ -712,7 +712,7 @@ "libsodium": { "type": "ghrel", "repo": "jedisct1/libsodium", - "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", + "match": "libsodium-(?!1\\.0\\.21)\\d+(\\.\\d+)*\\.tar\\.gz", "prefer-stable": true, "provide-pre-built": true, "license": { From b09337de09ad23dea235a127d4eb23c9563a0773 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 17 Jan 2026 10:51:21 +0100 Subject: [PATCH 139/682] add excimer extension --- config/ext.json | 8 ++++++++ config/source.json | 12 +++++++++++- src/SPC/builder/extension/excimer.php | 19 +++++++++++++++++++ src/SPC/builder/unix/UnixBuilderBase.php | 4 +--- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 src/SPC/builder/extension/excimer.php diff --git a/config/ext.json b/config/ext.json index d3fd2aa27..e88ad20cf 100644 --- a/config/ext.json +++ b/config/ext.json @@ -127,6 +127,14 @@ "sockets" ] }, + "excimer": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "type": "external", + "source": "ext-excimer" + }, "exif": { "type": "builtin" }, diff --git a/config/source.json b/config/source.json index c96822d6b..0f32bcc9a 100644 --- a/config/source.json +++ b/config/source.json @@ -126,13 +126,23 @@ }, "ext-event": { "type": "url", - "url": "https://bitbucket.org/osmanov/pecl-event/get/3.0.8.tar.gz", + "url": "https://bitbucket.org/osmanov/pecl-event/get/3.1.4.tar.gz", "path": "php-src/ext/event", "license": { "type": "file", "path": "LICENSE" } }, + "ext-excimer": { + "type": "url", + "url": "https://pecl.php.net/get/excimer", + "path": "php-src/ext/excimer", + "filename": "excimer.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "ext-glfw": { "type": "git", "url": "https://github.com/mario-deluna/php-glfw", diff --git a/src/SPC/builder/extension/excimer.php b/src/SPC/builder/extension/excimer.php new file mode 100644 index 000000000..03dd8f228 --- /dev/null +++ b/src/SPC/builder/extension/excimer.php @@ -0,0 +1,19 @@ +extractDebugInfo($dst); - // strip if (!$this->getOption('no-strip')) { + $this->extractDebugInfo($dst); $this->stripBinary($dst); } From 19f941797ed0a8aac3d924469f5378fdbe46052d Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 17 Jan 2026 11:26:28 +0100 Subject: [PATCH 140/682] zig now supports -Wl,-exported_symbols_list --- src/SPC/builder/traits/UnixSystemUtilTrait.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/SPC/builder/traits/UnixSystemUtilTrait.php b/src/SPC/builder/traits/UnixSystemUtilTrait.php index b1ef9db45..ff75bf7c2 100644 --- a/src/SPC/builder/traits/UnixSystemUtilTrait.php +++ b/src/SPC/builder/traits/UnixSystemUtilTrait.php @@ -72,12 +72,8 @@ public static function getDynamicExportedSymbols(string $lib_file): ?string if (!is_file($symbol_file)) { throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); } - // https://github.com/ziglang/zig/issues/24662 - if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { - return '-Wl,--export-dynamic'; - } - // macOS - if (SPCTarget::getTargetOS() !== 'Linux') { + // macOS/zig + if (SPCTarget::getTargetOS() !== 'Linux' || ToolchainManager::getToolchainClass() === ZigToolchain::class) { return "-Wl,-exported_symbols_list,{$symbol_file}"; } return "-Wl,--dynamic-list={$symbol_file}"; From 1e2b4017ac773cae62ba79c4b31adc90c0823435 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 17 Jan 2026 11:27:29 +0100 Subject: [PATCH 141/682] test excimer --- src/globals/test-extensions.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 93f86348b..ec3c71287 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,7 +13,7 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', + // '8.1', // '8.2', // '8.3', '8.4', @@ -23,15 +23,15 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - // 'macos-15-intel', // bin/spc for x86_64 - // 'macos-15', // bin/spc for arm64 + 'macos-15-intel', // bin/spc for x86_64 + 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', + // 'windows-2025', ]; // whether enable thread safe @@ -50,13 +50,13 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'curl', + 'Linux', 'Darwin' => 'opcache', 'Windows' => 'bcmath', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => '', + 'Linux' => 'excimer', 'Darwin' => '', 'Windows' => '', }; @@ -67,7 +67,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { 'Linux', 'Darwin' => 'libwebp', - 'Windows' => 'nghttp2', + 'Windows' => '', }; // Please change your test base combination. We recommend testing with `common`. From af75ffaf24837f6d684048777d7e0b9f3486467a Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 19 Jan 2026 10:22:33 +0100 Subject: [PATCH 142/682] suggestions, change openssldir --- config/env.ini | 4 ++++ config/ext.json | 8 ++++++-- src/SPC/builder/Extension.php | 3 --- src/SPC/builder/linux/library/openssl.php | 6 ++++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/config/env.ini b/config/env.ini index 7448cc373..b989f5c35 100644 --- a/config/env.ini +++ b/config/env.ini @@ -115,6 +115,10 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" +; optional, path to openssl conf. This affects where openssl will look for the default CA. +; default on Debian/Alpine: /etc/ssl, default on RHEL: /etc/pki/tls +OPENSSLDIR="" + [macos] ; build target: macho or macho (possibly we could support macho-universal in the future) ; Currently we do not support universal and cross-compilation for macOS. diff --git a/config/ext.json b/config/ext.json index 419be5a9b..c5bbb45de 100644 --- a/config/ext.json +++ b/config/ext.json @@ -499,7 +499,9 @@ "mysqlnd" ], "lib-depends": [ - "libsodium", + "libsodium" + ], + "lib-suggests": [ "openssl" ] }, @@ -514,7 +516,9 @@ "mysqlnd" ], "lib-depends": [ - "libsodium", + "libsodium" + ], + "lib-suggests": [ "openssl" ] }, diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 816ea8116..925a4c8ef 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -398,9 +398,6 @@ public function buildShared(array $visited = []): void $dependency->buildShared([...$visited, $this->getName()]); } } - if (Config::getExt($this->getName(), 'type') === 'addon') { - return; - } $this->builder->emitPatchPoint('before-shared-ext[' . $this->getName() . ']-build'); match (PHP_OS_FAMILY) { 'Darwin', 'Linux' => $this->buildUnixShared(), diff --git a/src/SPC/builder/linux/library/openssl.php b/src/SPC/builder/linux/library/openssl.php index 8d071f2d9..bfc3936ba 100644 --- a/src/SPC/builder/linux/library/openssl.php +++ b/src/SPC/builder/linux/library/openssl.php @@ -21,6 +21,7 @@ namespace SPC\builder\linux\library; +use SPC\builder\linux\SystemUtil; use SPC\store\FileSystem; class openssl extends LinuxLibraryBase @@ -51,8 +52,9 @@ public function build(): void $zlib_extra = ''; } - $openssl_conf = getenv('OPENSSL_CONF'); - $openssl_dir = $openssl_conf ? dirname($openssl_conf) : '/etc/ssl'; + $openssl_dir = getenv('OPENSSLDIR') ?: null; + // TODO: in v3 use the following: $openssl_dir ??= SystemUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; + $openssl_dir ??= '/etc/ssl'; $ex_lib = trim($ex_lib); shell()->cd($this->source_dir)->initializeEnv($this) From 372760e469d57a1852c93d7a62c989cf8ce4011e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 19 Jan 2026 18:56:28 +0800 Subject: [PATCH 143/682] Update patch point docs --- docs/en/guide/manual-build.md | 34 ++++++++++++++++++---------------- docs/zh/guide/manual-build.md | 2 ++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/en/guide/manual-build.md b/docs/en/guide/manual-build.md index 28f0ec87e..319022ca3 100644 --- a/docs/en/guide/manual-build.md +++ b/docs/en/guide/manual-build.md @@ -549,22 +549,24 @@ otherwise it will be executed repeatedly in other events. The following are the supported `patch_point` event names and corresponding locations: -| Event name | Event description | -|------------------------------|----------------------------------------------------------------------------------------------------| -| before-libs-extract | Triggered before the dependent libraries extracted | -| after-libs-extract | Triggered after the compiled dependent libraries extracted | -| before-php-extract | Triggered before PHP source code extracted | -| after-php-extract | Triggered after PHP source code extracted | -| before-micro-extract | Triggered before phpmicro extract | -| after-micro-extract | Triggered after phpmicro extracted | -| before-exts-extract | Triggered before the extension (to be compiled) extracted to the PHP source directory | -| after-exts-extract | Triggered after the extension extracted to the PHP source directory | -| before-library[*name*]-build | Triggered before the library named `name` is compiled (such as `before-library[postgresql]-build`) | -| after-library[*name*]-build | Triggered after the library named `name` is compiled | -| before-php-buildconf | Triggered before compiling PHP command `./buildconf` | -| before-php-configure | Triggered before compiling PHP command `./configure` | -| before-php-make | Triggered before compiling PHP command `make` | -| before-sanity-check | Triggered after compiling PHP but before running extended checks | +| Event name | Event description | +|---------------------------------|----------------------------------------------------------------------------------------------------| +| before-libs-extract | Triggered before the dependent libraries extracted | +| after-libs-extract | Triggered after the compiled dependent libraries extracted | +| before-php-extract | Triggered before PHP source code extracted | +| after-php-extract | Triggered after PHP source code extracted | +| before-micro-extract | Triggered before phpmicro extract | +| after-micro-extract | Triggered after phpmicro extracted | +| before-exts-extract | Triggered before the extension (to be compiled) extracted to the PHP source directory | +| after-exts-extract | Triggered after the extension extracted to the PHP source directory | +| before-library[*name*]-build | Triggered before the library named `name` is compiled (such as `before-library[postgresql]-build`) | +| after-library[*name*]-build | Triggered after the library named `name` is compiled | +| after-shared-ext[*name*]-build | Triggered after the shared extension named `name` is compiled | +| before-shared-ext[*name*]-build | Triggered before the shared extension named `name` is compiled | +| before-php-buildconf | Triggered before compiling PHP command `./buildconf` | +| before-php-configure | Triggered before compiling PHP command `./configure` | +| before-php-make | Triggered before compiling PHP command `make` | +| before-sanity-check | Triggered after compiling PHP but before running extended checks | The following is a simple example of temporarily modifying the PHP source code. Enable the CLI function to search for the `php.ini` configuration in the current working directory: diff --git a/docs/zh/guide/manual-build.md b/docs/zh/guide/manual-build.md index 4c24cab8f..d7745a02b 100644 --- a/docs/zh/guide/manual-build.md +++ b/docs/zh/guide/manual-build.md @@ -500,6 +500,8 @@ bin/spc dev:sort-config ext | after-exts-extract | 在要编译的扩展解压到 PHP 源码目录后触发 | | before-library[*name*]-build | 在名称为 `name` 的库编译前触发(如 `before-library[postgresql]-build`) | | after-library[*name*]-build | 在名称为 `name` 的库编译后触发 | +| after-shared-ext[*name*]-build | 在名称为 `name` 的共享扩展编译后触发(如 `after-shared-ext[redis]-build`) | +| before-shared-ext[*name*]-build | 在名称为 `name` 的共享扩展编译前触发 | | before-php-buildconf | 在编译 PHP 命令 `./buildconf` 前触发 | | before-php-configure | 在编译 PHP 命令 `./configure` 前触发 | | before-php-make | 在编译 PHP 命令 `make` 前触发 | From b3c450291a4fe5c7437af446283a11025cf6648a Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 19 Jan 2026 12:00:06 +0100 Subject: [PATCH 144/682] up version --- src/SPC/ConsoleApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 9500a231a..19fdd41dc 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.7.11'; + public const string VERSION = '2.8.0'; public function __construct() { From 2c22bf25ea4d63b445fbaa9356931a3e5c3ef939 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 20 Jan 2026 16:56:46 +0800 Subject: [PATCH 145/682] Refactor getOSRelease method for improved readability and efficiency --- src/StaticPHP/Util/System/LinuxUtil.php | 64 ++++++++++++------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/StaticPHP/Util/System/LinuxUtil.php b/src/StaticPHP/Util/System/LinuxUtil.php index 79ad89ed7..2b1adf3d9 100644 --- a/src/StaticPHP/Util/System/LinuxUtil.php +++ b/src/StaticPHP/Util/System/LinuxUtil.php @@ -12,7 +12,6 @@ class LinuxUtil extends UnixUtil /** * Get current linux distro name and version. * - * @noinspection PhpMissingBreakStatementInspection * @return array{dist: string, ver: string, family: string} Linux distro info (unknown if not found) */ public static function getOSRelease(): array @@ -22,42 +21,39 @@ public static function getOSRelease(): array 'ver' => 'unknown', 'family' => 'unknown', ]; - switch (true) { - case file_exists('/etc/centos-release'): - $lines = file('/etc/centos-release'); - $centos = true; - goto rh; - case file_exists('/etc/redhat-release'): - $lines = file('/etc/redhat-release'); - $centos = false; - rh: - foreach ($lines as $line) { - if (preg_match('/release\s+(\d*(\.\d+)*)/', $line, $matches)) { - /* @phpstan-ignore-next-line */ - $ret['dist'] = $centos ? 'centos' : 'redhat'; - $ret['ver'] = $matches[1]; - } + + if (file_exists('/etc/centos-release') || file_exists('/etc/redhat-release')) { + $is_centos = file_exists('/etc/centos-release'); + $file = $is_centos ? '/etc/centos-release' : '/etc/redhat-release'; + $lines = file($file); + + foreach ($lines as $line) { + if (preg_match('/release\s+(\d*(\.\d+)*)/', $line, $matches)) { + $ret['dist'] = $is_centos ? 'centos' : 'redhat'; + $ret['ver'] = $matches[1]; + } + } + } elseif (file_exists('/etc/os-release')) { + $lines = file('/etc/os-release'); + + foreach ($lines as $line) { + if (preg_match('/^ID=(.*)$/', $line, $matches)) { + $ret['dist'] = $matches[1]; } - break; - case file_exists('/etc/os-release'): - $lines = file('/etc/os-release'); - foreach ($lines as $line) { - if (preg_match('/^ID=(.*)$/', $line, $matches)) { - $ret['dist'] = $matches[1]; - } - if (preg_match('/^ID_LIKE=(.*)$/', $line, $matches)) { - $ret['family'] = $matches[1]; - } - if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) { - $ret['ver'] = $matches[1]; - } + if (preg_match('/^ID_LIKE=(.*)$/', $line, $matches)) { + $ret['family'] = $matches[1]; } - $ret['dist'] = trim($ret['dist'], '"\''); - $ret['ver'] = trim($ret['ver'], '"\''); - if (strcasecmp($ret['dist'], 'centos') === 0) { - $ret['dist'] = 'redhat'; + if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) { + $ret['ver'] = $matches[1]; } - break; + } + + $ret['dist'] = trim($ret['dist'], '"\''); + $ret['ver'] = trim($ret['ver'], '"\''); + + if (strcasecmp($ret['dist'], 'centos') === 0) { + $ret['dist'] = 'redhat'; + } } return $ret; } From a0cab24e568f5f80de547127af3157f9ebc9daf0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 22 Jan 2026 09:32:22 +0800 Subject: [PATCH 146/682] Remove skeleton command --- skeleton-test.php | 28 -- src/StaticPHP/Command/Dev/SkeletonCommand.php | 402 ----------------- src/StaticPHP/ConsoleApplication.php | 2 - src/StaticPHP/Skeleton/ArtifactGenerator.php | 115 ----- src/StaticPHP/Skeleton/PackageGenerator.php | 412 ------------------ 5 files changed, 959 deletions(-) delete mode 100644 skeleton-test.php delete mode 100644 src/StaticPHP/Command/Dev/SkeletonCommand.php delete mode 100644 src/StaticPHP/Skeleton/ArtifactGenerator.php delete mode 100644 src/StaticPHP/Skeleton/PackageGenerator.php diff --git a/skeleton-test.php b/skeleton-test.php deleted file mode 100644 index 59fbbb7e1..000000000 --- a/skeleton-test.php +++ /dev/null @@ -1,28 +0,0 @@ -addDependency('bar') - ->addStaticLib('libfoo.a', 'unix') - ->addStaticLib('libfoo.a', 'unix') - ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz'])) - ->enableBuild(['Darwin', 'Linux'], 'build') - ->addFunctionExecutorBinding('build', new ExecutorGenerator(UnixCMakeExecutor::class)); - -$pkg_config = $package_generator->generateConfigArray(); -$artifact_config = $artifact_generator->generateConfigArray(); - -echo '===== pkg.json =====' . PHP_EOL; -echo json_encode($pkg_config, 64 | 128 | 256) . PHP_EOL; -echo '===== artifact.json =====' . PHP_EOL; -echo json_encode($artifact_config, 64 | 128 | 256) . PHP_EOL; -echo '===== php code for package =====' . PHP_EOL; -echo $package_generator->generatePackageClassFile('Package\Library'); diff --git a/src/StaticPHP/Command/Dev/SkeletonCommand.php b/src/StaticPHP/Command/Dev/SkeletonCommand.php deleted file mode 100644 index b19eb4a11..000000000 --- a/src/StaticPHP/Command/Dev/SkeletonCommand.php +++ /dev/null @@ -1,402 +0,0 @@ -output->writeln('The dev:skel command is not available in phar mode.'); - return 1; - } - if (SystemTarget::getTargetOS() === 'Windows') { - $this->output->writeln('The dev:skel command is not available on Windows systems.'); - return 1; - } - - $this->runMainMenu(); - - return 0; - } - - public function validatePackageName(string $name): ?string - { - if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { - return 'Library name can only contain letters, numbers, underscores, and hyphens.'; - } - // must start with a letter - if (!preg_match('/^[a-zA-Z]/', $name)) { - return 'Library name must start with a letter.'; - } - return null; - } - - private function runMainMenu(): void - { - $main = select('Please select the skeleton option', [ - 'library' => 'Create a new library package', - 'target' => 'Create a new target package', - 'php-extension' => 'Create a new PHP extension', - 'q' => 'Exit', - ]); - $generator = match ($main) { - 'library' => $this->runCreateLib(), - 'target' => $this->runCreateTarget(), - 'php-extension' => $this->runCreateExt(), - 'q' => exit(0), - default => null, - }; - $write = $generator->writeAll(); - $this->output->writeln("Package config in: {$write['package_config']}"); - $this->output->writeln("Artifact config in: {$write['artifact_config']}"); - $this->output->writeln('Package class:'); - $this->output->writeln($write['package_class_content']); - } - - private function runCreateLib(): PackageGenerator - { - // init empty - $static_libs = ''; - $headers = ''; - $static_bins = ''; - $pkg_configs = ''; - - // ask name - $package_name = text('Please enter your library name', placeholder: 'e.g. pcre2', validate: [$this, 'validatePackageName']); - - // ask OS - $os = select("[{$package_name}] On which OS family do you want to build this library?", [ - 'unix' => 'Both Linux and Darwin (unix-like OS)', - 'linux' => 'Linux only', - 'macos' => 'Darwin(macOS) only', - 'windows' => 'Windows only', - 'all' => 'All supported OS (' . implode(', ', SUPPORTED_OS_FAMILY) . ')', - ]); - - $produce = select("[{$package_name}] What does this library produce?", [ - 'static_libs' => 'Static Libraries (.a/.lib)', - 'headers' => 'Header Files (.h)', - 'static_bins' => 'Static Binaries (executables)', - 'pkg_configs' => 'Pkg-Config files (.pc)', - 'all' => 'All of the above', - ]); - - if ($produce === 'all' || $produce === 'static_libs') { - $static_libs = text( - 'Please enter the names of the static libraries produced', - placeholder: 'e.g. libpcre2.a, libbar.a', - default: str_starts_with($package_name, 'lib') ? "{$package_name}.a" : "lib{$package_name}.a", - validate: function ($value) { - $names = array_map('trim', explode(',', $value)); - if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) { - return 'Library names can only contain letters, numbers, underscores, hyphens, and dots.'; - } - return null; - }, - hint: 'Separate multiple names with commas' - ); - } - if ($produce === 'all' || $produce === 'headers') { - $headers = text( - 'Please enter the names of the header files produced', - placeholder: 'e.g. foo.h, bar.h', - default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.h' : $package_name . '.h', - validate: function ($value) { - $names = array_map('trim', explode(',', $value)); - if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) { - return 'Header file names can only contain letters, numbers, underscores, hyphens, and dots.'; - } - return null; - }, - hint: 'Separate multiple names with commas, directories are allowed (e.g. openssl directory)' - ); - } - if ($produce === 'all' || $produce === 'static_bins') { - $static_bins = text( - 'Please enter the names of the static binaries produced', - placeholder: 'e.g. foo, bar', - default: $package_name, - validate: function ($value) { - $names = array_map('trim', explode(',', $value)); - if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) { - return 'Binary names can only contain letters, numbers, underscores, hyphens, and dots.'; - } - return null; - }, - hint: 'Separate multiple names with commas' - ); - } - if ($produce === 'all' || $produce === 'pkg_configs') { - $pkg_configs = text( - 'Please enter the names of the pkg-config files produced', - placeholder: 'e.g. foo.pc, bar.pc', - default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.pc' : $package_name . '.pc', - validate: function ($value) { - if (!str_ends_with($value, '.pc')) { - return 'Pkg-config file names must end with .pc extension.'; - } - return null; - }, - hint: 'Separate multiple names with commas' - ); - } - - if ($headers === '' && $static_bins === '' && $static_libs === '' && $pkg_configs === '') { - $this->output->writeln('You must specify at least one of static libraries, header files, or static binaries produced.'); - exit(1); - } - - // ask source - $artifact_generator = $this->runCreateArtifact($package_name, true, false, null); - $package_generator = new PackageGenerator($package_name, 'library'); - // set artifact - $package_generator = $package_generator->addArtifact($artifact_generator); - // set os - $package_generator = match ($os) { - 'unix' => $package_generator->enableBuild(['Darwin', 'Linux'], 'build'), - 'linux' => $package_generator->enableBuild(['Linux'], 'build'), - 'macos' => $package_generator->enableBuild(['Darwin'], 'build'), - 'windows' => $package_generator->enableBuild(['Windows'], 'build'), - 'all' => $package_generator->enableBuild(SUPPORTED_OS_FAMILY, 'build'), - default => $package_generator, - }; - // set produce - if ($static_libs !== '') { - $lib_names = array_map('trim', explode(',', $static_libs)); - foreach ($lib_names as $lib_name) { - $package_generator = $package_generator->addStaticLib($lib_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); - } - } - if ($headers !== '') { - $header_names = array_map('trim', explode(',', $headers)); - foreach ($header_names as $header_name) { - $package_generator = $package_generator->addHeaderFile($header_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); - } - } - if ($static_bins !== '') { - $bin_names = array_map('trim', explode(',', $static_bins)); - foreach ($bin_names as $bin_name) { - $package_generator = $package_generator->addStaticBin($bin_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); - } - } - if ($pkg_configs !== '') { - $pc_names = array_map('trim', explode(',', $pkg_configs)); - foreach ($pc_names as $pc_name) { - $package_generator = $package_generator->addPkgConfigFile($pc_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os)); - } - } - // ask for package config writing selection, same as artifact - $package_configs = Registry::getLoadedPackageConfigs(); - $package_config_file = select("[{$package_name}] Please select the package config file to write the package config to", $package_configs); - return $package_generator->setConfigFile($package_config_file); - } - - private function runCreateArtifact( - string $package_name, - ?bool $create_source, - ?bool $create_binary, - string|true|null $default_extract_dir = true - ): ArtifactGenerator { - $artifact = new ArtifactGenerator($package_name); - - if ($create_source === null) { - $create_source = confirm("[{$package_name}] Do you want to create a source artifact?"); - } - - if (!$create_source) { - goto binary; - } - - $source_type = select("[{$package_name}] Where is the source code located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME); - - $source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'source'); - $artifact = $artifact->setSource($source_config); - - binary: - if ($create_binary === null) { - $create_binary = confirm("[{$package_name}] Do you want to create a binary artifact?"); - } - - if (!$create_binary) { - goto end; - } - - $binary_fix = [ - 'macos-x86_64' => null, - 'macos-aarch64' => null, - 'linux-x86_64' => null, - 'linux-aarch64' => null, - 'windows-x86_64' => null, - ]; - while (($os = select("[{$package_name}] Please configure the binary downloading options for OS", [ - 'macos-x86_64' => 'macos-x86_64' . ($binary_fix['macos-x86_64'] ? ' (done)' : ''), - 'macos-aarch64' => 'macos-aarch64' . ($binary_fix['macos-aarch64'] ? ' (done)' : ''), - 'linux-x86_64' => 'linux-x86_64' . ($binary_fix['linux-x86_64'] ? ' (done)' : ''), - 'linux-aarch64' => 'linux-aarch64' . ($binary_fix['linux-aarch64'] ? ' (done)' : ''), - 'windows-x86_64' => 'windows-x86_64' . ($binary_fix['windows-x86_64'] ? ' (done)' : ''), - 'copy' => 'Duplicate from another OS', - 'finish' => 'Submit', - ])) !== 'finish') { - $source_type = select("[{$package_name}] Where is the binary for {$os} located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME); - $source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'binary'); - // set to artifact - $artifact = $artifact->setBinary($os, $source_config); - $binary_fix[$os] = true; - } - - end: - - // generate config files, select existing package config file to write - $artifact_configs = Registry::getLoadedArtifactConfigs(); - $artifact_config_file = select("[{$package_name}] Please select the artifact config file to write the artifact config to", $artifact_configs); - return $artifact->setConfigFile($artifact_config_file); - } - - private function runCreateTarget(): PackageGenerator - { - throw new WrongUsageException('Not implemented'); - } - - private function runCreateExt(): PackageGenerator - { - throw new WrongUsageException('Not implemented'); - } - - private function askDownloadTypeConfig(string $package_name, int|string $source_type, bool|string|null $default_extract_dir, string $config_type): array - { - $source_config = ['type' => $source_type]; - switch ($source_type) { - case 'bitbuckettag': - $source_config['repo'] = text("[{$package_name}] Please enter the BitBucket repository (e.g. user/repo)"); - break; - case 'filelist': - $source_config['url'] = text( - "[{$package_name}] Please enter the file index website URL", - placeholder: 'e.g. https://ftp.gnu.org/pub/gnu/gettext/', - hint: 'Make sure the target url is a directory listing page like ftp.gnu.org.' - ); - $source_config['regex'] = text( - "[{$package_name}] Please enter the regex pattern to match the archive file", - placeholder: 'e.g. /gettext-(\d+\.\d+(\.\d+)?)\.tar\.gz/', - default: "/href=\"(?{$package_name}-(?[^\"]+)\\.tar\\.gz)\"/", - hint: 'Make sure the regex contains a capturing group for the version number.' - ); - break; - case 'git': - $source_config['url'] = text( - "[{$package_name}] Please enter the Git repository URL", - validate: function ($value) { - if (!filter_var($value, FILTER_VALIDATE_URL) && !preg_match('/^(git|ssh|http|https|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/', $value)) { - return 'Please enter a valid Git repository URL.'; - } - return null; - }, - hint: 'e.g. https://github.com/user/repo.git' - ); - $source_config['rev'] = text( - "[{$package_name}] Please enter the Git revision (branch, tag, or commit hash)", - default: 'main', - hint: 'e.g. main, master, v1.0.0, or a commit hash' - ); - break; - case 'ghrel': - $source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)"); - $source_config['match'] = text( - "[{$package_name}] Please enter the regex pattern to match the source archive file", - placeholder: 'e.g. /foo-(\d+\.\d+(\.\d+)?)\.tar\.gz/', - default: "{$package_name}-.+\\.tar\\.gz", - ); - break; - case 'ghtar': - case 'ghtagtar': - $source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)"); - $source_config['prefer-stable'] = confirm("[{$package_name}] Do you want to prefer stable releases?"); - if ($source_type === 'ghtagtar' && confirm('Do you want to match tags with a specific pattern?', default: false)) { - $source_config['match'] = text( - "[{$package_name}] Please enter the regex pattern to match tags", - placeholder: 'e.g. v(\d+\.\d+(\.\d+)?)', - ); - } - break; - case 'local': - $source_config['dirname'] = text( - "[{$package_name}] Please enter the local directory path", - validate: function ($value) { - if (trim($value) === '') { - return 'Local source directory cannot be empty.'; - } - if (!is_dir($value)) { - return 'The specified local source directory does not exist.'; - } - return null; - }, - ); - break; - case 'pie': - $source_config['repo'] = text( - "[{$package_name}] Please enter the PIE repository name", - placeholder: 'e.g. user/repo', - ); - break; - case 'url': - $source_config['url'] = text( - "[{$package_name}] Please enter the file download URL", - validate: function ($value) { - if (!filter_var($value, FILTER_VALIDATE_URL)) { - return 'Please enter a valid URL.'; - } - return null; - }, - ); - break; - case 'custom': - break; - } - // ask extract dir if is true - if ($default_extract_dir === true) { - if (confirm('Do you want to specify a custom extract directory?')) { - $extract_hint = match ($config_type) { - 'source' => 'the source will be from the `source/` dir by default', - 'binary' => 'the binary will be from the `pkgroot/{arch}-{os}/` dir by default', - default => '', - }; - $default_extract_dir = text( - "[{$package_name}] Please enter the source extract directory", - validate: function ($value) { - if (trim($value) === '') { - return 'Extract directory cannot be empty.'; - } - return null; - }, - hint: 'You can use relative path, ' . $extract_hint . '.' - ); - } else { - $default_extract_dir = null; - } - } - if ($default_extract_dir !== null) { - $source_config['extract'] = $default_extract_dir; - } - - // return config - return $source_config; - } -} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index fd7650aa8..9e37698c6 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -9,7 +9,6 @@ use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\ShellCommand; -use StaticPHP\Command\Dev\SkeletonCommand; use StaticPHP\Command\Dev\SortConfigCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; @@ -61,7 +60,6 @@ public function __construct() new ShellCommand(), new IsInstalledCommand(), new EnvCommand(), - new SkeletonCommand(), new SortConfigCommand(), ]); diff --git a/src/StaticPHP/Skeleton/ArtifactGenerator.php b/src/StaticPHP/Skeleton/ArtifactGenerator.php deleted file mode 100644 index e05f94d14..000000000 --- a/src/StaticPHP/Skeleton/ArtifactGenerator.php +++ /dev/null @@ -1,115 +0,0 @@ -name; - } - - public function setSource(array $source): static - { - $clone = clone $this; - $clone->source = $source; - return $clone; - } - - public function setCustomSource(): static - { - $clone = clone $this; - $clone->source = ['type' => 'custom']; - $clone->generate_class = true; - $clone->generate_custom_source_func = true; - return $clone; - } - - public function getSource(): ?array - { - return $this->source; - } - - public function setBinary(string $os, array $config): static - { - $clone = clone $this; - if ($clone->binary === null) { - $clone->binary = [$os => $config]; - } else { - $clone->binary[$os] = $config; - } - return $clone; - } - - public function generateConfigArray(): array - { - $config = []; - - if ($this->source) { - $config['source'] = $this->source; - } - if ($this->binary) { - $config['binary'] = $this->binary; - } - return $config; - } - - public function setConfigFile(string $file): static - { - $clone = clone $this; - $clone->config_file = $file; - return $clone; - } - - /** - * Write the artifact configuration to the config file. - */ - public function writeConfigFile(): string - { - if ($this->config_file === null) { - throw new ValidationException('Config file path is not set.'); - } - $config_array = $this->generateConfigArray(); - $config_file_json = json_decode(FileSystem::readFile($this->config_file), true); - if (!is_array($config_file_json)) { - throw new ValidationException('Existing config file is not a valid JSON array.'); - } - - $config_file_json[$this->name] = $config_array; - // sort keys - ksort($config_file_json); - $json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($json_content === false) { - throw new ValidationException('Failed to encode config array to JSON.'); - } - if (file_put_contents($this->config_file, $json_content) === false) { - throw new FileSystemException("Failed to write config file: {$this->config_file}"); - } - return $this->config_file; - } -} diff --git a/src/StaticPHP/Skeleton/PackageGenerator.php b/src/StaticPHP/Skeleton/PackageGenerator.php deleted file mode 100644 index 89802899a..000000000 --- a/src/StaticPHP/Skeleton/PackageGenerator.php +++ /dev/null @@ -1,412 +0,0 @@ - $depends An array of dependencies required by the package, categorized by operating system. */ - protected array $depends = []; - - /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */ - protected array $suggests = []; - - /** @var array $frameworks An array of macOS frameworks for the package */ - protected array $frameworks = []; - - /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */ - protected array $static_libs = []; - - /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $headers An array of header files required by the package, categorized by operating system. */ - protected array $headers = []; - - /** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */ - protected array $static_bins = []; - - protected ?string $config_file = null; - - /** @var null|ArtifactGenerator $artifact Artifact */ - protected ?ArtifactGenerator $artifact = null; - - /** @var array $licenses Licenses */ - protected array $licenses = []; - - /** @var array<'Darwin'|'Linux'|'Windows', null|string> $build_for_enables Enable build function generating */ - protected array $build_for_enables = [ - 'Darwin' => null, - 'Linux' => null, - 'Windows' => null, - ]; - - /** @var array */ - protected array $func_executor_binding = []; - - /** - * @param string $package_name Package name - * @param 'library'|'php-extension'|'target'|'virtual-target' $type Package type ('library', 'target', 'virtual-target', etc.) - */ - public function __construct(protected string $package_name, protected string $type) {} - - /** - * Add package dependency. - * - * @param string $package Package name - * @param string $os_category Operating system ('' for all OSes, 'unix', 'windows', 'macos') - */ - public function addDependency(string $package, string $os_category = ''): static - { - if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { - throw new ValidationException("Invalid OS suffix: {$os_category}"); - } - $clone = clone $this; - if (!isset($clone->depends[$os_category])) { - $clone->depends[$os_category] = []; - } - if (!in_array($package, $clone->depends[$os_category], true)) { - $clone->depends[$os_category][] = $package; - } - return $clone; - } - - /** - * Add package suggestion. - * - * @param string $package Package name - * @param string $os_category Operating system ('' for all OSes) - */ - public function addSuggestion(string $package, string $os_category = ''): static - { - if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { - throw new ValidationException("Invalid OS suffix: {$os_category}"); - } - $clone = clone $this; - if (!isset($clone->suggests[$os_category])) { - $clone->suggests[$os_category] = []; - } - if (!in_array($package, $clone->suggests[$os_category], true)) { - $clone->suggests[$os_category][] = $package; - } - return $clone; - } - - public function addStaticLib(string $lib_a, string $os_category = ''): static - { - if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { - throw new ValidationException("Invalid OS suffix: {$os_category}"); - } - if (!str_ends_with($lib_a, '.lib') && !str_ends_with($lib_a, '.a')) { - throw new ValidationException("Static library must end with .lib or .a, got: {$lib_a}"); - } - if (str_ends_with($lib_a, '.lib') && in_array($os_category, ['unix', 'linux', 'macos'], true)) { - throw new ValidationException("Static library with .lib extension cannot be added for non-Windows OS: {$lib_a}"); - } - if (str_ends_with($lib_a, '.a') && $os_category === 'windows') { - throw new ValidationException("Static library with .a extension cannot be added for Windows OS: {$lib_a}"); - } - if (isset($this->static_libs[$os_category]) && in_array($lib_a, $this->static_libs[$os_category], true)) { - // already exists - return $this; - } - $clone = clone $this; - if (!isset($clone->static_libs[$os_category])) { - $clone->static_libs[$os_category] = []; - } - if (!in_array($lib_a, $clone->static_libs[$os_category], true)) { - $clone->static_libs[$os_category][] = $lib_a; - } - return $clone; - } - - public function addHeader(string $header_file, string $os_category = ''): static - { - if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { - throw new ValidationException("Invalid OS suffix: {$os_category}"); - } - $clone = clone $this; - if (!isset($clone->headers[$os_category])) { - $clone->headers[$os_category] = []; - } - if (!in_array($header_file, $clone->headers[$os_category], true)) { - $clone->headers[$os_category][] = $header_file; - } - return $clone; - } - - public function addStaticBin(string $bin_file, string $os_category = ''): static - { - if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) { - throw new ValidationException("Invalid OS suffix: {$os_category}"); - } - $clone = clone $this; - if (!isset($clone->static_bins[$os_category])) { - $clone->static_bins[$os_category] = []; - } - if (!in_array($bin_file, $clone->static_bins[$os_category], true)) { - $clone->static_bins[$os_category][] = $bin_file; - } - return $clone; - } - - /** - * Add package artifact. - * - * @param ArtifactGenerator $artifactGenerator Artifact generator - */ - public function addArtifact(ArtifactGenerator $artifactGenerator): static - { - $clone = clone $this; - $clone->artifact = $artifactGenerator; - return $clone; - } - - /** - * Add license from string. - * - * @param string $text License content - */ - public function addLicenseFromString(string $text): static - { - $clone = clone $this; - $clone->licenses[] = [ - 'type' => 'text', - 'text' => $text, - ]; - return $clone; - } - - /** - * Add license from file. - * - * @param string $file_path License file path - */ - public function addLicenseFromFile(string $file_path): static - { - $clone = clone $this; - $clone->licenses[] = [ - 'type' => 'file', - 'path' => $file_path, - ]; - return $clone; - } - - /** - * Enable build for specific OS. - * - * @param 'Darwin'|'Linux'|'Windows'|array<'Darwin'|'Linux'|'Windows'> $build_for Build for OS - */ - public function enableBuild(array|string $build_for, ?string $build_function_name = null): static - { - $clone = clone $this; - if (is_array($build_for)) { - foreach ($build_for as $bf) { - $clone = $clone->enableBuild($bf, $build_function_name ?? 'build'); - } - return $clone; - } - if (!in_array($build_for, SUPPORTED_OS_FAMILY, true)) { - throw new ValidationException("Unsupported build_for value: {$build_for}"); - } - $clone->build_for_enables[$build_for] = $build_function_name ?? "buildFor{$build_for}"; - return $clone; - } - - /** - * Bind function executor. - * - * @param string $func_name Function name - * @param ExecutorGenerator $executor Executor generator - */ - public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator $executor): static - { - $clone = clone $this; - $clone->func_executor_binding[$func_name] = $executor; - return $clone; - } - - public function generatePackageClassFile(string $namespace, bool $uppercase = false): string - { - $printer = new class extends Printer { - public string $indentation = ' '; - }; - $file = new PhpFile(); - $namespace = $file->setStrictTypes()->addNamespace($namespace); - - $uses = []; - - // class name and package attribute - $class_name = str_replace('-', '_', $uppercase ? ucwords($this->package_name, '-') : $this->package_name); - $class_attribute = match ($this->type) { - 'library' => Library::class, - 'php-extension' => Extension::class, - 'target', 'virtual-target' => Target::class, - }; - $package_class = match ($this->type) { - 'library' => LibraryPackage::class, - 'php-extension' => PhpExtensionPackage::class, - 'target', 'virtual-target' => TargetPackage::class, - }; - $uses[] = $class_attribute; - $uses[] = $package_class; - $uses[] = BuildFor::class; - $uses[] = PackageInstaller::class; - - foreach ($uses as $use) { - $namespace->addUse($use); - } - - // add class attribute - $class = $namespace->addClass($class_name); - $class->addAttribute($class_attribute, [$this->package_name]); - - // add build functions if enabled - $funcs = []; - foreach ($this->build_for_enables as $os_family => $func_name) { - if ($func_name !== null) { - $funcs[$func_name][] = $os_family; - } - } - foreach ($funcs as $name => $oss) { - $method = $class->addMethod(name: $name ?: 'build') - ->setPublic() - ->setReturnType('void'); - // check if function executor is bound - if (isset($this->func_executor_binding[$name])) { - $executor = $this->func_executor_binding[$name]; - [$executor_use, $code] = $executor->generateCode(); - $namespace->addUse($executor_use); - $method->setBody($code); - } - $method->addParameter('package')->setType($package_class); - $method->addParameter('installer')->setType(PackageInstaller::class); - foreach ($oss as $os) { - $method->addAttribute(BuildFor::class, [$os]); - } - } - - return $printer->printFile($file); - } - - /** - * Generate package config - */ - public function generateConfigArray(): array - { - $config = ['type' => $this->type]; - - // Add dependencies - foreach ($this->depends as $suffix => $depends) { - $k = $suffix !== '' ? "depends@{$suffix}" : 'depends'; - $config[$k] = $depends; - } - - // add suggests - foreach ($this->suggests as $suffix => $suggests) { - $k = $suffix !== '' ? "suggests@{$suffix}" : 'suggests'; - $config[$k] = $suggests; - } - - // Add frameworks - if (!empty($this->frameworks)) { - $config['frameworks'] = $this->frameworks; - } - - // Add static libs - foreach ($this->static_libs as $suffix => $libs) { - $k = $suffix !== '' ? "static-libs@{$suffix}" : 'static-libs'; - $config[$k] = $libs; - } - - // Add headers - foreach ($this->headers as $suffix => $headers) { - $k = $suffix !== '' ? "headers@{$suffix}" : 'headers'; - $config[$k] = $headers; - } - - // Add static bins - foreach ($this->static_bins as $suffix => $bins) { - $k = $suffix !== '' ? "static-bins@{$suffix}" : 'static-bins'; - $config[$k] = $bins; - } - - // Add artifact - if ($this->artifact !== null) { - $config['artifact'] = $this->artifact->getName(); - } - - // Add licenses - if (!empty($this->licenses)) { - if (count($this->licenses) === 1) { - $config['license'] = $this->licenses[0]; - } else { - $config['license'] = $this->licenses; - } - } - - return $config; - } - - public function setConfigFile(string $config_file): static - { - $clone = clone $this; - $clone->config_file = $config_file; - return $clone; - } - - public function writeConfigFile(): string - { - if ($this->config_file === null) { - throw new ValidationException('Config file path is not set.'); - } - $config_array = $this->generateConfigArray(); - $config_file_json = json_decode(FileSystem::readFile($this->config_file), true); - if (!is_array($config_file_json)) { - throw new ValidationException('Existing config file is not a valid JSON array.'); - } - $config_file_json[$this->package_name] = $config_array; - ksort($config_file_json); - $json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - if ($json_content === false) { - throw new ValidationException('Failed to encode package config to JSON.'); - } - if (file_put_contents($this->config_file, $json_content) === false) { - throw new FileSystemException("Failed to write config file: {$this->config_file}"); - } - return $this->config_file; - } - - public function writeAll(): array - { - // write config - $package_config_file = $this->writeConfigFile(); - $artifact_config_file = $this->artifact->writeConfigFile(); - - // write class file - $package_class_file_content = $this->generatePackageClassFile('StaticPHP\Packages'); - $package_class_file_path = str_replace('-', '_', $this->package_name) . '.php'; - // file_put_contents($package_class_file_path, $package_class_file_content); // Uncomment this line to actually write the file - return [ - 'package_config' => $package_config_file, - 'artifact_config' => $artifact_config_file, - 'package_class_file' => $package_class_file_path, - 'package_class_content' => $package_class_file_content, - ]; - } -} From 1865762f80e2d6c7ec13cf690c3d1edc4e34f3b0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 22 Jan 2026 16:03:01 +0800 Subject: [PATCH 147/682] Fix config yaml support --- src/StaticPHP/Config/ArtifactConfig.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index 49abae926..0ae9e2849 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -6,6 +6,7 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\Registry; +use Symfony\Component\Yaml\Yaml; class ArtifactConfig { @@ -40,7 +41,11 @@ public static function loadFromFile(string $file, string $registry_name): string if ($content === false) { throw new WrongUsageException("Failed to read artifact config file: {$file}"); } - $data = json_decode($content, true); + $data = match (pathinfo($file, PATHINFO_EXTENSION)) { + 'json' => json_decode($content, true), + 'yml', 'yaml' => Yaml::parse($content), + default => throw new WrongUsageException("Unsupported artifact config file format: {$file}"), + }; if (!is_array($data)) { throw new WrongUsageException("Invalid JSON format in artifact config file: {$file}"); } From ae748757d1a9a420d55141b2cbe471d81c3c3682 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 22 Jan 2026 16:03:06 +0800 Subject: [PATCH 148/682] Fix config yaml support --- src/StaticPHP/Config/PackageConfig.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index 56ef7ab1c..92c01f70f 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -7,6 +7,7 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\Registry; use StaticPHP\Runtime\SystemTarget; +use Symfony\Component\Yaml\Yaml; class PackageConfig { @@ -47,10 +48,12 @@ public static function loadFromFile(string $file, string $registry_name): string if ($content === false) { throw new WrongUsageException("Failed to read package config file: {$file}"); } - $data = json_decode($content, true); - if (!is_array($data)) { - throw new WrongUsageException("Invalid JSON format in package config file: {$file}"); - } + // judge extension + $data = match (pathinfo($file, PATHINFO_EXTENSION)) { + 'json' => json_decode($content, true), + 'yml', 'yaml' => Yaml::parse($content), + default => throw new WrongUsageException("Unsupported package config file format: {$file}"), + }; ConfigValidator::validateAndLintPackages(basename($file), $data); foreach ($data as $pkg_name => $config) { self::$package_configs[$pkg_name] = $config; From 7b725bb4da0a605c8c63f0ebac50c58f5d07c21e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 22 Jan 2026 16:04:48 +0800 Subject: [PATCH 149/682] Add LicenseDumper component --- src/StaticPHP/Command/DumpLicenseCommand.php | 147 +++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Util/LicenseDumper.php | 262 +++++++++++++++++++ 3 files changed, 411 insertions(+) create mode 100644 src/StaticPHP/Command/DumpLicenseCommand.php create mode 100644 src/StaticPHP/Util/LicenseDumper.php diff --git a/src/StaticPHP/Command/DumpLicenseCommand.php b/src/StaticPHP/Command/DumpLicenseCommand.php new file mode 100644 index 000000000..d90ecbf95 --- /dev/null +++ b/src/StaticPHP/Command/DumpLicenseCommand.php @@ -0,0 +1,147 @@ +addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to dump licenses, comma separated, e.g "php-src,openssl,curl"'); + + // v2 compatible options + $this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Dump by extensions (automatically includes php-src), e.g "openssl,mbstring"'); + $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Dump by libraries, e.g "openssl,zlib,curl"'); + + // v3 options + $this->addOption('for-packages', 'p', InputOption::VALUE_REQUIRED, 'Dump by packages, e.g "php,libssl,libcurl"'); + $this->addOption('dump-dir', 'd', InputOption::VALUE_REQUIRED, 'Target directory for dumped licenses', BUILD_ROOT_PATH . '/license'); + $this->addOption('without-suggests', null, null, 'Do not include suggested packages when using --for-extensions or --for-packages'); + } + + public function handle(): int + { + $dumper = new LicenseDumper(); + $dump_dir = $this->getOption('dump-dir'); + $artifacts_to_dump = []; + + // Handle direct artifact argument + if ($artifacts = $this->getArgument('artifacts')) { + $artifacts_to_dump = array_merge($artifacts_to_dump, parse_comma_list($artifacts)); + } + + // Handle --for-extensions option + if ($exts = $this->getOption('for-extensions')) { + $artifacts_to_dump = array_merge( + $artifacts_to_dump, + $this->resolveFromExtensions(parse_extension_list($exts)) + ); + } + + // Handle --for-libs option (v2 compat) + if ($libs = $this->getOption('for-libs')) { + $artifacts_to_dump = array_merge( + $artifacts_to_dump, + $this->resolveFromPackages(parse_comma_list($libs)) + ); + } + + // Handle --for-packages option + if ($packages = $this->getOption('for-packages')) { + $artifacts_to_dump = array_merge( + $artifacts_to_dump, + $this->resolveFromPackages(parse_comma_list($packages)) + ); + } + + // Check if any artifacts to dump + if (empty($artifacts_to_dump)) { + $this->output->writeln('No artifacts specified. Use one of:'); + $this->output->writeln(' - Direct argument: dump-license php-src,openssl,curl'); + $this->output->writeln(' - --for-extensions: dump-license --for-extensions=openssl,mbstring'); + $this->output->writeln(' - --for-libs: dump-license --for-libs=openssl,zlib'); + $this->output->writeln(' - --for-packages: dump-license --for-packages=php,libssl'); + return self::FAILURE; + } + + // Deduplicate artifacts + $artifacts_to_dump = array_values(array_unique($artifacts_to_dump)); + + logger()->info('Dumping licenses for ' . count($artifacts_to_dump) . ' artifact(s)'); + logger()->debug('Artifacts: ' . implode(', ', $artifacts_to_dump)); + + // Add artifacts to dumper + $dumper->addArtifacts($artifacts_to_dump); + + // Dump + $success = $dumper->dump($dump_dir); + + if ($success) { + InteractiveTerm::success('Licenses dumped successfully: ' . $dump_dir); + // $this->output->writeln("✓ Successfully dumped licenses to: {$dump_dir}"); + // $this->output->writeln(" Total artifacts: " . count($artifacts_to_dump) . ''); + return self::SUCCESS; + } + + $this->output->writeln('Failed to dump licenses'); + return self::FAILURE; + } + + /** + * Resolve artifacts from extension names. + * + * @param array $extensions Extension names + * @return array Artifact names + */ + private function resolveFromExtensions(array $extensions): array + { + // Convert extension names to package names + $packages = array_map(fn ($ext) => "ext-{$ext}", $extensions); + + // Automatically include php-related artifacts + array_unshift($packages, 'php'); + array_unshift($packages, 'php-micro'); + array_unshift($packages, 'php-embed'); + array_unshift($packages, 'php-fpm'); + + return $this->resolveFromPackages($packages); + } + + /** + * Resolve artifacts from package names. + * + * @param array $packages Package names + * @return array Artifact names + */ + private function resolveFromPackages(array $packages): array + { + $artifacts = []; + $include_suggests = !$this->getOption('without-suggests'); + + // Resolve package dependencies + $resolved_packages = DependencyResolver::resolve($packages, [], $include_suggests); + + foreach ($resolved_packages as $pkg_name) { + try { + $pkg = PackageLoader::getPackage($pkg_name); + if ($artifact = $pkg->getArtifact()) { + $artifacts[] = $artifact->getName(); + } + } catch (\Throwable $e) { + logger()->debug("Package {$pkg_name} has no artifact or failed to load: {$e->getMessage()}"); + } + } + + return array_unique($artifacts); + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 9e37698c6..bd2e4eb19 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -12,6 +12,7 @@ use StaticPHP\Command\Dev\SortConfigCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; +use StaticPHP\Command\DumpLicenseCommand; use StaticPHP\Command\ExtractCommand; use StaticPHP\Command\InstallPackageCommand; use StaticPHP\Command\SPCConfigCommand; @@ -55,6 +56,7 @@ public function __construct() new BuildLibsCommand(), new ExtractCommand(), new SPCConfigCommand(), + new DumpLicenseCommand(), // dev commands new ShellCommand(), diff --git a/src/StaticPHP/Util/LicenseDumper.php b/src/StaticPHP/Util/LicenseDumper.php new file mode 100644 index 000000000..57fc3aec4 --- /dev/null +++ b/src/StaticPHP/Util/LicenseDumper.php @@ -0,0 +1,262 @@ + Artifact names to dump */ + private array $artifacts = []; + + /** + * Add artifacts by name. + * + * @param array $artifacts Artifact names + */ + public function addArtifacts(array $artifacts): self + { + $this->artifacts = array_unique(array_merge($this->artifacts, $artifacts)); + return $this; + } + + /** + * Dump all collected artifact licenses to target directory. + * + * @param string $target_dir Target directory path + * @return bool True on success + */ + public function dump(string $target_dir): bool + { + // Create target directory if not exists (don't clean existing files) + if (!is_dir($target_dir)) { + FileSystem::createDir($target_dir); + } else { + logger()->debug("Target directory exists, will append/update licenses: {$target_dir}"); + } + + $license_summary = []; + $dumped_count = 0; + + foreach ($this->artifacts as $artifact_name) { + $artifact = ArtifactLoader::getArtifactInstance($artifact_name); + if ($artifact === null) { + logger()->warning("Artifact not found, skipping: {$artifact_name}"); + continue; + } + + try { + $result = $this->dumpArtifactLicense($artifact, $target_dir, $license_summary); + if ($result) { + ++$dumped_count; + } + } catch (\Throwable $e) { + logger()->warning("Failed to dump license for {$artifact_name}: {$e->getMessage()}"); + } + } + + // Generate LICENSE-SUMMARY.json (read-modify-write) + $this->generateSummary($target_dir, $license_summary); + + logger()->info("Successfully dumped {$dumped_count} license(s) to: {$target_dir}"); + return true; + } + + /** + * Dump license for a single artifact. + * + * @param Artifact $artifact Artifact instance + * @param string $target_dir Target directory + * @param array &$license_summary Summary data to populate + * @return bool True if dumped + * @throws SPCInternalException + */ + private function dumpArtifactLicense(Artifact $artifact, string $target_dir, array &$license_summary): bool + { + $artifact_name = $artifact->getName(); + + // Get metadata from ArtifactConfig + $artifact_config = ArtifactConfig::get($artifact_name); + $config = $artifact_config['metadata'] ?? null; + + if ($config === null) { + logger()->debug("No metadata for artifact: {$artifact_name}"); + return false; + } + + $license_type = $config['license'] ?? null; + $license_files = $config['license-files'] ?? []; + + // Ensure license_files is array + if (is_string($license_files)) { + $license_files = [$license_files]; + } + + if (empty($license_files)) { + logger()->debug("No license files specified for: {$artifact_name}"); + return false; + } + + // Record in summary + $summary_license = $license_type ?? 'Custom'; + if (!isset($license_summary[$summary_license])) { + $license_summary[$summary_license] = []; + } + $license_summary[$summary_license][] = $artifact_name; + + // Dump each license file + $file_count = count($license_files); + $dumped_any = false; + + foreach ($license_files as $index => $license_file_path) { + // Construct output filename + if ($file_count === 1) { + $output_filename = "{$artifact_name}_LICENSE.txt"; + } else { + $output_filename = "{$artifact_name}_LICENSE_{$index}.txt"; + } + + $output_path = "{$target_dir}/{$output_filename}"; + + // Skip if file already exists (avoid duplicate writes) + if (file_exists($output_path)) { + logger()->debug("License file already exists, skipping: {$output_filename}"); + $dumped_any = true; // Still count as dumped + continue; + } + + // Try to read license file from source directory + $license_content = $this->readLicenseFile($artifact, $license_file_path); + if ($license_content === null) { + logger()->warning("License file not found for {$artifact_name}: {$license_file_path}"); + continue; + } + + // Write to target + if (file_put_contents($output_path, $license_content) === false) { + throw new SPCInternalException("Failed to write license file: {$output_path}"); + } + + logger()->info("Dumped license: {$output_filename}"); + $dumped_any = true; + } + + return $dumped_any; + } + + /** + * Read license file content from artifact's source directory. + * + * @param Artifact $artifact Artifact instance + * @param string $license_file_path Relative path to license file + * @return null|string License content, or null if not found + */ + private function readLicenseFile(Artifact $artifact, string $license_file_path): ?string + { + $artifact_name = $artifact->getName(); + + // Try source directory first (if extracted) + if ($artifact->isSourceExtracted()) { + $source_dir = $artifact->getSourceDir(); + $full_path = "{$source_dir}/{$license_file_path}"; + + logger()->debug("Checking license file: {$full_path}"); + if (file_exists($full_path)) { + logger()->info("Reading license from source: {$full_path}"); + return file_get_contents($full_path); + } + } else { + logger()->debug("Artifact source not extracted: {$artifact_name}"); + } + + // Fallback: try SOURCE_PATH directly + $fallback_path = SOURCE_PATH . "/{$artifact_name}/{$license_file_path}"; + logger()->debug("Checking fallback path: {$fallback_path}"); + if (file_exists($fallback_path)) { + logger()->info("Reading license from fallback path: {$fallback_path}"); + return file_get_contents($fallback_path); + } + + logger()->debug("License file not found in any location for {$artifact_name}"); + return null; + } + + /** + * Generate LICENSE-SUMMARY.json file with read-modify-write support. + * + * @param string $target_dir Target directory + * @param array $license_summary License summary data (license_type => [artifacts]) + */ + private function generateSummary(string $target_dir, array $license_summary): void + { + if (empty($license_summary)) { + logger()->debug('No licenses to summarize'); + return; + } + + $summary_file = "{$target_dir}/LICENSE-SUMMARY.json"; + + // Read existing summary if exists + $existing_data = []; + if (file_exists($summary_file)) { + $content = file_get_contents($summary_file); + $existing_data = json_decode($content, true) ?? []; + logger()->debug('Loaded existing LICENSE-SUMMARY.json'); + } + + // Initialize structure + if (!isset($existing_data['artifacts'])) { + $existing_data['artifacts'] = []; + } + if (!isset($existing_data['summary'])) { + $existing_data['summary'] = ['license_types' => []]; + } + + // Merge new license information + foreach ($license_summary as $license_type => $artifacts) { + foreach ($artifacts as $artifact_name) { + // Add/update artifact info + $existing_data['artifacts'][$artifact_name] = [ + 'license' => $license_type, + 'dumped_at' => date('Y-m-d H:i:s'), + ]; + + // Update license_types summary + if (!isset($existing_data['summary']['license_types'][$license_type])) { + $existing_data['summary']['license_types'][$license_type] = []; + } + if (!in_array($artifact_name, $existing_data['summary']['license_types'][$license_type])) { + $existing_data['summary']['license_types'][$license_type][] = $artifact_name; + } + } + } + + // Sort license types and artifacts + ksort($existing_data['summary']['license_types']); + foreach ($existing_data['summary']['license_types'] as &$artifacts) { + sort($artifacts); + } + ksort($existing_data['artifacts']); + + // Update totals + $existing_data['summary']['total_artifacts'] = count($existing_data['artifacts']); + $existing_data['summary']['total_license_types'] = count($existing_data['summary']['license_types']); + $existing_data['summary']['last_updated'] = date('Y-m-d H:i:s'); + + // Write JSON file + file_put_contents( + $summary_file, + json_encode($existing_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) + ); + + logger()->info('Generated LICENSE-SUMMARY.json with ' . $existing_data['summary']['total_artifacts'] . ' artifact(s)'); + } +} From 22fc7030f69a6d42aff0a424c2ad1d5f31c38be5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 22 Jan 2026 16:05:21 +0800 Subject: [PATCH 150/682] Implement attr, brotli, bzip2 build for unix --- config/artifact.yaml | 33 ++++++++++++++++++++ config/pkg.lib.yaml | 21 +++++++++++++ spc.registry.json | 4 +-- src/Package/Artifact/attr.php | 23 ++++++++++++++ src/Package/Artifact/bzip2.php | 24 +++++++++++++++ src/Package/Library/attr.php | 29 ++++++++++++++++++ src/Package/Library/brotli.php | 55 ++++++++++++++++++++++++++++++++++ src/Package/Library/bzip2.php | 25 ++++++++++++++++ src/globals/licenses/bzip2.txt | 14 +++++++++ 9 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 config/artifact.yaml create mode 100644 config/pkg.lib.yaml create mode 100644 src/Package/Artifact/attr.php create mode 100644 src/Package/Artifact/bzip2.php create mode 100644 src/Package/Library/attr.php create mode 100644 src/Package/Library/brotli.php create mode 100644 src/Package/Library/bzip2.php create mode 100644 src/globals/licenses/bzip2.txt diff --git a/config/artifact.yaml b/config/artifact.yaml new file mode 100644 index 000000000..7ed407363 --- /dev/null +++ b/config/artifact.yaml @@ -0,0 +1,33 @@ +attr: + source: + type: url + url: 'https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz' + source-mirror: + type: url + url: 'https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz' + metadata: + license-files: ['doc/COPYING.LGPL'] + license: LGPL-2.1-or-later + +brotli: + source: + type: ghtagtar + repo: google/brotli + match: 'v1\.\d.*' + binary: hosted # 等价于v2的provide-pre-built: true + metadata: + license-files: ['LICENSE'] + license: MIT + +bzip2: + source: + type: url + url: 'https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz' + source-mirror: + type: filelist + url: 'https://sourceware.org/pub/bzip2/' + regex: '/href="(?bzip2-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: ['{registry_root}/src/globals/licenses/bzip2.txt'] + license: bzip2-1.0.6 diff --git a/config/pkg.lib.yaml b/config/pkg.lib.yaml new file mode 100644 index 000000000..baf4ebc9d --- /dev/null +++ b/config/pkg.lib.yaml @@ -0,0 +1,21 @@ +attr: + type: library + static-libs@unix: + - libattr.a + artifact: attr +brotli: + type: library + pkg-configs: + - libbrotlicommon + - libbrotlidec + - libbrotlienc + headers: + - brotli + artifact: brotli +bzip2: + type: library + static-libs@unix: + - libbz2.a + headers: + - bzlib.h + artifact: bzip2 diff --git a/spc.registry.json b/spc.registry.json index 7c5a8ce7b..3ced56564 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -12,13 +12,13 @@ }, "config": [ "config/pkg.ext.json", - "config/pkg.lib.json", + "config/pkg.lib.yaml", "config/pkg.target.json" ] }, "artifact": { "config": [ - "config/artifact.json" + "config/artifact.yaml" ], "psr-4": { "Package\\Artifact": "src/Package/Artifact" diff --git a/src/Package/Artifact/attr.php b/src/Package/Artifact/attr.php new file mode 100644 index 000000000..e80c8c831 --- /dev/null +++ b/src/Package/Artifact/attr.php @@ -0,0 +1,23 @@ +getSourceDir()); + } + } +} diff --git a/src/Package/Artifact/bzip2.php b/src/Package/Artifact/bzip2.php new file mode 100644 index 000000000..a6a1e58e5 --- /dev/null +++ b/src/Package/Artifact/bzip2.php @@ -0,0 +1,24 @@ +getSourceDir() . '/Makefile', + 'CFLAGS=-Wall', + 'CFLAGS=-fPIC -Wall' + ); + } +} diff --git a/src/Package/Library/attr.php b/src/Package/Library/attr.php new file mode 100644 index 000000000..40637e80a --- /dev/null +++ b/src/Package/Library/attr.php @@ -0,0 +1,29 @@ +appendEnv([ + 'CFLAGS' => '-Wno-int-conversion -Wno-implicit-function-declaration', + ]) + ->exec('libtoolize --force --copy') + ->exec('./autogen.sh || autoreconf -if') + ->configure('--disable-nls') + ->make('install-attributes_h install-data install-libattr_h install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfDATA', with_install: false); + $lib->patchPkgconfPrefix(['libattr.pc'], PKGCONF_PATCH_PREFIX); + } +} diff --git a/src/Package/Library/brotli.php b/src/Package/Library/brotli.php new file mode 100644 index 000000000..f22b9ef29 --- /dev/null +++ b/src/Package/Library/brotli.php @@ -0,0 +1,55 @@ +setBuildDir($lib->getSourceDir() . '/build-dir') + ->addConfigureArgs("-DSHARE_INSTALL_PREFIX={$lib->getBuildRootPath()}") + ->build(); + + // Patch pkg-config files + $lib->patchPkgconfPrefix(['libbrotlicommon.pc', 'libbrotlidec.pc', 'libbrotlienc.pc'], PKGCONF_PATCH_PREFIX); + + // Add -lbrotlicommon to libbrotlidec.pc and libbrotlienc.pc + FileSystem::replaceFileLineContainsString( + $lib->getLibDir() . '/pkgconfig/libbrotlidec.pc', + 'Libs: -L${libdir} -lbrotlidec', + 'Libs: -L${libdir} -lbrotlidec -lbrotlicommon' + ); + FileSystem::replaceFileLineContainsString( + $lib->getLibDir() . '/pkgconfig/libbrotlienc.pc', + 'Libs: -L${libdir} -lbrotlienc', + 'Libs: -L${libdir} -lbrotlienc -lbrotlicommon' + ); + + // Create symlink: libbrotli.a -> libbrotlicommon.a + shell()->cd($lib->getLibDir())->exec('ln -sf libbrotlicommon.a libbrotli.a'); + + // Remove dynamic libraries + foreach (FileSystem::scanDirFiles($lib->getLibDir(), false, true) as $filename) { + if (str_starts_with($filename, 'libbrotli') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { + unlink($lib->getLibDir() . '/' . $filename); + } + } + + // Remove brotli binary if exists + if (file_exists($lib->getBinDir() . '/brotli')) { + unlink($lib->getBinDir() . '/brotli'); + } + } +} diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php new file mode 100644 index 000000000..7f554ab48 --- /dev/null +++ b/src/Package/Library/bzip2.php @@ -0,0 +1,25 @@ +cd($lib->getSourceDir())->initializeEnv($lib) + ->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") + ->exec("make -j{$builder->concurrency} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") + ->exec('cp libbz2.a ' . $lib->getLibDir()) + ->exec('cp bzlib.h ' . $lib->getIncludeDir()); + } +} diff --git a/src/globals/licenses/bzip2.txt b/src/globals/licenses/bzip2.txt new file mode 100644 index 000000000..2f2ed1cd0 --- /dev/null +++ b/src/globals/licenses/bzip2.txt @@ -0,0 +1,14 @@ +This program, "bzip2", the associated library "libbzip2", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010 + +PATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement. From c27ed8b0b4990848d4676bb45d48c500ec92bb4b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 22 Jan 2026 16:50:31 +0800 Subject: [PATCH 151/682] Implement fastlz, zlib (unix) --- config/artifact.yaml | 19 ++++++++++++++++++ config/pkg.lib.yaml | 17 ++++++++++++++++ src/Package/Library/fastlz.php | 36 ++++++++++++++++++++++++++++++++++ src/Package/Library/zlib.php | 24 +++++++++++++++++++++++ src/globals/licenses/zlib.txt | 20 +++++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 src/Package/Library/fastlz.php create mode 100644 src/Package/Library/zlib.php create mode 100644 src/globals/licenses/zlib.txt diff --git a/config/artifact.yaml b/config/artifact.yaml index 7ed407363..cfbb832d0 100644 --- a/config/artifact.yaml +++ b/config/artifact.yaml @@ -31,3 +31,22 @@ bzip2: metadata: license-files: ['{registry_root}/src/globals/licenses/bzip2.txt'] license: bzip2-1.0.6 + +fastlz: + source: + type: git + url: 'https://github.com/ariya/FastLZ.git' + rev: master + metadata: + license-files: ['LICENSE.MIT'] + license: MIT + +zlib: + source: + type: ghrel + repo: madler/zlib + match: 'zlib.+\.tar\.gz' + binary: hosted + metadata: + license-files: ['{registry_root}/src/globals/licenses/zlib.txt'] + license: Zlib-Custom diff --git a/config/pkg.lib.yaml b/config/pkg.lib.yaml index baf4ebc9d..f38a5512d 100644 --- a/config/pkg.lib.yaml +++ b/config/pkg.lib.yaml @@ -19,3 +19,20 @@ bzip2: headers: - bzlib.h artifact: bzip2 + +fastlz: + type: library + static-libs@unix: + - libfastlz.a + headers: + - fastlz.h + artifact: fastlz + +zlib: + type: library + static-libs@unix: + - libz.a + headers: + - zlib.h + - zconf.h + artifact: zlib diff --git a/src/Package/Library/fastlz.php b/src/Package/Library/fastlz.php new file mode 100644 index 000000000..f9dd010dc --- /dev/null +++ b/src/Package/Library/fastlz.php @@ -0,0 +1,36 @@ +cd($lib->getSourceDir())->initializeEnv($lib) + ->exec("{$cc} -c -O3 -fPIC fastlz.c -o fastlz.o") + ->exec("{$ar} rcs libfastlz.a fastlz.o"); + + // Copy header file + if (!copy($lib->getSourceDir() . '/fastlz.h', $lib->getIncludeDir() . '/fastlz.h')) { + throw new BuildFailureException('Failed to copy fastlz.h'); + } + + // Copy static library + if (!copy($lib->getSourceDir() . '/libfastlz.a', $lib->getLibDir() . '/libfastlz.a')) { + throw new BuildFailureException('Failed to copy libfastlz.a'); + } + } +} diff --git a/src/Package/Library/zlib.php b/src/Package/Library/zlib.php new file mode 100644 index 000000000..8706dfe9b --- /dev/null +++ b/src/Package/Library/zlib.php @@ -0,0 +1,24 @@ +exec("./configure --static --prefix={$lib->getBuildRootPath()}")->make(); + + // Patch pkg-config file + $lib->patchPkgconfPrefix(['zlib.pc'], PKGCONF_PATCH_PREFIX); + } +} diff --git a/src/globals/licenses/zlib.txt b/src/globals/licenses/zlib.txt new file mode 100644 index 000000000..b698f3434 --- /dev/null +++ b/src/globals/licenses/zlib.txt @@ -0,0 +1,20 @@ +(C) 1995-2022 Jean-loup Gailly and Mark Adler + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +Jean-loup Gailly Mark Adler +jloup@gzip.org madler@alumni.caltech.edu From 223dd10ac6bbe97934e0e4fa7648b655d6b71ca9 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 24 Jan 2026 20:26:16 +0100 Subject: [PATCH 152/682] fix spx shared libadd --- src/SPC/builder/Extension.php | 2 +- src/SPC/builder/extension/excimer.php | 19 ------------------- src/SPC/builder/extension/spx.php | 7 +++++++ 3 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 src/SPC/builder/extension/excimer.php diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 925a4c8ef..f5a5d9561 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -542,7 +542,7 @@ public function getLibraryDependencies(bool $recursive = false): array */ protected function getSharedExtensionEnv(): array { - $config = (new SPCConfigUtil($this->builder))->getExtensionConfig($this); + $config = (new SPCConfigUtil($this->builder, ['no_php' => true]))->getExtensionConfig($this); [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; diff --git a/src/SPC/builder/extension/excimer.php b/src/SPC/builder/extension/excimer.php deleted file mode 100644 index 03dd8f228..000000000 --- a/src/SPC/builder/extension/excimer.php +++ /dev/null @@ -1,19 +0,0 @@ -source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h'); return true; } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + $env['SPX_SHARED_LIBADD'] = $env['LIBS']; + return $env; + } } From a7092212233668189db51c623198e42be1ccd3b2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 26 Jan 2026 00:43:57 +0800 Subject: [PATCH 153/682] Add skip helper function for calling events --- src/StaticPHP/DI/CallbackInvoker.php | 8 +++++++- src/StaticPHP/Exception/SkipException.php | 7 +++++++ src/globals/functions.php | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/StaticPHP/Exception/SkipException.php diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php index 0d77f7aab..5b70d7b2a 100644 --- a/src/StaticPHP/DI/CallbackInvoker.php +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -5,6 +5,7 @@ namespace StaticPHP\DI; use DI\Container; +use StaticPHP\Exception\SkipException; use StaticPHP\Exception\SPCInternalException; /** @@ -92,7 +93,12 @@ public function invoke(callable $callback, array $context = []): mixed ); } - return $callback(...$args); + try { + return $callback(...$args); + } catch (SkipException $e) { + logger()->debug("Skipped invocation: {$e->getMessage()}"); + return null; + } } /** diff --git a/src/StaticPHP/Exception/SkipException.php b/src/StaticPHP/Exception/SkipException.php new file mode 100644 index 000000000..af25b6da5 --- /dev/null +++ b/src/StaticPHP/Exception/SkipException.php @@ -0,0 +1,7 @@ + Date: Mon, 26 Jan 2026 00:46:42 +0800 Subject: [PATCH 154/682] Add openssl lib support --- config/artifact.yaml | 15 ++++ config/pkg.lib.yaml | 10 +++ src/Package/Artifact/openssl.php | 35 +++++++++ src/Package/Library/openssl.php | 89 ++++++++++++++++++++++ src/StaticPHP/Package/LibraryPackage.php | 11 +++ src/StaticPHP/Package/Package.php | 16 ++++ src/StaticPHP/Package/PackageInstaller.php | 53 +++++++++++++ src/StaticPHP/Util/SPCConfigUtil.php | 25 +++--- 8 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 src/Package/Artifact/openssl.php create mode 100644 src/Package/Library/openssl.php diff --git a/config/artifact.yaml b/config/artifact.yaml index cfbb832d0..d01d62670 100644 --- a/config/artifact.yaml +++ b/config/artifact.yaml @@ -41,6 +41,21 @@ fastlz: license-files: ['LICENSE.MIT'] license: MIT +openssl: + source: + type: ghrel + repo: openssl/openssl + match: 'openssl.+\.tar\.gz' + prefer-stable: true + source-mirror: + type: filelist + url: 'https://www.openssl.org/source/' + regex: '/href="(?openssl-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: ['LICENSE.txt'] + license: OpenSSL + zlib: source: type: ghrel diff --git a/config/pkg.lib.yaml b/config/pkg.lib.yaml index f38a5512d..7c4df0005 100644 --- a/config/pkg.lib.yaml +++ b/config/pkg.lib.yaml @@ -28,6 +28,16 @@ fastlz: - fastlz.h artifact: fastlz +openssl: + type: library + static-libs@unix: + - libssl.a + - libcrypto.a + headers: ['openssl'] + depends: + - zlib + artifact: openssl + zlib: type: library static-libs@unix: diff --git a/src/Package/Artifact/openssl.php b/src/Package/Artifact/openssl.php new file mode 100644 index 000000000..8809d9ce0 --- /dev/null +++ b/src/Package/Artifact/openssl.php @@ -0,0 +1,35 @@ +', + '#include ' . PHP_EOL . '#include ' + ); + } +} diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php new file mode 100644 index 000000000..e0ee9edc3 --- /dev/null +++ b/src/Package/Library/openssl.php @@ -0,0 +1,89 @@ +getInstaller()->getLibraryPackage('zlib')->getStaticLibFiles(); + $arch = getenv('SPC_ARCH'); + + shell()->cd($pkg->getSourceDir())->initializeEnv($pkg) + ->exec( + './Configure no-shared zlib ' . + "--prefix={$pkg->getBuildRootPath()} " . + '--libdir=lib ' . + '--openssldir=/etc/ssl ' . + "darwin64-{$arch}-cc" + ) + ->exec('make clean') + ->exec("make -j{$pkg->getBuilder()->concurrency} CNF_EX_LIBS=\"{$zlib_libs}\"") + ->exec('make install_sw'); + $this->patchPkgConfig($pkg); + } + + #[BuildFor('Linux')] + public function build(LibraryPackage $lib): void + { + $arch = getenv('SPC_ARCH'); + + $env = "CC='" . getenv('CC') . ' -idirafter ' . BUILD_INCLUDE_PATH . + ' -idirafter /usr/include/ ' . + ' -idirafter /usr/include/' . getenv('SPC_ARCH') . '-linux-gnu/ ' . + "' "; + + $ex_lib = trim($lib->getInstaller()->getLibraryPackage('zlib')->getStaticLibFiles()) . ' -ldl -pthread'; + $zlib_extra = + '--with-zlib-include=' . BUILD_INCLUDE_PATH . ' ' . + '--with-zlib-lib=' . BUILD_LIB_PATH . ' '; + + $openssl_dir = getenv('OPENSSLDIR') ?: null; + // TODO: in v3 use the following: $openssl_dir ??= SystemUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; + $openssl_dir ??= '/etc/ssl'; + $ex_lib = trim($ex_lib); + + shell()->cd($lib->getSourceDir())->initializeEnv($lib) + ->exec( + "{$env} ./Configure no-shared zlib " . + "--prefix={$lib->getBuildRootPath()} " . + "--libdir={$lib->getLibDir()} " . + "--openssldir={$openssl_dir} " . + "{$zlib_extra}" . + 'enable-pie ' . + 'no-legacy ' . + 'no-tests ' . + "linux-{$arch}" + ) + ->exec('make clean') + ->exec("make -j{$lib->getBuilder()->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") + ->exec('make install_sw'); + $this->patchPkgConfig($lib); + } + + private function patchPkgConfig(LibraryPackage $pkg): void + { + $pkg->patchPkgconfPrefix(['libssl.pc', 'openssl.pc', 'libcrypto.pc']); + // patch for openssl 3.3.0+ + if (!str_contains($file = FileSystem::readFile("{$pkg->getLibDir()}/pkgconfig/libssl.pc"), 'prefix=')) { + FileSystem::writeFile("{$pkg->getLibDir()}/pkgconfig/libssl.pc", "prefix={$pkg->getBuildRootPath()}\n{$file}"); + } + if (!str_contains($file = FileSystem::readFile("{$pkg->getLibDir()}/pkgconfig/openssl.pc"), 'prefix=')) { + FileSystem::writeFile("{$pkg->getLibDir()}/pkgconfig/openssl.pc", "prefix={$pkg->getBuildRootPath()}\n{$file}"); + } + if (!str_contains($file = FileSystem::readFile("{$pkg->getLibDir()}/pkgconfig/libcrypto.pc"), 'prefix=')) { + FileSystem::writeFile("{$pkg->getLibDir()}/pkgconfig/libcrypto.pc", "prefix={$pkg->getBuildRootPath()}\n{$file}"); + } + FileSystem::replaceFileRegex("{$pkg->getLibDir()}/pkgconfig/libcrypto.pc", '/Libs.private:.*/m', 'Requires.private: zlib'); + FileSystem::replaceFileRegex("{$pkg->getLibDir()}/cmake/OpenSSL/OpenSSLConfig.cmake", '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")'); + } +} diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 97ec80077..1cdd229e0 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -7,6 +7,7 @@ use StaticPHP\Config\PackageConfig; use StaticPHP\Exception\PatchException; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\SPCConfigUtil; /** * Represents a library package with platform-specific build functions. @@ -159,6 +160,16 @@ public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCON } } + /** + * Get static library files for current package and its dependencies. + */ + public function getStaticLibFiles(): string + { + $config = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true]); + $res = $config->config([$this->getName()]); + return $res['libs']; + } + /** * Get extra LIBS for current package. * You need to define the environment variable in the format of {LIBRARY_NAME}_LIBS diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index aa8ab6f00..943d03b0d 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -135,6 +135,22 @@ public function hasBuildFunctionForCurrentOS(): bool return isset($this->build_functions[PHP_OS_FAMILY]); } + /** + * Get the PackageBuilder instance for this package. + */ + public function getBuilder(): PackageBuilder + { + return ApplicationContext::get(PackageBuilder::class); + } + + /** + * Get the PackageInstaller instance for this package. + */ + public function getInstaller(): PackageInstaller + { + return ApplicationContext::get(PackageInstaller::class); + } + /** * Get the name of the package. */ diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index ae3b7346a..c4262d9a3 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -402,11 +402,64 @@ public function installBinary(Package $package): int return SPC_STATUS_INSTALLED; } + /** + * @internal internally calling only, for users, please use specific getter, such as 'getLibraryPackage', 'getTaretPackage', etc + * @param string $package_name Package name + */ public function getPackage(string $package_name): ?Package { return $this->packages[$package_name] ?? null; } + /** + * Get a library package by name. + * + * @param string $package_name Package name + * @return null|LibraryPackage The library package instance or null if not found + */ + public function getLibraryPackage(string $package_name): ?LibraryPackage + { + $pkg = $this->getPackage($package_name); + if ($pkg instanceof LibraryPackage) { + return $pkg; + } + return null; + } + + /** + * Get a target package by name. + * + * @param string $package_name Package name + * @return null|TargetPackage The target package instance or null if not found + */ + public function getTargetPackage(string $package_name): ?TargetPackage + { + $pkg = $this->getPackage($package_name); + if ($pkg instanceof TargetPackage) { + return $pkg; + } + return null; + } + + /** + * Get a PHP extension by name. + * + * @param string $package_or_ext_name Extension name + * @return null|PhpExtensionPackage The target package instance or null if not found + */ + public function getPhpExtensionPackage(string $package_or_ext_name): ?PhpExtensionPackage + { + $pkg = $this->getPackage($package_or_ext_name); + if ($pkg instanceof PhpExtensionPackage) { + return $pkg; + } + $pkg = $this->getPackage("ext-{$package_or_ext_name}"); + if ($pkg instanceof PhpExtensionPackage) { + return $pkg; + } + return null; + } + /** * Validate that a package has required artifacts. */ diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index c525f8c79..3a20e56fe 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -209,18 +209,21 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st $frameworks = []; foreach ($packages as $package) { - // add pkg-configs libs - $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); - foreach ($pkg_configs as $pkg_config) { - if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + // parse pkg-configs only for unix systems + if (SystemTarget::isUnix()) { + // add pkg-configs libs + $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + foreach ($pkg_configs as $pkg_config) { + if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + } + } + $pkg_configs = implode(' ', $pkg_configs); + if ($pkg_configs !== '') { + // static libs with dependencies come in reverse order, so reverse this too + $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); + $lib_names = [...$lib_names, ...$pc_libs]; } - } - $pkg_configs = implode(' ', $pkg_configs); - if ($pkg_configs !== '') { - // static libs with dependencies come in reverse order, so reverse this too - $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); - $lib_names = [...$lib_names, ...$pc_libs]; } // convert all static-libs to short names $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); From 3a575f0bf7f1a6487beef0fd69f10df76a38f8e7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 26 Jan 2026 00:50:39 +0800 Subject: [PATCH 155/682] Use yml instead of yaml (sync with craft) --- config/{artifact.yaml => artifact.yml} | 0 config/{pkg.lib.yaml => pkg.lib.yml} | 0 spc.registry.json | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename config/{artifact.yaml => artifact.yml} (100%) rename config/{pkg.lib.yaml => pkg.lib.yml} (100%) diff --git a/config/artifact.yaml b/config/artifact.yml similarity index 100% rename from config/artifact.yaml rename to config/artifact.yml diff --git a/config/pkg.lib.yaml b/config/pkg.lib.yml similarity index 100% rename from config/pkg.lib.yaml rename to config/pkg.lib.yml diff --git a/spc.registry.json b/spc.registry.json index 3ced56564..0e4720aa5 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -12,13 +12,13 @@ }, "config": [ "config/pkg.ext.json", - "config/pkg.lib.yaml", + "config/pkg.lib.yml", "config/pkg.target.json" ] }, "artifact": { "config": [ - "config/artifact.yaml" + "config/artifact.yml" ], "psr-4": { "Package\\Artifact": "src/Package/Artifact" From 51415fb6bf1771a72687028c4759918f90925add Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 26 Jan 2026 00:50:53 +0800 Subject: [PATCH 156/682] Use shorter summary json file name --- src/StaticPHP/Util/LicenseDumper.php | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/StaticPHP/Util/LicenseDumper.php b/src/StaticPHP/Util/LicenseDumper.php index 57fc3aec4..f45c395a7 100644 --- a/src/StaticPHP/Util/LicenseDumper.php +++ b/src/StaticPHP/Util/LicenseDumper.php @@ -63,7 +63,7 @@ public function dump(string $target_dir): bool } } - // Generate LICENSE-SUMMARY.json (read-modify-write) + // Generate SUMMARY.json (read-modify-write) $this->generateSummary($target_dir, $license_summary); logger()->info("Successfully dumped {$dumped_count} license(s) to: {$target_dir}"); @@ -190,7 +190,7 @@ private function readLicenseFile(Artifact $artifact, string $license_file_path): } /** - * Generate LICENSE-SUMMARY.json file with read-modify-write support. + * Generate SUMMARY.json file with read-modify-write support. * * @param string $target_dir Target directory * @param array $license_summary License summary data (license_type => [artifacts]) @@ -202,14 +202,14 @@ private function generateSummary(string $target_dir, array $license_summary): vo return; } - $summary_file = "{$target_dir}/LICENSE-SUMMARY.json"; + $summary_file = "{$target_dir}/SUMMARY.json"; // Read existing summary if exists $existing_data = []; if (file_exists($summary_file)) { $content = file_get_contents($summary_file); $existing_data = json_decode($content, true) ?? []; - logger()->debug('Loaded existing LICENSE-SUMMARY.json'); + logger()->debug('Loaded existing SUMMARY.json'); } // Initialize structure @@ -217,7 +217,7 @@ private function generateSummary(string $target_dir, array $license_summary): vo $existing_data['artifacts'] = []; } if (!isset($existing_data['summary'])) { - $existing_data['summary'] = ['license_types' => []]; + $existing_data['summary'] = ['license-types' => []]; } // Merge new license information @@ -226,30 +226,30 @@ private function generateSummary(string $target_dir, array $license_summary): vo // Add/update artifact info $existing_data['artifacts'][$artifact_name] = [ 'license' => $license_type, - 'dumped_at' => date('Y-m-d H:i:s'), + 'dumped-at' => date('Y-m-d H:i:s'), ]; // Update license_types summary - if (!isset($existing_data['summary']['license_types'][$license_type])) { - $existing_data['summary']['license_types'][$license_type] = []; + if (!isset($existing_data['summary']['license-types'][$license_type])) { + $existing_data['summary']['license-types'][$license_type] = []; } - if (!in_array($artifact_name, $existing_data['summary']['license_types'][$license_type])) { - $existing_data['summary']['license_types'][$license_type][] = $artifact_name; + if (!in_array($artifact_name, $existing_data['summary']['license-types'][$license_type])) { + $existing_data['summary']['license-types'][$license_type][] = $artifact_name; } } } // Sort license types and artifacts - ksort($existing_data['summary']['license_types']); - foreach ($existing_data['summary']['license_types'] as &$artifacts) { + ksort($existing_data['summary']['license-types']); + foreach ($existing_data['summary']['license-types'] as &$artifacts) { sort($artifacts); } ksort($existing_data['artifacts']); // Update totals - $existing_data['summary']['total_artifacts'] = count($existing_data['artifacts']); - $existing_data['summary']['total_license_types'] = count($existing_data['summary']['license_types']); - $existing_data['summary']['last_updated'] = date('Y-m-d H:i:s'); + $existing_data['summary']['total-artifacts'] = count($existing_data['artifacts']); + $existing_data['summary']['total-license-types'] = count($existing_data['summary']['license-types']); + $existing_data['summary']['last-updated'] = date('Y-m-d H:i:s'); // Write JSON file file_put_contents( @@ -257,6 +257,6 @@ private function generateSummary(string $target_dir, array $license_summary): vo json_encode($existing_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ); - logger()->info('Generated LICENSE-SUMMARY.json with ' . $existing_data['summary']['total_artifacts'] . ' artifact(s)'); + logger()->info('Generated SUMMARY.json with ' . $existing_data['summary']['total-artifacts'] . ' artifact(s)'); } } From 4531c9fe57d54062c499c007687c412df13e8451 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 27 Jan 2026 00:57:58 +0100 Subject: [PATCH 157/682] add option to allow linking musl dynamically on alpine --- config/env.ini | 4 +++- src/SPC/util/SPCTarget.php | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/env.ini b/config/env.ini index c1d4dbc4a..a16bc160f 100644 --- a/config/env.ini +++ b/config/env.ini @@ -75,8 +75,10 @@ SPC_MICRO_PATCHES=static_extensions_win32,cli_checks,disable_huge_page,vcruntime ; - musl-native: used for alpine linux, can build `musl` and `musl -dynamic` target. ; - gnu-native: used for general linux distros, can build gnu target for the installed glibc version only. -; LEGACY option to specify the target +; option to specify the target, superceded by SPC_TARGET if set SPC_LIBC=musl +; uncomment to link libc dynamically on musl +; SPC_MUSL_DYNAMIC=true ; Recommended: specify your target here. Zig toolchain will be used. ; examples: diff --git a/src/SPC/util/SPCTarget.php b/src/SPC/util/SPCTarget.php index b8f5367ae..037c6d8c3 100644 --- a/src/SPC/util/SPCTarget.php +++ b/src/SPC/util/SPCTarget.php @@ -27,10 +27,10 @@ public static function isStatic(): bool return true; } if (ToolchainManager::getToolchainClass() === GccNativeToolchain::class) { - return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist(); + return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC'); } if (ToolchainManager::getToolchainClass() === ClangNativeToolchain::class) { - return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist(); + return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC'); } // if SPC_LIBC is set, it means the target is static, remove it when 3.0 is released if ($target = getenv('SPC_TARGET')) { From c5882c1f8e2d95643c50e29740209c1ea9e8dae8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 30 Jan 2026 19:41:39 +0100 Subject: [PATCH 158/682] fix gettext v1.0 release --- src/SPC/builder/unix/library/gettext.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/library/gettext.php b/src/SPC/builder/unix/library/gettext.php index 332e25c96..d383faf77 100644 --- a/src/SPC/builder/unix/library/gettext.php +++ b/src/SPC/builder/unix/library/gettext.php @@ -16,7 +16,11 @@ protected function build(): void ->addConfigureArgs( '--disable-java', '--disable-c++', - '--with-included-gettext', + '--disable-d', + '--disable-rpath', + '--disable-modula2', + '--disable-libasprintf', + '--with-included-libintl', "--with-iconv-prefix={$this->getBuildRootPath()}", ); From a414c65f372f376f8d716fded13de53f2165a940 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 09:59:10 +0800 Subject: [PATCH 159/682] Forward-port #1022 spc target env var --- src/StaticPHP/Toolchain/ClangNativeToolchain.php | 2 +- src/StaticPHP/Toolchain/GccNativeToolchain.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Toolchain/ClangNativeToolchain.php b/src/StaticPHP/Toolchain/ClangNativeToolchain.php index c34e619cd..2513dd714 100644 --- a/src/StaticPHP/Toolchain/ClangNativeToolchain.php +++ b/src/StaticPHP/Toolchain/ClangNativeToolchain.php @@ -52,6 +52,6 @@ public function getCompilerInfo(): ?string public function isStatic(): bool { - return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist(); + return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC'); } } diff --git a/src/StaticPHP/Toolchain/GccNativeToolchain.php b/src/StaticPHP/Toolchain/GccNativeToolchain.php index 92b82892e..dbf9925ec 100644 --- a/src/StaticPHP/Toolchain/GccNativeToolchain.php +++ b/src/StaticPHP/Toolchain/GccNativeToolchain.php @@ -49,6 +49,6 @@ public function getCompilerInfo(): ?string public function isStatic(): bool { - return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist(); + return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC'); } } From 455d42d1628f63280be3206d3a48211a83715899 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 13:32:35 +0800 Subject: [PATCH 160/682] Separate package config --- README-en.md | 3 - config/artifact.yaml | 67 -------------------- config/pkg.lib.yaml | 48 -------------- config/pkg/lib/attr.yml | 14 ++++ config/pkg/lib/brotli.yml | 17 +++++ config/pkg/lib/bzip2.yml | 18 ++++++ config/pkg/lib/fastlz.yml | 14 ++++ config/pkg/lib/gmp.yml | 19 ++++++ config/pkg/lib/openssl.yml | 22 +++++++ config/pkg/lib/zlib.yml | 16 +++++ spc.registry.json | 7 +- src/Package/Artifact/openssl.php | 4 +- src/Package/Library/gmp.php | 25 ++++++++ src/StaticPHP/Config/ArtifactConfig.php | 15 +++++ src/StaticPHP/Config/ConfigValidator.php | 42 +++++++++++- src/StaticPHP/Config/PackageConfig.php | 31 ++++++++- src/StaticPHP/Doctor/Item/LinuxToolCheck.php | 5 +- src/StaticPHP/Package/Package.php | 17 ++++- src/StaticPHP/Registry/Registry.php | 48 ++++++++------ src/StaticPHP/Util/LicenseDumper.php | 18 ++++-- src/globals/licenses/gmp.txt | 1 + 21 files changed, 294 insertions(+), 157 deletions(-) delete mode 100755 README-en.md delete mode 100644 config/artifact.yaml delete mode 100644 config/pkg.lib.yaml create mode 100644 config/pkg/lib/attr.yml create mode 100644 config/pkg/lib/brotli.yml create mode 100644 config/pkg/lib/bzip2.yml create mode 100644 config/pkg/lib/fastlz.yml create mode 100644 config/pkg/lib/gmp.yml create mode 100644 config/pkg/lib/openssl.yml create mode 100644 config/pkg/lib/zlib.yml create mode 100644 src/Package/Library/gmp.php create mode 100644 src/globals/licenses/gmp.txt diff --git a/README-en.md b/README-en.md deleted file mode 100755 index 505fd70a2..000000000 --- a/README-en.md +++ /dev/null @@ -1,3 +0,0 @@ -# static-php-cli - -English README has been moved to [README.md](README.md). diff --git a/config/artifact.yaml b/config/artifact.yaml deleted file mode 100644 index d01d62670..000000000 --- a/config/artifact.yaml +++ /dev/null @@ -1,67 +0,0 @@ -attr: - source: - type: url - url: 'https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz' - source-mirror: - type: url - url: 'https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz' - metadata: - license-files: ['doc/COPYING.LGPL'] - license: LGPL-2.1-or-later - -brotli: - source: - type: ghtagtar - repo: google/brotli - match: 'v1\.\d.*' - binary: hosted # 等价于v2的provide-pre-built: true - metadata: - license-files: ['LICENSE'] - license: MIT - -bzip2: - source: - type: url - url: 'https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz' - source-mirror: - type: filelist - url: 'https://sourceware.org/pub/bzip2/' - regex: '/href="(?bzip2-(?[^"]+)\.tar\.gz)"/' - binary: hosted - metadata: - license-files: ['{registry_root}/src/globals/licenses/bzip2.txt'] - license: bzip2-1.0.6 - -fastlz: - source: - type: git - url: 'https://github.com/ariya/FastLZ.git' - rev: master - metadata: - license-files: ['LICENSE.MIT'] - license: MIT - -openssl: - source: - type: ghrel - repo: openssl/openssl - match: 'openssl.+\.tar\.gz' - prefer-stable: true - source-mirror: - type: filelist - url: 'https://www.openssl.org/source/' - regex: '/href="(?openssl-(?[^"]+)\.tar\.gz)"/' - binary: hosted - metadata: - license-files: ['LICENSE.txt'] - license: OpenSSL - -zlib: - source: - type: ghrel - repo: madler/zlib - match: 'zlib.+\.tar\.gz' - binary: hosted - metadata: - license-files: ['{registry_root}/src/globals/licenses/zlib.txt'] - license: Zlib-Custom diff --git a/config/pkg.lib.yaml b/config/pkg.lib.yaml deleted file mode 100644 index 7c4df0005..000000000 --- a/config/pkg.lib.yaml +++ /dev/null @@ -1,48 +0,0 @@ -attr: - type: library - static-libs@unix: - - libattr.a - artifact: attr -brotli: - type: library - pkg-configs: - - libbrotlicommon - - libbrotlidec - - libbrotlienc - headers: - - brotli - artifact: brotli -bzip2: - type: library - static-libs@unix: - - libbz2.a - headers: - - bzlib.h - artifact: bzip2 - -fastlz: - type: library - static-libs@unix: - - libfastlz.a - headers: - - fastlz.h - artifact: fastlz - -openssl: - type: library - static-libs@unix: - - libssl.a - - libcrypto.a - headers: ['openssl'] - depends: - - zlib - artifact: openssl - -zlib: - type: library - static-libs@unix: - - libz.a - headers: - - zlib.h - - zconf.h - artifact: zlib diff --git a/config/pkg/lib/attr.yml b/config/pkg/lib/attr.yml new file mode 100644 index 000000000..4e7858513 --- /dev/null +++ b/config/pkg/lib/attr.yml @@ -0,0 +1,14 @@ +attr: + type: library + static-libs@unix: + - libattr.a + artifact: + source: + type: url + url: 'https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz' + source-mirror: + type: url + url: 'https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz' + metadata: + license-files: ['doc/COPYING.LGPL'] + license: LGPL-2.1-or-later diff --git a/config/pkg/lib/brotli.yml b/config/pkg/lib/brotli.yml new file mode 100644 index 000000000..342359aab --- /dev/null +++ b/config/pkg/lib/brotli.yml @@ -0,0 +1,17 @@ +brotli: + type: library + pkg-configs: + - libbrotlicommon + - libbrotlidec + - libbrotlienc + headers: + - brotli + artifact: + source: + type: ghtagtar + repo: google/brotli + match: 'v1\.\d.*' + binary: hosted + metadata: + license-files: ['LICENSE'] + license: MIT diff --git a/config/pkg/lib/bzip2.yml b/config/pkg/lib/bzip2.yml new file mode 100644 index 000000000..6f7b483b5 --- /dev/null +++ b/config/pkg/lib/bzip2.yml @@ -0,0 +1,18 @@ +bzip2: + type: library + static-libs@unix: + - libbz2.a + headers: + - bzlib.h + artifact: + source: + type: url + url: 'https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz' + source-mirror: + type: filelist + url: 'https://sourceware.org/pub/bzip2/' + regex: '/href="(?bzip2-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: ['{registry_root}/src/globals/licenses/bzip2.txt'] + license: bzip2-1.0.6 \ No newline at end of file diff --git a/config/pkg/lib/fastlz.yml b/config/pkg/lib/fastlz.yml new file mode 100644 index 000000000..32f26d8a0 --- /dev/null +++ b/config/pkg/lib/fastlz.yml @@ -0,0 +1,14 @@ +fastlz: + type: library + static-libs@unix: + - libfastlz.a + headers: + - fastlz.h + artifact: + source: + type: git + url: 'https://github.com/ariya/FastLZ.git' + rev: master + metadata: + license-files: ['LICENSE.MIT'] + license: MIT \ No newline at end of file diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml new file mode 100644 index 000000000..8a28069b8 --- /dev/null +++ b/config/pkg/lib/gmp.yml @@ -0,0 +1,19 @@ +gmp: + type: library + static-libs@unix: + - libgmp.a + headers: + - gmp.h + pkg-configs: + - gmp + artifact: + source: + type: filelist + url: 'https://gmplib.org/download/gmp/' + regex: '/href="(?gmp-(?[^"]+)\.tar\.xz)"/' + source-mirror: + type: url + url: 'https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz' + metadata: + license-files: ['@/gmp.txt'] + license: Custom diff --git a/config/pkg/lib/openssl.yml b/config/pkg/lib/openssl.yml new file mode 100644 index 000000000..ce83a4d41 --- /dev/null +++ b/config/pkg/lib/openssl.yml @@ -0,0 +1,22 @@ +openssl: + type: library + static-libs@unix: + - libssl.a + - libcrypto.a + headers: ['openssl'] + depends: + - zlib + artifact: + source: + type: ghrel + repo: openssl/openssl + match: 'openssl.+\.tar\.gz' + prefer-stable: true + source-mirror: + type: filelist + url: 'https://www.openssl.org/source/' + regex: '/href="(?openssl-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: ['LICENSE.txt'] + license: OpenSSL diff --git a/config/pkg/lib/zlib.yml b/config/pkg/lib/zlib.yml new file mode 100644 index 000000000..0a7eec716 --- /dev/null +++ b/config/pkg/lib/zlib.yml @@ -0,0 +1,16 @@ +zlib: + type: library + static-libs@unix: + - libz.a + headers: + - zlib.h + - zconf.h + artifact: + source: + type: ghrel + repo: madler/zlib + match: 'zlib.+\.tar\.gz' + binary: hosted + metadata: + license-files: ['{registry_root}/src/globals/licenses/zlib.txt'] + license: Zlib-Custom \ No newline at end of file diff --git a/spc.registry.json b/spc.registry.json index 3ced56564..e8be4b5d8 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -1,5 +1,5 @@ { - "name": "internal", + "name": "core", "autoload": "vendor/autoload.php", "doctor": { "psr-4": { @@ -12,14 +12,11 @@ }, "config": [ "config/pkg.ext.json", - "config/pkg.lib.yaml", + "config/pkg/lib", "config/pkg.target.json" ] }, "artifact": { - "config": [ - "config/artifact.yaml" - ], "psr-4": { "Package\\Artifact": "src/Package/Artifact" } diff --git a/src/Package/Artifact/openssl.php b/src/Package/Artifact/openssl.php index 8809d9ce0..325e6a09e 100644 --- a/src/Package/Artifact/openssl.php +++ b/src/Package/Artifact/openssl.php @@ -27,9 +27,9 @@ public function patchOpenssl11Darwin(string $target_path): void spc_skip_if(!file_exists("{$target_path}/openssl/test/v3ext.c"), 'v3ext.c not found, skipping patch.'); FileSystem::replaceFileStr( - SOURCE_PATH . '/openssl/test/v3ext.c', + "{$target_path}/openssl/test/v3ext.c", '#include ', - '#include ' . PHP_EOL . '#include ' + "#include \n#include " ); } } diff --git a/src/Package/Library/gmp.php b/src/Package/Library/gmp.php new file mode 100644 index 000000000..f8eb5f8e4 --- /dev/null +++ b/src/Package/Library/gmp.php @@ -0,0 +1,25 @@ +appendEnv(['CFLAGS' => '-std=c17']) + ->configure('--enable-fat') + ->make(); + $lib->patchPkgconfPrefix(['gmp.pc']); + } +} diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index 0ae9e2849..49bfec233 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -77,4 +77,19 @@ public static function get(string $artifact_name): ?array { return self::$artifact_configs[$artifact_name] ?? null; } + + /** + * Register an inline artifact configuration. + * Used when artifact is defined inline within a package configuration. + * + * @param string $artifact_name Artifact name (usually same as package name) + * @param array $config Artifact configuration + * @param string $registry_name Registry name + * @param string $source_info Source info for debugging + */ + public static function registerInlineArtifact(string $artifact_name, array $config, string $registry_name, string $source_info = 'inline'): void + { + self::$artifact_configs[$artifact_name] = $config; + Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $source_info); + } } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index b32f41063..1f3d5b9d9 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -18,7 +18,7 @@ class ConfigValidator 'type' => ConfigType::STRING, 'depends' => ConfigType::LIST_ARRAY, // @ 'suggests' => ConfigType::LIST_ARRAY, // @ - 'artifact' => ConfigType::STRING, + 'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT 'license' => [ConfigType::class, 'validateLicenseField'], 'lang' => ConfigType::STRING, 'frameworks' => ConfigType::LIST_ARRAY, // @ @@ -102,7 +102,14 @@ public static function validateAndLintArtifacts(string $config_file_name, mixed if (!is_array($data)) { throw new ValidationException("{$config_file_name} is broken"); } + + // Define allowed artifact fields + $allowed_artifact_fields = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata']; + foreach ($data as $name => $artifact) { + // First pass: validate unknown fields + self::validateNoInvalidFields('artifact', $name, $artifact, $allowed_artifact_fields); + foreach ($artifact as $k => $v) { // check source field if ($k === 'source' || $k === 'source-mirror') { @@ -202,6 +209,11 @@ public static function validateAndLintPackages(string $config_file_name, mixed & throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field"); } + // validate and lint inline artifact object if present + if (isset($pkg['artifact']) && is_array($pkg['artifact'])) { + self::validateAndLintInlineArtifact($name, $data[$name]['artifact']); + } + // check if "php-extension" package has php-extension specific fields and validate inside if ($pkg['type'] === 'php-extension') { self::validatePhpExtensionFields($name, $pkg); @@ -234,6 +246,19 @@ public static function validatePlatformString(string $platform): void } } + /** + * Validate artifact field - can be string (reference) or object (inline). + * + * @param mixed $value Field value + */ + public static function validateArtifactField(mixed $value): bool + { + if (!is_string($value) && !is_assoc_array($value)) { + return false; + } + return true; + } + /** * Validate an artifact download object field. * @@ -373,4 +398,19 @@ private static function validateNoInvalidFields(string $config_type, int|string } } } + + /** + * Validate and lint inline artifact object structure. + * + * @param string $pkg_name Package name + * @param array $artifact Inline artifact configuration (passed by reference to apply linting) + */ + private static function validateAndLintInlineArtifact(string $pkg_name, array &$artifact): void + { + // Validate and lint as if it's a standalone artifact + $temp_data = [$pkg_name => $artifact]; + self::validateAndLintArtifacts("inline artifact in package '{$pkg_name}'", $temp_data); + // Write back the linted artifact configuration + $artifact = $temp_data[$pkg_name]; + } } diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index 92c01f70f..bd9786c3d 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -23,7 +23,7 @@ public static function loadFromDir(string $dir, string $registry_name): array throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); } $loaded = []; - $files = glob("{$dir}/pkg.*.json"); + $files = glob("{$dir}/*"); if (is_array($files)) { foreach ($files as $file) { self::loadFromFile($file, $registry_name); @@ -58,10 +58,39 @@ public static function loadFromFile(string $file, string $registry_name): string foreach ($data as $pkg_name => $config) { self::$package_configs[$pkg_name] = $config; Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file); + + // Register inline artifact if present + if (isset($config['artifact']) && is_array($config['artifact'])) { + ArtifactConfig::registerInlineArtifact( + $pkg_name, + $config['artifact'], + $registry_name, + "inline in {$file}" + ); + } } return $file; } + public static function loadFromArray(array $data, string $registry_name): void + { + ConfigValidator::validateAndLintPackages('array_input', $data); + foreach ($data as $pkg_name => $config) { + self::$package_configs[$pkg_name] = $config; + Registry::_bindPackageConfigFile($pkg_name, $registry_name, 'array_input'); + + // Register inline artifact if present + if (isset($config['artifact']) && is_array($config['artifact'])) { + ArtifactConfig::registerInlineArtifact( + $pkg_name, + $config['artifact'], + $registry_name, + 'inline in array_input' + ); + } + } + } + /** * Check if a package configuration exists. */ diff --git a/src/StaticPHP/Doctor/Item/LinuxToolCheck.php b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php index 09161c74e..f4cdc3111 100644 --- a/src/StaticPHP/Doctor/Item/LinuxToolCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php @@ -19,7 +19,6 @@ class LinuxToolCheck 'bzip2', 'cmake', 'gcc', 'g++', 'patch', 'binutils-gold', 'libtoolize', 'which', - 'patchelf', ]; public const TOOLS_DEBIAN = [ @@ -28,7 +27,6 @@ class LinuxToolCheck 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'xz', 'libtoolize', 'which', - 'patchelf', ]; public const TOOLS_RHEL = [ @@ -36,8 +34,7 @@ class LinuxToolCheck 'git', 'autoconf', 'automake', 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'which', - 'xz', 'libtool', 'gettext-devel', - 'patchelf', 'file', + 'xz', 'libtool', 'gettext-devel', 'file', ]; public const TOOLS_ARCH = [ diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 943d03b0d..a0f415d13 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -175,8 +175,21 @@ public function getType(): string public function getArtifact(): ?Artifact { // find config - $artifact_name = PackageConfig::get($this->name, 'artifact'); - return $artifact_name !== null ? ArtifactLoader::getArtifactInstance($artifact_name) : null; + $artifact_field = PackageConfig::get($this->name, 'artifact'); + + if ($artifact_field === null) { + return null; + } + + if (is_string($artifact_field)) { + return ArtifactLoader::getArtifactInstance($artifact_field); + } + + if (is_array($artifact_field)) { + return ArtifactLoader::getArtifactInstance($this->name); + } + + return null; } /** diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index e464ed471..68aa766d2 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -13,6 +13,8 @@ class Registry { + private static ?string $current_registry_name = null; + /** @var string[] List of loaded registries */ private static array $loaded_registries = []; @@ -35,7 +37,7 @@ class Registry public static function getRegistryConfig(?string $registry_name = null): array { if ($registry_name === null && spc_mode(SPC_MODE_SOURCE)) { - return self::$registry_configs['internal']; + return self::$registry_configs['core']; } if ($registry_name !== null && isset(self::$registry_configs[$registry_name])) { return self::$registry_configs[$registry_name]; @@ -83,6 +85,8 @@ public static function loadRegistry(string $registry_file, bool $auto_require = logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}"); + self::$current_registry_name = $registry_name; + // Load composer autoload if specified (for external registries with their own dependencies) if (isset($data['autoload']) && is_string($data['autoload'])) { $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file)); @@ -94,24 +98,6 @@ public static function loadRegistry(string $registry_file, bool $auto_require = } } - // load doctor items from PSR-4 directories - if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { - foreach ($data['doctor']['psr-4'] as $namespace => $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); - } - } - - // load doctor items from specific classes - // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} - if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) { - foreach ($data['doctor']['classes'] as $key => $value) { - [$class, $file] = self::parseClassEntry($key, $value); - self::requireClassFile($class, $file, dirname($registry_file), $auto_require); - DoctorLoader::loadFromClass($class); - } - } - // load package configs if (isset($data['package']['config']) && is_array($data['package']['config'])) { foreach ($data['package']['config'] as $path) { @@ -136,6 +122,24 @@ public static function loadRegistry(string $registry_file, bool $auto_require = } } + // load doctor items from PSR-4 directories + if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { + foreach ($data['doctor']['psr-4'] as $namespace => $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load doctor items from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) { + foreach ($data['doctor']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + DoctorLoader::loadFromClass($class); + } + } + // load packages from PSR-4 directories if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) { foreach ($data['package']['psr-4'] as $namespace => $path) { @@ -193,6 +197,7 @@ public static function loadRegistry(string $registry_file, bool $auto_require = } ConsoleApplication::_addAdditionalCommands($instances); } + self::$current_registry_name = null; } /** @@ -300,6 +305,11 @@ public static function getLoadedArtifactConfigs(): array return self::$loaded_artifact_configs; } + public static function getCurrentRegistryName(): ?string + { + return self::$current_registry_name; + } + /** * Parse a class entry from the classes array. * Supports two formats: diff --git a/src/StaticPHP/Util/LicenseDumper.php b/src/StaticPHP/Util/LicenseDumper.php index 57fc3aec4..54c6266bb 100644 --- a/src/StaticPHP/Util/LicenseDumper.php +++ b/src/StaticPHP/Util/LicenseDumper.php @@ -163,18 +163,26 @@ private function readLicenseFile(Artifact $artifact, string $license_file_path): { $artifact_name = $artifact->getName(); - // Try source directory first (if extracted) - if ($artifact->isSourceExtracted()) { - $source_dir = $artifact->getSourceDir(); - $full_path = "{$source_dir}/{$license_file_path}"; + // replace + if (str_starts_with($license_file_path, '@/')) { + $license_file_path = str_replace('@/', ROOT_DIR . '/src/globals/licenses/', $license_file_path); + } + $source_dir = $artifact->getSourceDir(); + if (FileSystem::isRelativePath($license_file_path)) { + $full_path = "{$source_dir}/{$license_file_path}"; + } else { + $full_path = $license_file_path; + } + // Try source directory first (if extracted) + if ($artifact->isSourceExtracted() || file_exists($full_path)) { logger()->debug("Checking license file: {$full_path}"); if (file_exists($full_path)) { logger()->info("Reading license from source: {$full_path}"); return file_get_contents($full_path); } } else { - logger()->debug("Artifact source not extracted: {$artifact_name}"); + logger()->warning("Artifact source not extracted: {$artifact_name}"); } // Fallback: try SOURCE_PATH directly diff --git a/src/globals/licenses/gmp.txt b/src/globals/licenses/gmp.txt new file mode 100644 index 000000000..488f3b902 --- /dev/null +++ b/src/globals/licenses/gmp.txt @@ -0,0 +1 @@ +Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs. From f4a29c4830b0eacfe33313a83d6a360c54a97bfb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 14:13:05 +0800 Subject: [PATCH 161/682] Add dev:lint-config to replace sort-config command --- config/pkg/lib/attr.yml | 6 +++--- config/pkg/lib/brotli.yml | 14 +++++++------- config/pkg/lib/bzip2.yml | 10 +++++----- config/pkg/lib/fastlz.yml | 12 ++++++------ config/pkg/lib/gmp.yml | 12 ++++++------ config/pkg/lib/openssl.yml | 17 +++++++++-------- config/pkg/lib/zlib.yml | 14 +++++++------- src/StaticPHP/ConsoleApplication.php | 4 ++-- 8 files changed, 45 insertions(+), 44 deletions(-) diff --git a/config/pkg/lib/attr.yml b/config/pkg/lib/attr.yml index 4e7858513..289bcd67b 100644 --- a/config/pkg/lib/attr.yml +++ b/config/pkg/lib/attr.yml @@ -1,7 +1,5 @@ attr: type: library - static-libs@unix: - - libattr.a artifact: source: type: url @@ -10,5 +8,7 @@ attr: type: url url: 'https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz' metadata: - license-files: ['doc/COPYING.LGPL'] + license-files: [doc/COPYING.LGPL] license: LGPL-2.1-or-later + static-libs@unix: + - libattr.a diff --git a/config/pkg/lib/brotli.yml b/config/pkg/lib/brotli.yml index 342359aab..524f9ddc8 100644 --- a/config/pkg/lib/brotli.yml +++ b/config/pkg/lib/brotli.yml @@ -1,11 +1,5 @@ brotli: type: library - pkg-configs: - - libbrotlicommon - - libbrotlidec - - libbrotlienc - headers: - - brotli artifact: source: type: ghtagtar @@ -13,5 +7,11 @@ brotli: match: 'v1\.\d.*' binary: hosted metadata: - license-files: ['LICENSE'] + license-files: [LICENSE] license: MIT + headers: + - brotli + pkg-configs: + - libbrotlicommon + - libbrotlidec + - libbrotlienc diff --git a/config/pkg/lib/bzip2.yml b/config/pkg/lib/bzip2.yml index 6f7b483b5..132a24ffa 100644 --- a/config/pkg/lib/bzip2.yml +++ b/config/pkg/lib/bzip2.yml @@ -1,9 +1,5 @@ bzip2: type: library - static-libs@unix: - - libbz2.a - headers: - - bzlib.h artifact: source: type: url @@ -15,4 +11,8 @@ bzip2: binary: hosted metadata: license-files: ['{registry_root}/src/globals/licenses/bzip2.txt'] - license: bzip2-1.0.6 \ No newline at end of file + license: bzip2-1.0.6 + headers: + - bzlib.h + static-libs@unix: + - libbz2.a diff --git a/config/pkg/lib/fastlz.yml b/config/pkg/lib/fastlz.yml index 32f26d8a0..e3296831b 100644 --- a/config/pkg/lib/fastlz.yml +++ b/config/pkg/lib/fastlz.yml @@ -1,14 +1,14 @@ fastlz: type: library - static-libs@unix: - - libfastlz.a - headers: - - fastlz.h artifact: source: type: git url: 'https://github.com/ariya/FastLZ.git' rev: master metadata: - license-files: ['LICENSE.MIT'] - license: MIT \ No newline at end of file + license-files: [LICENSE.MIT] + license: MIT + headers: + - fastlz.h + static-libs@unix: + - libfastlz.a diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml index 8a28069b8..bdc13b559 100644 --- a/config/pkg/lib/gmp.yml +++ b/config/pkg/lib/gmp.yml @@ -1,11 +1,5 @@ gmp: type: library - static-libs@unix: - - libgmp.a - headers: - - gmp.h - pkg-configs: - - gmp artifact: source: type: filelist @@ -17,3 +11,9 @@ gmp: metadata: license-files: ['@/gmp.txt'] license: Custom + headers: + - gmp.h + pkg-configs: + - gmp + static-libs@unix: + - libgmp.a diff --git a/config/pkg/lib/openssl.yml b/config/pkg/lib/openssl.yml index ce83a4d41..22d065088 100644 --- a/config/pkg/lib/openssl.yml +++ b/config/pkg/lib/openssl.yml @@ -1,16 +1,10 @@ openssl: type: library - static-libs@unix: - - libssl.a - - libcrypto.a - headers: ['openssl'] - depends: - - zlib artifact: source: type: ghrel repo: openssl/openssl - match: 'openssl.+\.tar\.gz' + match: openssl.+\.tar\.gz prefer-stable: true source-mirror: type: filelist @@ -18,5 +12,12 @@ openssl: regex: '/href="(?openssl-(?[^"]+)\.tar\.gz)"/' binary: hosted metadata: - license-files: ['LICENSE.txt'] + license-files: [LICENSE.txt] license: OpenSSL + depends: + - zlib + headers: + - openssl + static-libs@unix: + - libssl.a + - libcrypto.a diff --git a/config/pkg/lib/zlib.yml b/config/pkg/lib/zlib.yml index 0a7eec716..9ca70f426 100644 --- a/config/pkg/lib/zlib.yml +++ b/config/pkg/lib/zlib.yml @@ -1,16 +1,16 @@ zlib: type: library - static-libs@unix: - - libz.a - headers: - - zlib.h - - zconf.h artifact: source: type: ghrel repo: madler/zlib - match: 'zlib.+\.tar\.gz' + match: zlib.+\.tar\.gz binary: hosted metadata: license-files: ['{registry_root}/src/globals/licenses/zlib.txt'] - license: Zlib-Custom \ No newline at end of file + license: Zlib-Custom + headers: + - zlib.h + - zconf.h + static-libs@unix: + - libz.a diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index bd2e4eb19..e65617053 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -8,8 +8,8 @@ use StaticPHP\Command\BuildTargetCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; +use StaticPHP\Command\Dev\LintConfigCommand; use StaticPHP\Command\Dev\ShellCommand; -use StaticPHP\Command\Dev\SortConfigCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\DumpLicenseCommand; @@ -62,7 +62,7 @@ public function __construct() new ShellCommand(), new IsInstalledCommand(), new EnvCommand(), - new SortConfigCommand(), + new LintConfigCommand(), ]); // add additional commands from registries From f437efebb7a93ce8a28ae90ab8d3228965775238 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 14:14:01 +0800 Subject: [PATCH 162/682] Add dev:lint-config to replace sort-config command --- .../Command/Dev/LintConfigCommand.php | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/LintConfigCommand.php diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php new file mode 100644 index 000000000..1b08bb85c --- /dev/null +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -0,0 +1,121 @@ +sortConfigFile($file, 'artifact'); + } + $loaded_pkg_configs = Registry::getLoadedPackageConfigs(); + foreach ($loaded_pkg_configs as $file) { + $this->sortConfigFile($file, 'package'); + } + return static::SUCCESS; + } + + public function artifactSortKey(string $a, string $b): int + { + // sort by predefined order, other not matching keys go to the end alphabetically + $order = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata']; + + $pos_a = array_search($a, $order, true); + $pos_b = array_search($b, $order, true); + + // Both in order list + if ($pos_a !== false && $pos_b !== false) { + return $pos_a <=> $pos_b; + } + + // Only $a in order list + if ($pos_a !== false) { + return -1; + } + + // Only $b in order list + if ($pos_b !== false) { + return 1; + } + + // Neither in order list, sort alphabetically + return $a <=> $b; + } + + public function packageSortKey(string $a, string $b): int + { + // sort by predefined order, other not matching keys go to the end alphabetically + $order = ['type', 'artifact']; + + // Handle suffix patterns (e.g., 'depends@unix', 'static-libs@windows') + $base_a = preg_replace('/@(unix|windows|macos|linux|freebsd|bsd)$/', '', $a); + $base_b = preg_replace('/@(unix|windows|macos|linux|freebsd|bsd)$/', '', $b); + + $pos_a = array_search($base_a, $order, true); + $pos_b = array_search($base_b, $order, true); + + // Both in order list + if ($pos_a !== false && $pos_b !== false) { + if ($pos_a === $pos_b) { + // Same base field, sort by suffix + return $a <=> $b; + } + return $pos_a <=> $pos_b; + } + + // Only $a in order list + if ($pos_a !== false) { + return -1; + } + + // Only $b in order list + if ($pos_b !== false) { + return 1; + } + + // Neither in order list, sort alphabetically + return $a <=> $b; + } + + private function sortConfigFile(mixed $file, string $config_type): void + { + // read file content with different extensions + $content = file_get_contents($file); + if ($content === false) { + $this->output->writeln("Failed to read artifact config file: {$file}"); + return; + } + $data = match (pathinfo($file, PATHINFO_EXTENSION)) { + 'json' => json_decode($content, true), + 'yml', 'yaml' => Yaml::parse($content), // skip yaml files for now + default => null, + }; + if (!is_array($data)) { + $this->output->writeln("Invalid JSON format in artifact config file: {$file}"); + return; + } + ksort($data); + foreach ($data as $artifact_name => &$config) { + uksort($config, $config_type === 'artifact' ? [$this, 'artifactSortKey'] : [$this, 'packageSortKey']); + } + unset($config); + $new_content = match (pathinfo($file, PATHINFO_EXTENSION)) { + 'json' => json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n", + 'yml', 'yaml' => Yaml::dump($data, 4, 2), + default => null, + }; + file_put_contents($file, $new_content); + $this->output->writeln("Sorted artifact config file: {$file}"); + } +} From 5a8341203ba190020fe21c46c6cfddbf0df76671 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 14:14:19 +0800 Subject: [PATCH 163/682] Remove sort config command --- .../Command/Dev/SortConfigCommand.php | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 src/StaticPHP/Command/Dev/SortConfigCommand.php diff --git a/src/StaticPHP/Command/Dev/SortConfigCommand.php b/src/StaticPHP/Command/Dev/SortConfigCommand.php deleted file mode 100644 index aa3a9ecdb..000000000 --- a/src/StaticPHP/Command/Dev/SortConfigCommand.php +++ /dev/null @@ -1,49 +0,0 @@ -sortConfigFile($file); - } - $loaded_pkg_configs = Registry::getLoadedPackageConfigs(); - foreach ($loaded_pkg_configs as $file) { - $this->sortConfigFile($file); - } - return static::SUCCESS; - } - - private function sortConfigFile(mixed $file): void - { - $content = file_get_contents($file); - if ($content === false) { - $this->output->writeln("Failed to read artifact config file: {$file}"); - return; - } - $data = json_decode($content, true); - if (!is_array($data)) { - $this->output->writeln("Invalid JSON format in artifact config file: {$file}"); - return; - } - ksort($data); - foreach ($data as $artifact_name => &$config) { - ksort($config); - } - unset($config); - $new_content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; - file_put_contents($file, $new_content); - $this->output->writeln("Sorted artifact config file: {$file}"); - } -} From 23db10d3cdfd95c5ab2262df20f94c291d4af70c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 15:35:59 +0800 Subject: [PATCH 164/682] Add libiconv,libssh2,libxml2,xz --- config/pkg/lib/libiconv.yml | 18 ++++++ config/pkg/lib/libssh2.yml | 22 +++++++ config/pkg/lib/libxml2.yml | 19 ++++++ config/pkg/lib/xz.yml | 20 +++++++ src/Package/Library/libiconv.php | 9 +-- src/Package/Library/libssh2.php | 29 +++++++++ src/Package/Library/libxml2.php | 69 ++++++++++++++-------- src/Package/Library/xz.php | 30 ++++++++++ src/StaticPHP/Command/BuildLibsCommand.php | 4 +- 9 files changed, 190 insertions(+), 30 deletions(-) create mode 100644 config/pkg/lib/libiconv.yml create mode 100644 config/pkg/lib/libssh2.yml create mode 100644 config/pkg/lib/libxml2.yml create mode 100644 config/pkg/lib/xz.yml create mode 100644 src/Package/Library/libssh2.php create mode 100644 src/Package/Library/xz.php diff --git a/config/pkg/lib/libiconv.yml b/config/pkg/lib/libiconv.yml new file mode 100644 index 000000000..7fc3bddad --- /dev/null +++ b/config/pkg/lib/libiconv.yml @@ -0,0 +1,18 @@ +libiconv: + type: library + artifact: + source: + type: filelist + url: 'https://ftp.gnu.org/gnu/libiconv/' + regex: '/href="(?libiconv-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: [COPYING.LIB] + license: LGPL-2.0-or-later + headers: + - iconv.h + - libcharset.h + - localcharset.h + static-libs@unix: + - libiconv.a + - libcharset.a diff --git a/config/pkg/lib/libssh2.yml b/config/pkg/lib/libssh2.yml new file mode 100644 index 000000000..4b66ed2b7 --- /dev/null +++ b/config/pkg/lib/libssh2.yml @@ -0,0 +1,22 @@ +libssh2: + type: library + artifact: + source: + type: ghrel + repo: libssh2/libssh2 + match: libssh2.+\.tar\.gz + prefer-stable: true + binary: hosted + metadata: + license-files: [COPYING] + license: BSD-3-Clause + depends: + - openssl + headers: + - libssh2.h + - libssh2_publickey.h + - libssh2_sftp.h + static-libs@unix: + - libssh2.a + pkg-configs: + - libssh2 diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml new file mode 100644 index 000000000..4d628ffb0 --- /dev/null +++ b/config/pkg/lib/libxml2.yml @@ -0,0 +1,19 @@ +libxml2: + type: library + artifact: + source: + type: ghtagtar + repo: GNOME/libxml2 + match: 'v2\.\d+\.\d+$' + metadata: + license-files: [Copyright] + license: MIT + depends@unix: + - libiconv + suggests@unix: + - xz + - zlib + headers: + - libxml2 + pkg-configs: + - libxml-2.0 diff --git a/config/pkg/lib/xz.yml b/config/pkg/lib/xz.yml new file mode 100644 index 000000000..a64cf2e9a --- /dev/null +++ b/config/pkg/lib/xz.yml @@ -0,0 +1,20 @@ +xz: + type: library + artifact: + source: + type: ghrel + repo: tukaani-project/xz + match: xz.+\.tar\.xz + prefer-stable: true + binary: hosted + metadata: + license-files: [COPYING] + license: 0BSD + depends@unix: + - libiconv + headers@unix: + - lzma + static-libs@unix: + - liblzma.a + pkg-configs: + - liblzma diff --git a/src/Package/Library/libiconv.php b/src/Package/Library/libiconv.php index ac91d188a..d2ac4d26c 100644 --- a/src/Package/Library/libiconv.php +++ b/src/Package/Library/libiconv.php @@ -12,16 +12,17 @@ #[Library('libiconv')] class libiconv { + #[BuildFor('Linux')] #[BuildFor('Darwin')] - public function build(LibraryPackage $package): void + public function build(LibraryPackage $lib): void { - UnixAutoconfExecutor::create($package) + UnixAutoconfExecutor::create($lib) ->configure( '--enable-extra-encodings', '--enable-year2038', ) ->make('install-lib', with_install: false) - ->make('install-lib', with_install: false, dir: "{$package->getSourceDir()}/libcharset"); - $package->patchLaDependencyPrefix(); + ->make('install-lib', with_install: false, dir: $lib->getSourceDir() . '/libcharset'); + $lib->patchLaDependencyPrefix(); } } diff --git a/src/Package/Library/libssh2.php b/src/Package/Library/libssh2.php new file mode 100644 index 000000000..f71d508ac --- /dev/null +++ b/src/Package/Library/libssh2.php @@ -0,0 +1,29 @@ +optionalPackage('zlib', ...cmake_boolean_args('ENABLE_ZLIB_COMPRESSION')) + ->addConfigureArgs( + '-DBUILD_EXAMPLES=OFF', + '-DBUILD_TESTING=OFF' + ) + ->build(); + + $lib->patchPkgconfPrefix(['libssh2.pc']); + } +} diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php index 4767efcb0..7c35d6855 100644 --- a/src/Package/Library/libxml2.php +++ b/src/Package/Library/libxml2.php @@ -8,47 +8,68 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; -use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; #[Library('libxml2')] class libxml2 { - #[BuildFor('Darwin')] - public function build(LibraryPackage $package): void + #[BuildFor('Linux')] + public function buildForLinux(LibraryPackage $lib): void { - $cmake = UnixCMakeExecutor::create($package) + UnixCMakeExecutor::create($lib) ->optionalPackage( 'zlib', '-DLIBXML2_WITH_ZLIB=ON ' . - "-DZLIB_LIBRARY={$package->getLibDir()}/libz.a " . - "-DZLIB_INCLUDE_DIR={$package->getIncludeDir()}", + "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a " . + "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", '-DLIBXML2_WITH_ZLIB=OFF', ) ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) ->addConfigureArgs( '-DLIBXML2_WITH_ICONV=ON', + '-DIconv_IS_BUILT_IN=OFF', '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md '-DLIBXML2_WITH_PYTHON=OFF', '-DLIBXML2_WITH_PROGRAMS=OFF', '-DLIBXML2_WITH_TESTS=OFF', - ); - - if (SystemTarget::getTargetOS() === 'Linux') { - $cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF'); - } - - $cmake->build(); - - FileSystem::replaceFileStr( - BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', - '-lxml2 -liconv', - '-lxml2' - ); - FileSystem::replaceFileStr( - BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', - '-lxml2', - '-lxml2 -liconv' - ); + ) + ->build(); + + $this->patchPkgConfig($lib); + } + + #[BuildFor('Darwin')] + public function buildForDarwin(LibraryPackage $lib): void + { + UnixCMakeExecutor::create($lib) + ->optionalPackage( + 'zlib', + '-DLIBXML2_WITH_ZLIB=ON ' . + "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a " . + "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", + '-DLIBXML2_WITH_ZLIB=OFF', + ) + ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) + ->addConfigureArgs( + '-DLIBXML2_WITH_ICONV=ON', + '-DLIBXML2_WITH_ICU=OFF', + '-DLIBXML2_WITH_PYTHON=OFF', + '-DLIBXML2_WITH_PROGRAMS=OFF', + '-DLIBXML2_WITH_TESTS=OFF', + ) + ->build(); + + $this->patchPkgConfig($lib); + } + + private function patchPkgConfig(LibraryPackage $lib): void + { + $pcFile = "{$lib->getLibDir()}/pkgconfig/libxml-2.0.pc"; + + // Remove -liconv from original + FileSystem::replaceFileStr($pcFile, '-lxml2 -liconv', '-lxml2'); + + // Add -liconv after -lxml2 + FileSystem::replaceFileStr($pcFile, '-lxml2', '-lxml2 -liconv'); } } diff --git a/src/Package/Library/xz.php b/src/Package/Library/xz.php new file mode 100644 index 000000000..3486d4c17 --- /dev/null +++ b/src/Package/Library/xz.php @@ -0,0 +1,30 @@ +configure( + '--disable-scripts', + '--disable-doc', + '--with-libiconv', + '--bindir=/tmp/xz', // xz binary will corrupt `tar` command, that's really strange. + ) + ->make(); + $lib->patchPkgconfPrefix(['liblzma.pc']); + $lib->patchLaDependencyPrefix(); + } +} diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index 2ff36b49a..309867c0c 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -7,10 +7,10 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; -#[AsCommand('build:libs')] +#[AsCommand('build:libs', 'Build specified library packages')] class BuildLibsCommand extends BaseCommand { - public function configure() + public function configure(): void { $this->addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated'); } From 1586825b5bd00c0e12176599851931c715a8c7b8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 15:55:41 +0800 Subject: [PATCH 165/682] Add builder options for build:libs command --- src/StaticPHP/Command/BuildLibsCommand.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index 309867c0c..a415ca3e1 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -4,8 +4,11 @@ namespace StaticPHP\Command; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; #[AsCommand('build:libs', 'Build specified library packages')] class BuildLibsCommand extends BaseCommand @@ -13,13 +16,20 @@ class BuildLibsCommand extends BaseCommand public function configure(): void { $this->addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated'); + // Builder options + $this->getDefinition()->addOptions([ + new InputOption('with-suggests', ['L', 'E'], null, 'Resolve and install suggested packages as well'), + new InputOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional packages to install/build, comma separated', ''), + new InputOption('no-download', null, null, 'Skip downloading artifacts (use existing cached files)'), + ...V2CompatLayer::getLegacyBuildOptions(), + ]); } public function handle(): int { $libs = parse_comma_list($this->input->getArgument('libraries')); - $installer = new \StaticPHP\Package\PackageInstaller($this->input->getOptions()); + $installer = new PackageInstaller($this->input->getOptions()); foreach ($libs as $lib) { $installer->addBuildPackage($lib); } From 82ab14165e966303565d5fa9f2a47fe658fe4b68 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 15:56:12 +0800 Subject: [PATCH 166/682] Add nghttp2, nghttp3, ngtcp2 --- config/pkg/lib/nghttp2.yml | 24 +++++++++++++++ config/pkg/lib/nghttp3.yml | 19 ++++++++++++ config/pkg/lib/ngtcp2.yml | 24 +++++++++++++++ src/Package/Library/nghttp2.php | 41 ++++++++++++++++++++++++++ src/Package/Library/nghttp3.php | 25 ++++++++++++++++ src/Package/Library/ngtcp2.php | 52 +++++++++++++++++++++++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 config/pkg/lib/nghttp2.yml create mode 100644 config/pkg/lib/nghttp3.yml create mode 100644 config/pkg/lib/ngtcp2.yml create mode 100644 src/Package/Library/nghttp2.php create mode 100644 src/Package/Library/nghttp3.php create mode 100644 src/Package/Library/ngtcp2.php diff --git a/config/pkg/lib/nghttp2.yml b/config/pkg/lib/nghttp2.yml new file mode 100644 index 000000000..9678d16b2 --- /dev/null +++ b/config/pkg/lib/nghttp2.yml @@ -0,0 +1,24 @@ +nghttp2: + type: library + artifact: + source: + type: ghrel + repo: nghttp2/nghttp2 + match: nghttp2.+\.tar\.xz + prefer-stable: true + metadata: + license-files: [COPYING] + depends: + - zlib + - openssl + suggests: + - libxml2 + - nghttp3 + - ngtcp2 + - brotli + headers: + - nghttp2 + static-libs@unix: + - libnghttp2.a + pkg-configs: + - libnghttp2 diff --git a/config/pkg/lib/nghttp3.yml b/config/pkg/lib/nghttp3.yml new file mode 100644 index 000000000..7d15c6020 --- /dev/null +++ b/config/pkg/lib/nghttp3.yml @@ -0,0 +1,19 @@ +nghttp3: + type: library + artifact: + source: + type: ghrel + repo: ngtcp2/nghttp3 + match: nghttp3.+\.tar\.xz + prefer-stable: true + metadata: + license-files: [COPYING] + license: MIT + depends: + - openssl + headers: + - nghttp3 + static-libs@unix: + - libnghttp3.a + pkg-configs: + - libnghttp3 diff --git a/config/pkg/lib/ngtcp2.yml b/config/pkg/lib/ngtcp2.yml new file mode 100644 index 000000000..44b0aaaeb --- /dev/null +++ b/config/pkg/lib/ngtcp2.yml @@ -0,0 +1,24 @@ +ngtcp2: + type: library + artifact: + source: + type: ghrel + repo: ngtcp2/ngtcp2 + match: ngtcp2.+\.tar\.xz + prefer-stable: true + metadata: + license-files: [COPYING] + license: MIT + depends: + - openssl + suggests: + - nghttp3 + - brotli + headers: + - ngtcp2 + static-libs@unix: + - libngtcp2.a + - libngtcp2_crypto_ossl.a + pkg-configs: + - libngtcp2 + - libngtcp2_crypto_ossl diff --git a/src/Package/Library/nghttp2.php b/src/Package/Library/nghttp2.php new file mode 100644 index 000000000..3a85ada4a --- /dev/null +++ b/src/Package/Library/nghttp2.php @@ -0,0 +1,41 @@ +optionalPackage('zlib', ...ac_with_args('zlib', true)) + ->optionalPackage('openssl', ...ac_with_args('openssl', true)) + ->optionalPackage('libxml2', ...ac_with_args('libxml2', true)) + ->optionalPackage('ngtcp2', ...ac_with_args('libngtcp2', true)) + ->optionalPackage('nghttp3', ...ac_with_args('libnghttp3', true)) + ->optionalPackage( + 'brotli', + fn (LibraryPackage $brotli) => implode(' ', [ + '--with-brotlidec=yes', + "LIBBROTLIDEC_CFLAGS=\"-I{$brotli->getIncludeDir()}\"", + "LIBBROTLIDEC_LIBS=\"{$brotli->getStaticLibFiles()}\"", + '--with-libbrotlienc=yes', + "LIBBROTLIENC_CFLAGS=\"-I{$brotli->getIncludeDir()}\"", + "LIBBROTLIENC_LIBS=\"{$brotli->getStaticLibFiles()}\"", + ]) + ) + ->configure('--enable-lib-only') + ->make(); + + $lib->patchPkgconfPrefix(['libnghttp2.pc'], PKGCONF_PATCH_PREFIX); + } +} diff --git a/src/Package/Library/nghttp3.php b/src/Package/Library/nghttp3.php new file mode 100644 index 000000000..1f686b7b5 --- /dev/null +++ b/src/Package/Library/nghttp3.php @@ -0,0 +1,25 @@ +configure('--enable-lib-only') + ->make(); + + $lib->patchPkgconfPrefix(['libnghttp3.pc'], PKGCONF_PATCH_PREFIX); + } +} diff --git a/src/Package/Library/ngtcp2.php b/src/Package/Library/ngtcp2.php new file mode 100644 index 000000000..15821225b --- /dev/null +++ b/src/Package/Library/ngtcp2.php @@ -0,0 +1,52 @@ +optionalPackage( + 'openssl', + fn (LibraryPackage $openssl) => implode(' ', [ + '--with-openssl=yes', + "OPENSSL_LIBS=\"{$openssl->getStaticLibFiles()}\"", + "OPENSSL_CFLAGS=\"-I{$openssl->getIncludeDir()}\"", + ]), + '--with-openssl=no' + ) + ->optionalPackage('nghttp3', ...ac_with_args('libnghttp3', true)) + ->optionalPackage( + 'brotli', + fn (LibraryPackage $brotli) => implode(' ', [ + '--with-brotlidec=yes', + "LIBBROTLIDEC_CFLAGS=\"-I{$brotli->getIncludeDir()}\"", + "LIBBROTLIDEC_LIBS=\"{$brotli->getStaticLibFiles()}\"", + '--with-libbrotlienc=yes', + "LIBBROTLIENC_CFLAGS=\"-I{$brotli->getIncludeDir()}\"", + "LIBBROTLIENC_LIBS=\"{$brotli->getStaticLibFiles()}\"", + ]) + ) + ->appendEnv(['PKG_CONFIG' => '$PKG_CONFIG --static']) + ->configure('--enable-lib-only') + ->make(); + + $lib->patchPkgconfPrefix(['libngtcp2.pc', 'libngtcp2_crypto_ossl.pc'], PKGCONF_PATCH_PREFIX); + + // On macOS, the static library may contain other static libraries + // ld: archive member 'libssl.a' not a mach-o file in libngtcp2_crypto_ossl.a + $AR = getenv('AR') ?: 'ar'; + shell()->cd($lib->getLibDir())->exec("{$AR} -t libngtcp2_crypto_ossl.a | grep '\\.a\$' | xargs -n1 {$AR} d libngtcp2_crypto_ossl.a"); + } +} From 3d102363c42761aaa9667553dc5a1385e623f2a8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 16:15:25 +0800 Subject: [PATCH 167/682] Add PatchBeforeBuild attribute --- .../Attribute/Package/PatchBeforeBuild.php | 8 +++++++ .../Package/PackageCallbacksTrait.php | 22 ++++++++++--------- src/StaticPHP/Registry/PackageLoader.php | 8 +++++++ 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 src/StaticPHP/Attribute/Package/PatchBeforeBuild.php diff --git a/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php b/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php new file mode 100644 index 000000000..254e1a6b6 --- /dev/null +++ b/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php @@ -0,0 +1,8 @@ +validate_callback = $callback; } - public function setPatchBeforeBuildCallback(callable $callback): void + public function addPatchBeforeBuildCallback(callable $callback): void { - $this->patch_before_build_callback = $callback; + $this->patch_before_build_callbacks[] = $callback; } public function patchBeforeBuild(): void @@ -58,16 +58,18 @@ public function patchBeforeBuild(): void if (file_exists("{$this->getSourceDir()}/.spc-patched")) { return; } - if ($this->patch_before_build_callback === null) { + if ($this->patch_before_build_callbacks === null) { return; } // Use CallbackInvoker with current package as context - $ret = ApplicationContext::invoke($this->patch_before_build_callback, [ - Package::class => $this, - static::class => $this, - ]); - if ($ret === true) { - FileSystem::writeFile("{$this->getSourceDir()}/.spc-patched", 'PATCHED!!!'); + foreach ($this->patch_before_build_callbacks as $callback) { + $ret = ApplicationContext::invoke($callback, [ + Package::class => $this, + static::class => $this, + ]); + if ($ret === true) { + FileSystem::writeFile("{$this->getSourceDir()}/.spc-patched", 'PATCHED!!!'); + } } } diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 29ce0a96b..e95021273 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -12,6 +12,7 @@ use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\Package\ResolveBuild; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; @@ -196,6 +197,8 @@ public static function loadFromClass(mixed $class): void match ($method_attribute->getName()) { // #[BuildFor(PHP_OS_FAMILY)] BuildFor::class => self::addBuildFunction($pkg, $method_instance, [$instance_class, $method->getName()]), + // #[BeforeBuild] + PatchBeforeBuild::class => self::addPatchBeforeBuildFunction($pkg, [$instance_class, $method->getName()]), // #[CustomPhpConfigureArg(PHP_OS_FAMILY)] CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]), // #[Stage('stage_name')] @@ -332,6 +335,11 @@ private static function addBuildFunction(Package $pkg, object $attr, callable $f $pkg->addBuildFunction($attr->os, $fn); } + private static function addPatchBeforeBuildFunction(Package $pkg, callable $fn): void + { + $pkg->addPatchBeforeBuildCallback($fn); + } + private static function addStage(\ReflectionMethod $method, Package $pkg, object $instance_class, object $method_instance): void { $name = $method_instance->function; From 6ee8dc799497485977ef6a3023b42850b24bb342 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 16:15:36 +0800 Subject: [PATCH 168/682] Add zstd,libcares --- config/pkg/lib/libcares.yml | 23 +++++++++++++++++++ config/pkg/lib/zstd.yml | 19 ++++++++++++++++ src/Package/Library/libcares.php | 38 ++++++++++++++++++++++++++++++++ src/Package/Library/zstd.php | 29 ++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 config/pkg/lib/libcares.yml create mode 100644 config/pkg/lib/zstd.yml create mode 100644 src/Package/Library/libcares.php create mode 100644 src/Package/Library/zstd.php diff --git a/config/pkg/lib/libcares.yml b/config/pkg/lib/libcares.yml new file mode 100644 index 000000000..da0d1ad64 --- /dev/null +++ b/config/pkg/lib/libcares.yml @@ -0,0 +1,23 @@ +libcares: + type: library + artifact: + source: + type: ghrel + repo: c-ares/c-ares + match: c-ares-.+\.tar\.gz + prefer-stable: true + source-mirror: + type: filelist + url: 'https://c-ares.org/download/' + regex: '/href="\/download\/(?c-ares-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: [LICENSE.md] + headers@unix: + - ares.h + - ares_dns.h + - ares_nameser.h + static-libs@unix: + - libcares.a + pkg-configs: + - libcares diff --git a/config/pkg/lib/zstd.yml b/config/pkg/lib/zstd.yml new file mode 100644 index 000000000..60d335197 --- /dev/null +++ b/config/pkg/lib/zstd.yml @@ -0,0 +1,19 @@ +zstd: + type: library + artifact: + source: + type: ghrel + repo: facebook/zstd + match: zstd.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + headers@unix: + - zdict.h + - zstd.h + - zstd_errors.h + static-libs@unix: + - libzstd.a + pkg-configs: + - libzstd diff --git a/src/Package/Library/libcares.php b/src/Package/Library/libcares.php new file mode 100644 index 000000000..fbfb58303 --- /dev/null +++ b/src/Package/Library/libcares.php @@ -0,0 +1,38 @@ +getSourceDir()}/src/lib/thirdparty/apple/dnsinfo.h")) { + FileSystem::createDir("{$lib->getSourceDir()}/src/lib/thirdparty/apple"); + copy(ROOT_DIR . '/src/globals/extra/libcares_dnsinfo.h', "{$lib->getSourceDir()}/src/lib/thirdparty/apple/dnsinfo.h"); + return true; + } + return false; + } + + #[BuildFor('Linux')] + #[BuildFor('Darwin')] + public function build(LibraryPackage $lib): void + { + UnixAutoconfExecutor::create($lib)->configure('--disable-tests')->make(); + + $lib->patchPkgconfPrefix(['libcares.pc'], PKGCONF_PATCH_PREFIX); + } +} diff --git a/src/Package/Library/zstd.php b/src/Package/Library/zstd.php new file mode 100644 index 000000000..ab538358e --- /dev/null +++ b/src/Package/Library/zstd.php @@ -0,0 +1,29 @@ +setBuildDir("{$lib->getSourceDir()}/build/cmake/build") + ->addConfigureArgs( + '-DZSTD_BUILD_STATIC=ON', + '-DZSTD_BUILD_SHARED=OFF', + ) + ->build(); + + $lib->patchPkgconfPrefix(['libzstd.pc'], PKGCONF_PATCH_PREFIX); + } +} From 19e11caa830b00b6525fb69a4e2144dc5258cfc0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 16:53:04 +0800 Subject: [PATCH 169/682] Add ldap,libcares,libsodium,libunistring, lint all configs --- config/pkg/lib/ldap.yml | 17 ++++++++ config/pkg/lib/libcares.yml | 4 +- config/pkg/lib/libsodium.yml | 15 +++++++ config/pkg/lib/libssh2.yml | 4 +- config/pkg/lib/libunistring.yml | 16 ++++++++ config/pkg/lib/libxml2.yml | 8 ++-- config/pkg/lib/nghttp2.yml | 12 +++--- config/pkg/lib/nghttp3.yml | 4 +- config/pkg/lib/ngtcp2.yml | 12 +++--- config/pkg/lib/xz.yml | 4 +- config/pkg/lib/zstd.yml | 4 +- src/Package/Library/ldap.php | 59 ++++++++++++++++++++++++++++ src/Package/Library/libsodium.php | 24 +++++++++++ src/Package/Library/libunistring.php | 25 ++++++++++++ 14 files changed, 182 insertions(+), 26 deletions(-) create mode 100644 config/pkg/lib/ldap.yml create mode 100644 config/pkg/lib/libsodium.yml create mode 100644 config/pkg/lib/libunistring.yml create mode 100644 src/Package/Library/ldap.php create mode 100644 src/Package/Library/libsodium.php create mode 100644 src/Package/Library/libunistring.php diff --git a/config/pkg/lib/ldap.yml b/config/pkg/lib/ldap.yml new file mode 100644 index 000000000..d1e7db2ae --- /dev/null +++ b/config/pkg/lib/ldap.yml @@ -0,0 +1,17 @@ +ldap: + type: library + artifact: + source: + type: filelist + url: 'https://www.openldap.org/software/download/OpenLDAP/openldap-release/' + regex: '/href="(?openldap-(?[^"]+)\.tgz)"/' + metadata: + license-files: [LICENSE] + depends: + - openssl + - zlib + - gmp + - libsodium + pkg-configs: + - ldap + - lber diff --git a/config/pkg/lib/libcares.yml b/config/pkg/lib/libcares.yml index da0d1ad64..5d3f8c9d1 100644 --- a/config/pkg/lib/libcares.yml +++ b/config/pkg/lib/libcares.yml @@ -17,7 +17,7 @@ libcares: - ares.h - ares_dns.h - ares_nameser.h - static-libs@unix: - - libcares.a pkg-configs: - libcares + static-libs@unix: + - libcares.a diff --git a/config/pkg/lib/libsodium.yml b/config/pkg/lib/libsodium.yml new file mode 100644 index 000000000..f5a551b93 --- /dev/null +++ b/config/pkg/lib/libsodium.yml @@ -0,0 +1,15 @@ +libsodium: + type: library + artifact: + source: + type: ghrel + repo: jedisct1/libsodium + match: 'libsodium-(?!1\.0\.21)\d+(\.\d+)*\.tar\.gz' + prefer-stable: true + binary: hosted + metadata: + license-files: [LICENSE] + pkg-configs: + - libsodium + static-libs@unix: + - libsodium.a diff --git a/config/pkg/lib/libssh2.yml b/config/pkg/lib/libssh2.yml index 4b66ed2b7..8e1d82754 100644 --- a/config/pkg/lib/libssh2.yml +++ b/config/pkg/lib/libssh2.yml @@ -16,7 +16,7 @@ libssh2: - libssh2.h - libssh2_publickey.h - libssh2_sftp.h - static-libs@unix: - - libssh2.a pkg-configs: - libssh2 + static-libs@unix: + - libssh2.a diff --git a/config/pkg/lib/libunistring.yml b/config/pkg/lib/libunistring.yml new file mode 100644 index 000000000..2b2ffd334 --- /dev/null +++ b/config/pkg/lib/libunistring.yml @@ -0,0 +1,16 @@ +libunistring: + type: library + artifact: + source: + type: filelist + url: 'https://ftp.gnu.org/gnu/libunistring/' + regex: '/href="(?libunistring-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: [COPYING.LIB] + license: LGPL-3.0-or-later + headers: + - unistr.h + - unistring/ + static-libs@unix: + - libunistring.a diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index 4d628ffb0..8ba0f8e18 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -4,16 +4,16 @@ libxml2: source: type: ghtagtar repo: GNOME/libxml2 - match: 'v2\.\d+\.\d+$' + match: v2\.\d+\.\d+$ metadata: license-files: [Copyright] license: MIT depends@unix: - libiconv - suggests@unix: - - xz - - zlib headers: - libxml2 pkg-configs: - libxml-2.0 + suggests@unix: + - xz + - zlib diff --git a/config/pkg/lib/nghttp2.yml b/config/pkg/lib/nghttp2.yml index 9678d16b2..20a1840db 100644 --- a/config/pkg/lib/nghttp2.yml +++ b/config/pkg/lib/nghttp2.yml @@ -11,14 +11,14 @@ nghttp2: depends: - zlib - openssl + headers: + - nghttp2 + pkg-configs: + - libnghttp2 + static-libs@unix: + - libnghttp2.a suggests: - libxml2 - nghttp3 - ngtcp2 - brotli - headers: - - nghttp2 - static-libs@unix: - - libnghttp2.a - pkg-configs: - - libnghttp2 diff --git a/config/pkg/lib/nghttp3.yml b/config/pkg/lib/nghttp3.yml index 7d15c6020..f9adc05b5 100644 --- a/config/pkg/lib/nghttp3.yml +++ b/config/pkg/lib/nghttp3.yml @@ -13,7 +13,7 @@ nghttp3: - openssl headers: - nghttp3 - static-libs@unix: - - libnghttp3.a pkg-configs: - libnghttp3 + static-libs@unix: + - libnghttp3.a diff --git a/config/pkg/lib/ngtcp2.yml b/config/pkg/lib/ngtcp2.yml index 44b0aaaeb..a609d3ca5 100644 --- a/config/pkg/lib/ngtcp2.yml +++ b/config/pkg/lib/ngtcp2.yml @@ -11,14 +11,14 @@ ngtcp2: license: MIT depends: - openssl - suggests: - - nghttp3 - - brotli headers: - ngtcp2 - static-libs@unix: - - libngtcp2.a - - libngtcp2_crypto_ossl.a pkg-configs: - libngtcp2 - libngtcp2_crypto_ossl + static-libs@unix: + - libngtcp2.a + - libngtcp2_crypto_ossl.a + suggests: + - nghttp3 + - brotli diff --git a/config/pkg/lib/xz.yml b/config/pkg/lib/xz.yml index a64cf2e9a..7d0af682b 100644 --- a/config/pkg/lib/xz.yml +++ b/config/pkg/lib/xz.yml @@ -14,7 +14,7 @@ xz: - libiconv headers@unix: - lzma - static-libs@unix: - - liblzma.a pkg-configs: - liblzma + static-libs@unix: + - liblzma.a diff --git a/config/pkg/lib/zstd.yml b/config/pkg/lib/zstd.yml index 60d335197..3d76e270c 100644 --- a/config/pkg/lib/zstd.yml +++ b/config/pkg/lib/zstd.yml @@ -13,7 +13,7 @@ zstd: - zdict.h - zstd.h - zstd_errors.h - static-libs@unix: - - libzstd.a pkg-configs: - libzstd + static-libs@unix: + - libzstd.a diff --git a/src/Package/Library/ldap.php b/src/Package/Library/ldap.php new file mode 100644 index 000000000..21e7f8c04 --- /dev/null +++ b/src/Package/Library/ldap.php @@ -0,0 +1,59 @@ +getSourceDir() . '/configure', '"-lssl -lcrypto', '"-lssl -lcrypto -lz ' . $extra); + return true; + } + + #[BuildFor('Linux')] + #[BuildFor('Darwin')] + public function build(LibraryPackage $lib): void + { + UnixAutoconfExecutor::create($lib) + ->optionalPackage('openssl', '--with-tls=openssl') + ->optionalPackage('gmp', '--with-mp=gmp') + ->optionalPackage('libsodium', '--with-argon2=libsodium', '--enable-argon2=no') + ->addConfigureArgs( + '--disable-slapd', + '--without-systemd', + '--without-cyrus-sasl', + 'ac_cv_func_pthread_kill_other_threads_np=no' + ) + ->appendEnv([ + 'CFLAGS' => '-Wno-date-time', + 'LDFLAGS' => "-L{$lib->getLibDir()}", + 'CPPFLAGS' => "-I{$lib->getIncludeDir()}", + ]) + ->configure() + ->exec('sed -i -e "s/SUBDIRS= include libraries clients servers tests doc/SUBDIRS= include libraries clients servers/g" Makefile') + ->make(); + + FileSystem::replaceFileLineContainsString( + $lib->getLibDir() . '/pkgconfig/ldap.pc', + 'Libs: -L${libdir} -lldap', + 'Libs: -L${libdir} -lldap -llber' + ); + $lib->patchPkgconfPrefix(['ldap.pc', 'lber.pc']); + $lib->patchLaDependencyPrefix(); + } +} diff --git a/src/Package/Library/libsodium.php b/src/Package/Library/libsodium.php new file mode 100644 index 000000000..50d706ba4 --- /dev/null +++ b/src/Package/Library/libsodium.php @@ -0,0 +1,24 @@ +configure()->make(); + + // Patch pkg-config file + $lib->patchPkgconfPrefix(['libsodium.pc'], PKGCONF_PATCH_PREFIX); + } +} diff --git a/src/Package/Library/libunistring.php b/src/Package/Library/libunistring.php new file mode 100644 index 000000000..5f5575dc2 --- /dev/null +++ b/src/Package/Library/libunistring.php @@ -0,0 +1,25 @@ +configure('--disable-nls') + ->make(); + + $lib->patchLaDependencyPrefix(); + } +} From a6c79e30a8d9b53e2a56d6e19695066cd96b05f8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 2 Feb 2026 16:53:28 +0800 Subject: [PATCH 170/682] Add dump license files after installing --- src/StaticPHP/Package/PackageInstaller.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index c4262d9a3..9ceb21078 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -17,6 +17,7 @@ use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; +use StaticPHP\Util\LicenseDumper; use StaticPHP\Util\V2CompatLayer; use ZM\Logger\ConsoleColor; @@ -208,6 +209,11 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): } } } + + $this->dumpLicenseFiles($this->packages); + if ($interactive) { + InteractiveTerm::success('Exported package licenses', true); + } } public function isBuildPackage(Package|string $package): bool @@ -460,6 +466,21 @@ public function getPhpExtensionPackage(string $package_or_ext_name): ?PhpExtensi return null; } + /** + * @param Package[] $packages + */ + private function dumpLicenseFiles(array $packages): void + { + $dumper = new LicenseDumper(); + foreach ($packages as $package) { + $artifact = $package->getArtifact(); + if ($artifact !== null) { + $dumper->addArtifacts([$artifact->getName()]); + } + } + $dumper->dump(BUILD_ROOT_PATH . '/license'); + } + /** * Validate that a package has required artifacts. */ From 3492992b562eac4c9dcf5703496c0defa2be873e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 09:42:53 +0800 Subject: [PATCH 171/682] Add ncurses --- config/pkg/lib/ncurses.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/lib/ncurses.yml diff --git a/config/pkg/lib/ncurses.yml b/config/pkg/lib/ncurses.yml new file mode 100644 index 000000000..be50a6c7f --- /dev/null +++ b/config/pkg/lib/ncurses.yml @@ -0,0 +1,12 @@ +ncurses: + type: library + artifact: + source: + type: filelist + url: 'https://ftp.gnu.org/pub/gnu/ncurses/' + regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: ['COPYING'] + static-libs@unix: + - libncurses.a From fddcdb87962c7787e4ae2415cf89022fac1247ab Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:05:11 +0800 Subject: [PATCH 172/682] Add filelist downloader debug message --- src/StaticPHP/Artifact/Downloader/Type/FileList.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticPHP/Artifact/Downloader/Type/FileList.php b/src/StaticPHP/Artifact/Downloader/Type/FileList.php index 8290a1cc3..314f0c330 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/FileList.php +++ b/src/StaticPHP/Artifact/Downloader/Type/FileList.php @@ -20,6 +20,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo throw new DownloaderException("Failed to get {$name} file list from {$config['url']}"); } $versions = []; + logger()->debug('Matched ' . count($matches['version']) . " versions for {$name}"); foreach ($matches['version'] as $i => $version) { $lower = strtolower($version); foreach (['alpha', 'beta', 'rc', 'pre', 'nightly', 'snapshot', 'dev'] as $beta) { From e732543bd731dce1afa62a1d1c8184846246d2f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:05:28 +0800 Subject: [PATCH 173/682] Fix wrong debug message show --- src/StaticPHP/Artifact/ArtifactDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b53ddd8a2..b4d79b769 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -438,7 +438,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, break; } } - $vvv = ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, and disable parallel downloading for more details." : ''; + $vvv = !ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, or disable parallel downloading for more details." : ''; throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}"); } From e4d6723b01f2b33f19282f35a8bc2b114d55a634 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:05:39 +0800 Subject: [PATCH 174/682] Add gettext --- config/pkg/lib/gettext.yml | 19 +++++++++++++ src/Package/Library/gettext.php | 48 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 config/pkg/lib/gettext.yml create mode 100644 src/Package/Library/gettext.php diff --git a/config/pkg/lib/gettext.yml b/config/pkg/lib/gettext.yml new file mode 100644 index 000000000..4be2e7d72 --- /dev/null +++ b/config/pkg/lib/gettext.yml @@ -0,0 +1,19 @@ +gettext: + type: library + artifact: + source: + type: filelist + url: 'https://ftp.gnu.org/pub/gnu/gettext/' + regex: '/href="(?gettext-(?[^"]+)\.tar\.xz)"/' + metadata: + license-files: [gettext-runtime/intl/COPYING.LIB] + license: LGPL-2.1-or-later + static-libs@unix: + - libintl.a + depends: + - libiconv + suggests: + - ncurses + - libxml2 + frameworks: + - CoreFoundation diff --git a/src/Package/Library/gettext.php b/src/Package/Library/gettext.php new file mode 100644 index 000000000..7af69905e --- /dev/null +++ b/src/Package/Library/gettext.php @@ -0,0 +1,48 @@ +optionalPackage('ncurses', "--with-libncurses-prefix={$pkg->getBuildRootPath()}") + ->optionalPackage('libxml2', "--with-libxml2-prefix={$pkg->getBuildRootPath()}") + ->addConfigureArgs( + '--disable-java', + '--disable-c++', + '--disable-d', + '--disable-rpath', + '--disable-modula2', + '--disable-libasprintf', + '--with-included-libintl', + "--with-iconv-prefix={$pkg->getBuildRootPath()}", + ); + + // zts + if ($builder->getOption('enable-zts')) { + $autoconf->addConfigureArgs('--enable-threads=isoc+posix') + ->appendEnv([ + 'CFLAGS' => '-lpthread -D_REENTRANT', + 'LDFLGAS' => '-lpthread', + ]); + } else { + $autoconf->addConfigureArgs('--disable-threads'); + } + + $autoconf->configure()->make(dir: "{$pkg->getSourceDir()}/gettext-runtime/intl"); + $pkg->patchLaDependencyPrefix(); + } +} From 2e8f6bbb310989088fc69a79cc92f5587e35758f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:10:10 +0800 Subject: [PATCH 175/682] Add idn2 --- config/pkg/lib/idn2.yml | 21 +++++++++++++++++++++ src/Package/Library/idn2.php | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 config/pkg/lib/idn2.yml create mode 100644 src/Package/Library/idn2.php diff --git a/config/pkg/lib/idn2.yml b/config/pkg/lib/idn2.yml new file mode 100644 index 000000000..c2970d3ec --- /dev/null +++ b/config/pkg/lib/idn2.yml @@ -0,0 +1,21 @@ +idn2: + type: library + artifact: + source: + type: filelist + url: 'https://ftp.gnu.org/gnu/libidn/' + regex: '/href="(?libidn2-(?[^"]+)\.tar\.gz)"/' + metadata: + license-files: ['COPYING.LESSERv3'] + license: LGPL-3.0-or-later + pkg-configs: + - libidn2 + headers: + - idn2.h + suggests@unix: + - libiconv + - gettext + - libunistring + depends@macos: + - libiconv + - gettext diff --git a/src/Package/Library/idn2.php b/src/Package/Library/idn2.php new file mode 100644 index 000000000..3cffd9be5 --- /dev/null +++ b/src/Package/Library/idn2.php @@ -0,0 +1,33 @@ +configure( + '--disable-nls', + '--disable-doc', + '--enable-year2038', + '--disable-rpath' + ) + ->optionalPackage('libiconv', '--with-libiconv-prefix=' . BUILD_ROOT_PATH) + ->optionalPackage('libunistring', '--with-libunistring-prefix=' . BUILD_ROOT_PATH) + ->optionalPackage('gettext', '--with-libnintl-prefix=' . BUILD_ROOT_PATH) + ->make(); + $lib->patchPkgconfPrefix(['libidn2.pc']); + $lib->patchLaDependencyPrefix(); + } +} From 668881960520109d636f851c91140e8586951d15 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:17:13 +0800 Subject: [PATCH 176/682] Add libedit --- config/pkg/lib/libedit.yml | 15 +++++++++++++++ src/Package/Library/libedit.php | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 config/pkg/lib/libedit.yml diff --git a/config/pkg/lib/libedit.yml b/config/pkg/lib/libedit.yml new file mode 100644 index 000000000..c782238bf --- /dev/null +++ b/config/pkg/lib/libedit.yml @@ -0,0 +1,15 @@ +libedit: + type: library + artifact: + source: + type: filelist + url: 'https://thrysoee.dk/editline/' + regex: '/href="(?libedit-(?[^"]+)\.tar\.gz)"/' + binary: hosted + metadata: + license-files: ['COPYING'] + license: BSD-3-Clause + static-libs@unix: + - libedit.a + depends: + - ncurses diff --git a/src/Package/Library/libedit.php b/src/Package/Library/libedit.php index 08a435da7..c06dbae3f 100644 --- a/src/Package/Library/libedit.php +++ b/src/Package/Library/libedit.php @@ -4,9 +4,9 @@ namespace Package\Library; -use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Util\FileSystem; @@ -14,7 +14,7 @@ #[Library('libedit')] class libedit extends LibraryPackage { - #[BeforeStage(stage: 'build')] + #[PatchBeforeBuild] public function patchBeforeBuild(): void { FileSystem::replaceFileRegex( From a2409d9c0fba0c45dedd5c27f9be7e531dd90a99 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:59:16 +0800 Subject: [PATCH 177/682] Add getSourceRoot for artifacts --- src/Package/Target/php/unix.php | 2 +- src/StaticPHP/Artifact/Artifact.php | 13 +++++++++++++ src/StaticPHP/Package/Package.php | 13 +++++++++++++ .../Runtime/Executor/UnixAutoconfExecutor.php | 8 ++++---- .../Runtime/Executor/UnixCMakeExecutor.php | 4 ++-- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 23d465dfb..1ce2ff95a 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -436,7 +436,7 @@ private function processLibphpSoFile(string $libphpSo, PackageInstaller $install */ private function makeVars(PackageInstaller $installer): array { - $config = (new SPCConfigUtil(['libs_only_deps' => true]))->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $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' : ''; diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index b5cf74c00..67876d900 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -268,6 +268,19 @@ public function getSourceDir(): string return FileSystem::convertPath(SOURCE_PATH . '/' . $path); } + /** + * Get source build root directory. + * It's only worked when 'source-root' is defined in artifact config. + * Normally it's equal to source dir. + */ + public function getSourceRoot(): string + { + if (isset($this->config['metadata']['source-root'])) { + return $this->getSourceDir() . '/' . ltrim($this->config['metadata']['source-root'], '/'); + } + return $this->getSourceDir(); + } + /** * Get binary extraction directory and mode. * diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index a0f415d13..cd4f38409 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -212,6 +212,19 @@ public function getSourceDir(): string throw new SPCInternalException("Source directory for package {$this->name} is not available because the source artifact is missing."); } + /** + * Get source build root directory. + * It's only worked when 'source-root' is defined in artifact config. + * Normally it's equal to source dir. + */ + public function getSourceRoot(): string + { + if (($artifact = $this->getArtifact()) && $artifact->hasSource()) { + return $artifact->getSourceRoot(); + } + throw new SPCInternalException("Source root for package {$this->name} is not available because the source artifact is missing."); + } + /** * Check if the package has a binary available for current OS and architecture. */ diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index e75a7ed06..41bc6e784 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -169,7 +169,7 @@ private function getDefaultConfigureArgs(): array */ private function initShell(): void { - $this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([ + $this->shell = shell()->cd($this->package->getSourceRoot())->initializeEnv($this->package)->appendEnv([ 'CFLAGS' => "-I{$this->package->getIncludeDir()}", 'CXXFLAGS' => "-I{$this->package->getIncludeDir()}", 'LDFLAGS' => "-L{$this->package->getLibDir()}", @@ -185,12 +185,12 @@ private function seekLogFileOnException(mixed $callable): static $callable(); return $this; } catch (SPCException $e) { - if (file_exists("{$this->package->getSourceDir()}/config.log")) { - logger()->debug("Config log file found: {$this->package->getSourceDir()}/config.log"); + if (file_exists("{$this->package->getSourceRoot()}/config.log")) { + logger()->debug("Config log file found: {$this->package->getSourceRoot()}/config.log"); $log_file = "lib.{$this->package->getName()}.console.log"; logger()->debug('Saved config log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); $e->addExtraLogFile("{$this->package->getName()} library config.log", $log_file); - copy("{$this->package->getSourceDir()}/config.log", SPC_LOGS_DIR . "/{$log_file}"); + copy("{$this->package->getSourceRoot()}/config.log", SPC_LOGS_DIR . "/{$log_file}"); } throw $e; } diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 107aac116..3d628b894 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -233,7 +233,7 @@ private function getDefaultCMakeArgs(): array private function initBuildDir(): void { if ($this->build_dir === null) { - $this->build_dir = "{$this->package->getSourceDir()}/build"; + $this->build_dir = "{$this->package->getSourceRoot()}/build"; } } @@ -295,7 +295,7 @@ private function makeCmakeToolchainFile(): string */ private function initShell(): void { - $this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([ + $this->shell = shell()->cd($this->package->getSourceRoot())->initializeEnv($this->package)->appendEnv([ 'CFLAGS' => "-I{$this->package->getIncludeDir()}", 'CXXFLAGS' => "-I{$this->package->getIncludeDir()}", 'LDFLAGS' => "-L{$this->package->getLibDir()}", From 09ddd2fdd8401db5b4f4b64d6175c2b5f3c3a414 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:59:37 +0800 Subject: [PATCH 178/682] Add methods to retrieve package sub-dependencies and configuration --- src/StaticPHP/Util/DependencyResolver.php | 85 +++++++++++++++++ src/StaticPHP/Util/SPCConfigUtil.php | 111 ++++++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index defb00ba2..b03f80892 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -55,6 +55,58 @@ public static function resolve(array $packages, array $dependency_overrides = [] return $resolved; } + /** + * Get all dependencies of a specific package within a resolved package set. + * This is useful when you need to get build flags for a specific library and its deps. + * + * The method will only include dependencies that exist in the resolved set, + * which properly handles optional dependencies (suggests) - only those that + * were actually resolved will be included. + * + * @param string $package_name The package to get dependencies for + * @param string[] $resolved_packages The resolved package list (from resolve()) + * @param bool $include_suggests Whether to include suggests that are in resolved set + * @return string[] Dependencies of the package (NOT including itself), ordered for building + */ + public static function getSubDependencies(string $package_name, array $resolved_packages, bool $include_suggests = false): array + { + // Create a lookup set for O(1) membership check + $resolved_set = array_flip($resolved_packages); + + // Verify the target package is in the resolved set + if (!isset($resolved_set[$package_name])) { + return []; + } + + // Build dependency map from config + $dep_map = []; + foreach ($resolved_packages as $pkg) { + $dep_map[$pkg] = [ + 'depends' => PackageConfig::get($pkg, 'depends', []), + 'suggests' => PackageConfig::get($pkg, 'suggests', []), + ]; + } + + // Collect all sub-dependencies recursively (excluding the package itself) + $visited = []; + $sorted = []; + + // Get dependencies to process for the target package + $deps = $dep_map[$package_name]['depends'] ?? []; + if ($include_suggests) { + $deps = array_merge($deps, $dep_map[$package_name]['suggests'] ?? []); + } + + // Only visit dependencies that are in the resolved set + foreach ($deps as $dep) { + if (isset($resolved_set[$dep])) { + self::visitSubDeps($dep, $dep_map, $resolved_set, $include_suggests, $visited, $sorted); + } + } + + return $sorted; + } + /** * Build a reverse dependency map for the resolved packages. * For each package that is depended upon, list which packages depend on it. @@ -89,6 +141,39 @@ private static function buildReverseDependencyMap(array $resolved, array $dep_li return $why; } + /** + * Recursive helper for getSubDependencies. + * Visits dependencies in topological order (dependencies first). + */ + private static function visitSubDeps( + string $pkg_name, + array $dep_map, + array $resolved_set, + bool $include_suggests, + array &$visited, + array &$sorted + ): void { + if (isset($visited[$pkg_name])) { + return; + } + $visited[$pkg_name] = true; + + // Get dependencies to process + $deps = $dep_map[$pkg_name]['depends'] ?? []; + if ($include_suggests) { + $deps = array_merge($deps, $dep_map[$pkg_name]['suggests'] ?? []); + } + + // Only visit dependencies that are in the resolved set + foreach ($deps as $dep) { + if (isset($resolved_set[$dep])) { + self::visitSubDeps($dep, $dep_map, $resolved_set, $include_suggests, $visited, $sorted); + } + } + + $sorted[] = $pkg_name; + } + /** * Visitor pattern implementation for dependency resolution. * diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 3a20e56fe..a31b771f8 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -149,6 +149,117 @@ public function getLibraryConfig(array|LibraryPackage $lib, bool $include_sugges return $ret; } + /** + * Get build configuration for a package and its sub-dependencies within a resolved set. + * + * This is useful when you need to statically link something against a specific + * library and all its transitive dependencies. It properly handles optional + * dependencies by only including those that were actually resolved. + * + * @param string $package_name The package to get config for + * @param string[] $resolved_packages The full resolved package list + * @param bool $include_suggests Whether to include resolved suggests + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array + { + // Get sub-dependencies within the resolved set + $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); + + if (empty($sub_deps)) { + return [ + 'cflags' => '', + 'ldflags' => '', + 'libs' => '', + ]; + } + + // Use libs_only_deps mode and no_php for library linking + $save_no_php = $this->no_php; + $save_libs_only_deps = $this->libs_only_deps; + $this->no_php = true; + $this->libs_only_deps = true; + + $ret = $this->configWithResolvedPackages($sub_deps); + + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + + return $ret; + } + + /** + * Get configuration using already-resolved packages (skip dependency resolution). + * + * @param string[] $resolved_packages Already resolved package names in build order + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function configWithResolvedPackages(array $resolved_packages): array + { + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved_packages); + $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved_packages)}"; + } + + // C++ + if ($this->hasCpp($resolved_packages)) { + $libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + $libs = "-lphp {$libs} -lc"; + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + private function hasCpp(array $packages): bool { foreach ($packages as $package) { From c536fedff7f2e447cc2200fb0234b5077640a9e7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 10:59:56 +0800 Subject: [PATCH 179/682] Add krb5 and lint configs --- config/pkg/lib/gettext.yml | 8 ++--- config/pkg/lib/idn2.yml | 12 +++---- config/pkg/lib/krb5.yml | 23 +++++++++++++ config/pkg/lib/libedit.yml | 6 ++-- config/pkg/lib/ncurses.yml | 2 +- config/pkg/lib/zlib.yml | 2 +- src/Package/Library/krb5.php | 63 ++++++++++++++++++++++++++++++++++++ 7 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 config/pkg/lib/krb5.yml create mode 100644 src/Package/Library/krb5.php diff --git a/config/pkg/lib/gettext.yml b/config/pkg/lib/gettext.yml index 4be2e7d72..5b1f57a63 100644 --- a/config/pkg/lib/gettext.yml +++ b/config/pkg/lib/gettext.yml @@ -8,12 +8,12 @@ gettext: metadata: license-files: [gettext-runtime/intl/COPYING.LIB] license: LGPL-2.1-or-later - static-libs@unix: - - libintl.a depends: - libiconv + frameworks: + - CoreFoundation + static-libs@unix: + - libintl.a suggests: - ncurses - libxml2 - frameworks: - - CoreFoundation diff --git a/config/pkg/lib/idn2.yml b/config/pkg/lib/idn2.yml index c2970d3ec..5d2e20439 100644 --- a/config/pkg/lib/idn2.yml +++ b/config/pkg/lib/idn2.yml @@ -6,16 +6,16 @@ idn2: url: 'https://ftp.gnu.org/gnu/libidn/' regex: '/href="(?libidn2-(?[^"]+)\.tar\.gz)"/' metadata: - license-files: ['COPYING.LESSERv3'] + license-files: [COPYING.LESSERv3] license: LGPL-3.0-or-later - pkg-configs: - - libidn2 + depends@macos: + - libiconv + - gettext headers: - idn2.h + pkg-configs: + - libidn2 suggests@unix: - libiconv - gettext - libunistring - depends@macos: - - libiconv - - gettext diff --git a/config/pkg/lib/krb5.yml b/config/pkg/lib/krb5.yml new file mode 100644 index 000000000..305a95a18 --- /dev/null +++ b/config/pkg/lib/krb5.yml @@ -0,0 +1,23 @@ +krb5: + type: library + artifact: + source: + type: ghtagtar + repo: krb5/krb5 + match: krb5.+-final + metadata: + license-files: [NOTICE] + license: BSD-3-Clause + source-root: src + depends: + - openssl + frameworks: + - Kerberos + headers: + - krb5.h + - gssapi/gssapi.h + pkg-configs: + - krb5-gssapi + suggests: + - ldap + - libedit diff --git a/config/pkg/lib/libedit.yml b/config/pkg/lib/libedit.yml index c782238bf..02d6dd810 100644 --- a/config/pkg/lib/libedit.yml +++ b/config/pkg/lib/libedit.yml @@ -7,9 +7,9 @@ libedit: regex: '/href="(?libedit-(?[^"]+)\.tar\.gz)"/' binary: hosted metadata: - license-files: ['COPYING'] + license-files: [COPYING] license: BSD-3-Clause - static-libs@unix: - - libedit.a depends: - ncurses + static-libs@unix: + - libedit.a diff --git a/config/pkg/lib/ncurses.yml b/config/pkg/lib/ncurses.yml index be50a6c7f..cbc1ba676 100644 --- a/config/pkg/lib/ncurses.yml +++ b/config/pkg/lib/ncurses.yml @@ -7,6 +7,6 @@ ncurses: regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' binary: hosted metadata: - license-files: ['COPYING'] + license-files: [COPYING] static-libs@unix: - libncurses.a diff --git a/config/pkg/lib/zlib.yml b/config/pkg/lib/zlib.yml index 9ca70f426..cf7f11ba0 100644 --- a/config/pkg/lib/zlib.yml +++ b/config/pkg/lib/zlib.yml @@ -7,7 +7,7 @@ zlib: match: zlib.+\.tar\.gz binary: hosted metadata: - license-files: ['{registry_root}/src/globals/licenses/zlib.txt'] + license-files: ['@/zlib.txt'] license: Zlib-Custom headers: - zlib.h diff --git a/src/Package/Library/krb5.php b/src/Package/Library/krb5.php new file mode 100644 index 000000000..303c3b63e --- /dev/null +++ b/src/Package/Library/krb5.php @@ -0,0 +1,63 @@ +cd($lib->getSourceRoot())->exec('autoreconf -if'); + + $resolved = array_keys($installer->getResolvedPackages()); + $spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); + $config = $spc->getPackageDepsConfig($lib->getName(), $resolved, include_suggests: true); + $extraEnv = [ + 'CFLAGS' => '-fcommon', + 'LIBS' => $config['libs'], + ]; + if (getenv('SPC_LD_LIBRARY_PATH') && getenv('SPC_LIBRARY_PATH')) { + $extraEnv = [...$extraEnv, ...[ + 'LD_LIBRARY_PATH' => getenv('SPC_LD_LIBRARY_PATH'), + 'LIBRARY_PATH' => getenv('SPC_LIBRARY_PATH'), + ]]; + } + $args = [ + '--disable-nls', + '--disable-rpath', + '--without-system-verto', + ]; + if (SystemTarget::getTargetOS() === 'Darwin') { + $extraEnv['LDFLAGS'] = '-framework Kerberos'; + $args[] = 'ac_cv_func_secure_getenv=no'; + } + UnixAutoconfExecutor::create($lib) + ->appendEnv($extraEnv) + ->optionalPackage('ldap', '--with-ldap', '--without-ldap') + ->optionalPackage('libedit', '--with-libedit', '--without-libedit') + ->configure(...$args) + ->make(); + $lib->patchPkgconfPrefix([ + 'krb5-gssapi.pc', + 'krb5.pc', + 'kadm-server.pc', + 'kadm-client.pc', + 'kdb.pc', + 'mit-krb5-gssapi.pc', + 'mit-krb5.pc', + 'gssrpc.pc', + ]); + } +} From 103b5b35853f69813399b5446f5ff450e7fe2d2c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 11:22:32 +0800 Subject: [PATCH 180/682] Upgrade phpstan to v2 --- composer.json | 2 +- composer.lock | 299 +++++++++--------- phpstan.neon | 6 +- src/Package/Target/php.php | 1 - src/StaticPHP/Artifact/ArtifactDownloader.php | 2 +- src/StaticPHP/Artifact/ArtifactExtractor.php | 1 + src/StaticPHP/Registry/PackageLoader.php | 6 +- src/StaticPHP/Util/LicenseDumper.php | 2 +- src/StaticPHP/Util/System/UnixUtil.php | 2 +- 9 files changed, 162 insertions(+), 159 deletions(-) diff --git a/composer.json b/composer.json index fb17f9a0d..fbdbc94b8 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "captainhook/hook-installer": "^1.0", "friendsofphp/php-cs-fixer": "^3.60", "humbug/box": "^4.5.0 || ^4.6.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.3 || ^9.5" }, "autoload": { diff --git a/composer.lock b/composer.lock index a0538ce8e..40489a068 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "edb3243ddaa8b05d8f6545266a146e93", + "content-hash": "f30595c9c60e55083112410cd1ffb203", "packages": [ { "name": "laravel/prompts", - "version": "v0.3.8", + "version": "v0.3.11", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" + "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", + "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", + "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", "shasum": "" }, "require": { @@ -61,22 +61,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.8" + "source": "https://github.com/laravel/prompts/tree/v0.3.11" }, - "time": "2025-11-21T20:52:52+00:00" + "time": "2026-01-27T02:55:06+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.7", + "version": "v2.0.8", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", "shasum": "" }, "require": { @@ -124,7 +124,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-11-21T20:52:36+00:00" + "time": "2026-01-08T16:22:46+00:00" }, { "name": "nette/php-generator", @@ -200,16 +200,16 @@ }, { "name": "nette/utils", - "version": "v4.1.0", + "version": "v4.1.1", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", - "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", + "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", "shasum": "" }, "require": { @@ -283,9 +283,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.0" + "source": "https://github.com/nette/utils/tree/v4.1.1" }, - "time": "2025-12-01T17:49:23+00:00" + "time": "2025-12-22T12:14:32+00:00" }, { "name": "php-di/invoker", @@ -520,16 +520,16 @@ }, { "name": "symfony/console", - "version": "v7.4.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", "shasum": "" }, "require": { @@ -594,7 +594,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.0" + "source": "https://github.com/symfony/console/tree/v7.4.4" }, "funding": [ { @@ -614,7 +614,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-01-13T11:36:38+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1020,16 +1020,16 @@ }, { "name": "symfony/process", - "version": "v7.4.0", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -1061,7 +1061,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.0" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -1081,7 +1081,7 @@ "type": "tidelift" } ], - "time": "2025-10-16T11:21:06+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/service-contracts", @@ -1172,16 +1172,16 @@ }, { "name": "symfony/string", - "version": "v8.0.0", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f929eccf09531078c243df72398560e32fa4cf4f" + "reference": "758b372d6882506821ed666032e43020c4f57194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", - "reference": "f929eccf09531078c243df72398560e32fa4cf4f", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", "shasum": "" }, "require": { @@ -1238,7 +1238,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.0" + "source": "https://github.com/symfony/string/tree/v8.0.4" }, "funding": [ { @@ -1258,20 +1258,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:37:55+00:00" + "time": "2026-01-12T12:37:40+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.0", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", - "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", + "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", "shasum": "" }, "require": { @@ -1314,7 +1314,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.0" + "source": "https://github.com/symfony/yaml/tree/v7.4.1" }, "funding": [ { @@ -1334,7 +1334,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2025-12-04T18:11:45+00:00" }, { "name": "zhamao/logger", @@ -1719,16 +1719,16 @@ }, { "name": "amphp/parallel", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce" + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/321b45ae771d9c33a068186b24117e3cd1c48dce", - "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce", + "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", + "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", "shasum": "" }, "require": { @@ -1791,7 +1791,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.2" + "source": "https://github.com/amphp/parallel/tree/v2.3.3" }, "funding": [ { @@ -1799,7 +1799,7 @@ "type": "github" } ], - "time": "2025-08-27T21:55:40+00:00" + "time": "2025-11-15T06:23:42+00:00" }, { "name": "amphp/parser", @@ -2217,7 +2217,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.27.3", + "version": "5.27.5", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2271,7 +2271,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.27.3" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.27.5" }, "funding": [ { @@ -2968,16 +2968,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.91.0", + "version": "v3.93.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "c4a25f20390337789c26b693ae46faa125040352" + "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c4a25f20390337789c26b693ae46faa125040352", - "reference": "c4a25f20390337789c26b693ae46faa125040352", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b3546ab487c0762c39f308dc1ec0ea2c461fc21a", + "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a", "shasum": "" }, "require": { @@ -3009,16 +3009,17 @@ }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.31.0", - "justinrainbow/json-schema": "^6.5", - "keradus/cli-executor": "^2.2", + "infection/infection": "^0.32", + "justinrainbow/json-schema": "^6.6", + "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.9", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0", - "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0" + "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -3033,7 +3034,7 @@ "PhpCsFixer\\": "src/" }, "exclude-from-classmap": [ - "src/Fixer/Internal/*" + "src/**/Internal/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3059,7 +3060,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.91.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.1" }, "funding": [ { @@ -3067,7 +3068,7 @@ "type": "github" } ], - "time": "2025-11-28T22:07:42+00:00" + "time": "2026-01-28T23:50:50+00:00" }, { "name": "humbug/box", @@ -3316,21 +3317,21 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.2", + "version": "6.6.4", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7" + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/3c25fe750c1599716ef26aa997f7c026cee8c4b7", - "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", "shasum": "" }, "require": { "ext-json": "*", - "marc-mabe/php-enum": "^4.0", + "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" }, "require-dev": { @@ -3385,9 +3386,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" }, - "time": "2025-11-28T15:24:03+00:00" + "time": "2025-12-19T15:01:32+00:00" }, { "name": "kelunik/certificate", @@ -3449,20 +3450,20 @@ }, { "name": "league/uri", - "version": "7.6.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "f625804987a0a9112d954f9209d91fec52182344" + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", - "reference": "f625804987a0a9112d954f9209d91fec52182344", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.6", + "league/uri-interfaces": "^7.8", "php": "^8.1", "psr/http-factory": "^1" }, @@ -3476,11 +3477,11 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", - "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3535,7 +3536,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.6.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.0" }, "funding": [ { @@ -3543,20 +3544,20 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2026-01-14T17:24:56+00:00" }, { "name": "league/uri-interfaces", - "version": "7.6.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", - "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", "shasum": "" }, "require": { @@ -3569,7 +3570,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3619,7 +3620,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" }, "funding": [ { @@ -3627,7 +3628,7 @@ "type": "github" } ], - "time": "2025-11-18T12:17:23+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "marc-mabe/php-enum", @@ -3816,16 +3817,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3868,9 +3869,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/composer-distributor", @@ -4259,16 +4260,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.5", + "version": "5.6.6", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", - "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", "shasum": "" }, "require": { @@ -4278,7 +4279,7 @@ "phpdocumentor/reflection-common": "^2.2", "phpdocumentor/type-resolver": "^1.7", "phpstan/phpdoc-parser": "^1.7|^2.0", - "webmozart/assert": "^1.9.1" + "webmozart/assert": "^1.9.1 || ^2" }, "require-dev": { "mockery/mockery": "~1.3.5 || ~1.6.0", @@ -4317,9 +4318,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" }, - "time": "2025-11-27T19:50:05+00:00" + "time": "2025-12-22T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4381,16 +4382,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.0", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -4422,21 +4423,21 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "2.1.38", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -4477,7 +4478,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-01-30T17:12:46+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4802,16 +4803,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -4832,7 +4833,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -4883,7 +4884,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -4907,7 +4908,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "psr/event-dispatcher", @@ -5141,16 +5142,16 @@ }, { "name": "react/child-process", - "version": "v0.6.6", + "version": "v0.6.7", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", "shasum": "" }, "require": { @@ -5204,7 +5205,7 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.6" + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" }, "funding": [ { @@ -5212,7 +5213,7 @@ "type": "open_collective" } ], - "time": "2025-01-01T16:37:48+00:00" + "time": "2025-12-23T15:25:20+00:00" }, { "name": "react/dns", @@ -5835,16 +5836,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -5900,7 +5901,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -5920,7 +5921,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -6732,16 +6733,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.0.0", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "573f95783a2ec6e38752979db139f09fec033f03" + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", - "reference": "573f95783a2ec6e38752979db139f09fec033f03", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", "shasum": "" }, "require": { @@ -6793,7 +6794,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" }, "funding": [ { @@ -6813,7 +6814,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T14:17:19+00:00" + "time": "2026-01-05T11:45:55+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6963,16 +6964,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.0", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", + "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", "shasum": "" }, "require": { @@ -7007,7 +7008,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.0" + "source": "https://github.com/symfony/finder/tree/v7.4.5" }, "funding": [ { @@ -7027,7 +7028,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T05:42:40+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/options-resolver", @@ -7332,16 +7333,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", "shasum": "" }, "require": { @@ -7395,7 +7396,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" }, "funding": [ { @@ -7415,7 +7416,7 @@ "type": "tidelift" } ], - "time": "2025-10-27T20:36:44+00:00" + "time": "2026-01-01T22:13:48+00:00" }, { "name": "thecodingmachine/safe", diff --git a/phpstan.neon b/phpstan.neon index 45e512ba0..ce7cdb2d2 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,11 +11,13 @@ parameters: - '#Function Swoole\\Coroutine\\run not found.#' - '#Static call to instance method ZM\\Logger\\ConsoleColor#' - '#Constant GNU_ARCH not found#' + - + identifiers: + - 'if.alwaysFalse' dynamicConstantNames: - PHP_OS_FAMILY excludePaths: analyseAndScan: - - ./src/globals/ext-tests/swoole.php - - ./src/globals/ext-tests/swoole.phpt + - ./src/globals/ext-tests/ - ./src/globals/test-extensions.php - ./src/SPC/ diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 8cd6e9407..32e8a13b1 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -241,7 +241,6 @@ private function makeStaticExtensionString(PackageInstaller $installer): string { $arg = []; foreach ($installer->getResolvedPackages() as $package) { - /** @var PhpExtensionPackage $package */ if ($package->getType() !== 'php-extension' || !$package instanceof PhpExtensionPackage) { continue; } diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b4d79b769..b07c56617 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -312,7 +312,7 @@ public function download(bool $interactive = true): void FileSystem::createDir(DOWNLOAD_PATH); } logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ..."); - // Download artifacts parallely + // Download artifacts parallelly if ($this->parallel > 1) { $this->downloadWithConcurrency(); } else { diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 93b363823..217a6f54f 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -247,6 +247,7 @@ protected function extractBinary(Artifact $artifact): int $artifact->emitAfterBinaryExtract($target_path, $platform); logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + /* @phpstan-ignore-next-line */ if ($hash !== null && $cache_info['cache_type'] !== 'file') { FileSystem::writeFile("{$target_path}/.spc-hash", $hash); } diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index e95021273..424363eb3 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -169,7 +169,7 @@ public static function loadFromClass(mixed $class): void } } - $pkg = self::$packages[$attribute_instance->name]; + $pkg = self::$packages[$attribute_instance->name] ?? null; // Use the package instance if it's a Package subclass, otherwise create a new instance $instance_class = is_a($class_name, Package::class, true) ? $pkg : $refClass->newInstance(); @@ -184,7 +184,7 @@ public static function loadFromClass(mixed $class): void if (!in_array($package_type, $pkg_type_attr, true)) { throw new RegistryException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].'); } - if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) { + if ($pkg instanceof Package && !PackageConfig::isPackageExists($pkg->getName())) { throw new RegistryException("Package [{$pkg->getName()}] config not found for class {$class}"); } @@ -355,7 +355,7 @@ private static function addBeforeStage(\ReflectionMethod $method, ?Package $pkg, $stage = $method_instance->stage; $stage = match (true) { is_string($stage) => $stage, - is_array($stage) && count($stage) === 2 => $stage[1], + count($stage) === 2 => $stage[1], default => throw new RegistryException('Invalid stage definition in BeforeStage attribute.'), }; if ($method_instance->package_name === '' && $pkg === null) { diff --git a/src/StaticPHP/Util/LicenseDumper.php b/src/StaticPHP/Util/LicenseDumper.php index 54c6266bb..f439ec523 100644 --- a/src/StaticPHP/Util/LicenseDumper.php +++ b/src/StaticPHP/Util/LicenseDumper.php @@ -75,7 +75,7 @@ public function dump(string $target_dir): bool * * @param Artifact $artifact Artifact instance * @param string $target_dir Target directory - * @param array &$license_summary Summary data to populate + * @param array &$license_summary Summary data to populate * @return bool True if dumped * @throws SPCInternalException */ diff --git a/src/StaticPHP/Util/System/UnixUtil.php b/src/StaticPHP/Util/System/UnixUtil.php index 8dd606188..aca50d9e4 100644 --- a/src/StaticPHP/Util/System/UnixUtil.php +++ b/src/StaticPHP/Util/System/UnixUtil.php @@ -44,7 +44,7 @@ public static function exportDynamicSymbols(string $lib_file): void continue; } $name = preg_replace('/@.*$/', '', $name); - if ($name !== '' && $name !== false) { + if (!empty($name)) { $defined[] = $name; } } From 7041e060f2fb028f028a6b0c85d0025cd5aa21c6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 13:02:43 +0800 Subject: [PATCH 181/682] Add curl --- config/pkg/lib/curl.yml | 33 ++++++++++++++++++ src/Package/Library/curl.php | 61 +++++++++++++++++++++++++++++++++ src/StaticPHP/Doctor/Doctor.php | 3 +- 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 config/pkg/lib/curl.yml create mode 100644 src/Package/Library/curl.php diff --git a/config/pkg/lib/curl.yml b/config/pkg/lib/curl.yml new file mode 100644 index 000000000..38d632ce8 --- /dev/null +++ b/config/pkg/lib/curl.yml @@ -0,0 +1,33 @@ +curl: + type: library + artifact: + source: + type: ghrel + repo: curl/curl + match: 'curl.+\.tar\.xz' + prefer-stable: true + metadata: + license-files: [COPYING] + license: curl + static-libs@unix: + - libcurl.a + headers: + - curl + depends@unix: + - openssl + - zlib + suggests@unix: + - libssh2 + - brotli + - nghttp2 + - nghttp3 + - ngtcp2 + - zstd + - libcares + - ldap + - idn2 + - krb5 + frameworks: + - CoreFoundation + - CoreServices + - SystemConfiguration diff --git a/src/Package/Library/curl.php b/src/Package/Library/curl.php new file mode 100644 index 000000000..a2856ad4b --- /dev/null +++ b/src/Package/Library/curl.php @@ -0,0 +1,61 @@ +cd($lib->getSourceDir())->exec('sed -i.save s@\${CMAKE_C_IMPLICIT_LINK_LIBRARIES}@@ ./CMakeLists.txt'); + if (SystemTarget::getTargetOS() === 'Darwin') { + FileSystem::replaceFileRegex("{$lib->getSourceDir()}/curl/CMakeLists.txt", '/NOT COREFOUNDATION_FRAMEWORK/m', 'FALSE'); + FileSystem::replaceFileRegex("{$lib->getSourceDir()}/curl/CMakeLists.txt", '/NOT SYSTEMCONFIGURATION_FRAMEWORK/m', 'FALSE'); + FileSystem::replaceFileRegex("{$lib->getSourceDir()}/curl/CMakeLists.txt", '/NOT CORESERVICES_FRAMEWORK/m', 'FALSE'); + } + return true; + } + + #[BuildFor('Linux')] + #[BuildFor('Darwin')] + public function build(LibraryPackage $lib): void + { + UnixCMakeExecutor::create($lib) + ->optionalPackage('openssl', '-DCURL_USE_OPENSSL=ON -DCURL_CA_BUNDLE=OFF -DCURL_CA_PATH=OFF -DCURL_CA_FALLBACK=ON', '-DCURL_USE_OPENSSL=OFF -DCURL_ENABLE_SSL=OFF') + ->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI')) + ->optionalPackage('libssh2', ...cmake_boolean_args('CURL_USE_LIBSSH2')) + ->optionalPackage('nghttp2', ...cmake_boolean_args('USE_NGHTTP2')) + ->optionalPackage('nghttp3', ...cmake_boolean_args('USE_NGHTTP3')) + ->optionalPackage('ngtcp2', ...cmake_boolean_args('USE_NGTCP2')) + ->optionalPackage('ldap', ...cmake_boolean_args('CURL_DISABLE_LDAP', true)) + ->optionalPackage('zstd', ...cmake_boolean_args('CURL_ZSTD')) + ->optionalPackage('idn2', ...cmake_boolean_args('USE_LIBIDN2')) + ->optionalPackage('psl', ...cmake_boolean_args('CURL_USE_LIBPSL')) + ->optionalPackage('krb5', ...cmake_boolean_args('CURL_USE_GSSAPI')) + ->optionalPackage('idn2', ...cmake_boolean_args('CURL_USE_IDN2')) + ->optionalPackage('libcares', '-DENABLE_ARES=ON') + ->addConfigureArgs( + '-DBUILD_CURL_EXE=OFF', + '-DBUILD_LIBCURL_DOCS=OFF', + ) + ->build(); + + // patch pkgconf + $lib->patchPkgconfPrefix(['libcurl.pc']); + shell()->cd("{$lib->getLibDir()}/cmake/CURL/") + ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); + } +} diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index d86e42ac2..36db37ae8 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -146,7 +146,8 @@ private function getValidCheckList(): iterable foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) { /* @var CheckItem $item */ // optional check - if ($optional !== null && !call_user_func($optional)) { + /* @phpstan-ignore-next-line */ + if (is_callable($optional) && !call_user_func($optional)) { continue; // skip this when the optional check is false } // limit_os check From 6fdbf629dc7dac1ba9501ca8020ecd467fc63efc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 14:00:21 +0800 Subject: [PATCH 182/682] Fix selective artifact installation detect --- src/StaticPHP/Artifact/Artifact.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 67876d900..b3e27371f 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -164,7 +164,14 @@ public function isBinaryExtracted(?string $target_os = null, bool $compare_hash // For selective mode, cannot reliably check extraction status if ($mode === 'selective') { - return false; + // check files existence + foreach ($extract_config['files'] as $target_file) { + $target_file = FileSystem::replacePathVariable($target_file); + if (!file_exists($target_file)) { + return false; + } + } + return true; } // For standalone mode, check directory or file and hash From 38f742156d614ad427bb4d9c099d2d90e675b6c8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 14:01:41 +0800 Subject: [PATCH 183/682] Use zig toolchain by default, lint files --- config/env.ini | 24 +- config/pkg.ext.json | 534 +++++++++--------- config/pkg.target.json | 67 +-- config/pkg/lib/curl.yml | 10 +- config/pkg/lib/gettext.yml | 6 +- config/pkg/lib/idn2.yml | 8 +- config/pkg/lib/krb5.yml | 6 +- config/pkg/lib/libxml2.yml | 6 +- config/pkg/lib/nghttp2.yml | 10 +- config/pkg/lib/ngtcp2.yml | 6 +- config/pkg/target/pkg-config.yml | 9 + config/pkg/target/zig.yml | 4 + spc.registry.json | 1 + .../Command/Dev/LintConfigCommand.php | 2 +- src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 10 +- src/StaticPHP/Package/PackageInstaller.php | 2 +- .../Toolchain/ClangNativeToolchain.php | 8 +- .../Toolchain/GccNativeToolchain.php | 8 +- src/StaticPHP/Toolchain/MuslToolchain.php | 10 +- src/StaticPHP/Util/PkgConfigUtil.php | 7 +- 20 files changed, 373 insertions(+), 365 deletions(-) create mode 100644 config/pkg/target/pkg-config.yml create mode 100644 config/pkg/target/zig.yml diff --git a/config/env.ini b/config/env.ini index 718e556ff..c77f6bcd0 100644 --- a/config/env.ini +++ b/config/env.ini @@ -83,7 +83,7 @@ SPC_MICRO_PATCHES=static_extensions_win32,cli_checks,disable_huge_page,vcruntime ; - gnu-native: used for general linux distros, can build gnu target for the installed glibc version only. ; option to specify the target, superceded by SPC_TARGET if set -SPC_LIBC=musl +; SPC_LIBC=musl ; uncomment to link libc dynamically on musl ; SPC_MUSL_DYNAMIC=true @@ -94,13 +94,13 @@ SPC_LIBC=musl ; `native-native` - links against system libc dynamically ; `native-native-musl` - links against musl libc statically ; `native-native-musl -dynamic` - links against musl libc dynamically -; SPC_TARGET= +SPC_TARGET=native-native-musl -; compiler environments -CC=${SPC_LINUX_DEFAULT_CC} -CXX=${SPC_LINUX_DEFAULT_CXX} -AR=${SPC_LINUX_DEFAULT_AR} -LD=${SPC_LINUX_DEFAULT_LD} +; compiler environments (default value is defined by selected toolchain) +CC=${SPC_DEFAULT_CC} +CXX=${SPC_DEFAULT_CXX} +AR=${SPC_DEFAULT_AR} +LD=${SPC_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build SPC_DEFAULT_C_FLAGS="-fPIC -Os" SPC_DEFAULT_CXX_FLAGS="-fPIC -Os" @@ -132,11 +132,11 @@ OPENSSLDIR="" ; build target: macho or macho (possibly we could support macho-universal in the future) ; Currently we do not support universal and cross-compilation for macOS. SPC_TARGET=native-macos -; compiler environments -CC=clang -CXX=clang++ -AR=ar -LD=ld +; compiler environments (default value is defined by selected toolchain) +CC=${SPC_DEFAULT_CC} +CXX=${SPC_DEFAULT_CXX} +AR=${SPC_DEFAULT_AR} +LD=${SPC_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build SPC_DEFAULT_C_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os" SPC_DEFAULT_CXX_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os" diff --git a/config/pkg.ext.json b/config/pkg.ext.json index b1e819eb3..d1ca0b3f0 100644 --- a/config/pkg.ext.json +++ b/config/pkg.ext.json @@ -1,5 +1,6 @@ { "ext-amqp": { + "type": "php-extension", "artifact": "amqp", "depends": [ "librabbitmq" @@ -16,29 +17,29 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-apcu": { + "type": "php-extension", "artifact": "apcu", "license": { "type": "file", "path": "LICENSE" - }, - "type": "php-extension" + } }, "ext-ast": { + "type": "php-extension", "artifact": "ast", "license": { "type": "file", "path": "LICENSE" - }, - "type": "php-extension" + } }, "ext-bcmath": { "type": "php-extension" }, "ext-brotli": { + "type": "php-extension", "artifact": "ext-brotli", "depends": [ "brotli" @@ -49,18 +50,17 @@ }, "php-extension": { "arg-type": "enable" - }, - "type": "php-extension" + } }, "ext-bz2": { + "type": "php-extension", "depends": [ "bzip2" ], "php-extension": { "arg-type@windows": "with", "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-calendar": { "type": "php-extension" @@ -69,6 +69,7 @@ "type": "php-extension" }, "ext-curl": { + "type": "php-extension", "depends": [ "curl" ], @@ -79,19 +80,19 @@ "php-extension": { "arg-type": "with", "notes": true - }, - "type": "php-extension" + } }, "ext-dba": { - "php-extension": { - "arg-type": "custom" - }, + "type": "php-extension", "suggests": [ "qdbm" ], - "type": "php-extension" + "php-extension": { + "arg-type": "custom" + } }, "ext-dio": { + "type": "php-extension", "artifact": "dio", "license": { "type": "file", @@ -101,10 +102,10 @@ "support": { "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-dom": { + "type": "php-extension", "depends": [ "libxml2", "zlib" @@ -118,18 +119,18 @@ }, "arg-type": "custom", "arg-type@windows": "with" - }, - "type": "php-extension" + } }, "ext-ds": { + "type": "php-extension", "artifact": "ext-ds", "license": { "type": "file", "path": "LICENSE" - }, - "type": "php-extension" + } }, "ext-enchant": { + "type": "php-extension", "php-extension": { "support": { "Windows": "wip", @@ -137,10 +138,10 @@ "Darwin": "wip", "Linux": "wip" } - }, - "type": "php-extension" + } }, "ext-ev": { + "type": "php-extension", "artifact": "ev", "depends": [ "ext-sockets" @@ -151,15 +152,18 @@ }, "php-extension": { "arg-type@windows": "with" - }, - "type": "php-extension" + } }, "ext-event": { + "type": "php-extension", "artifact": "ext-event", "depends": [ "libevent", "ext-openssl" ], + "suggests": [ + "ext-sockets" + ], "license": { "type": "file", "path": "LICENSE" @@ -171,16 +175,13 @@ }, "arg-type": "custom", "notes": true - }, - "suggests": [ - "ext-sockets" - ], - "type": "php-extension" + } }, "ext-exif": { "type": "php-extension" }, "ext-ffi": { + "type": "php-extension", "depends": [ "libffi" ], @@ -194,8 +195,7 @@ }, "arg-type": "custom", "notes": true - }, - "type": "php-extension" + } }, "ext-fileinfo": { "type": "php-extension" @@ -204,17 +204,24 @@ "type": "php-extension" }, "ext-ftp": { + "type": "php-extension", "suggests": [ "openssl" - ], - "type": "php-extension" + ] }, "ext-gd": { + "type": "php-extension", "depends": [ "zlib", "libpng", "ext-zlib" ], + "suggests": [ + "libavif", + "libwebp", + "libjpeg", + "freetype" + ], "php-extension": { "support": { "BSD": "wip" @@ -222,16 +229,10 @@ "arg-type": "custom", "arg-type@windows": "with", "notes": true - }, - "suggests": [ - "libavif", - "libwebp", - "libjpeg", - "freetype" - ], - "type": "php-extension" + } }, "ext-gettext": { + "type": "php-extension", "depends": [ "gettext" ], @@ -241,10 +242,10 @@ "BSD": "wip" }, "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-glfw": { + "type": "php-extension", "artifact": "ext-glfw", "depends": [ "glfw" @@ -262,10 +263,10 @@ }, "arg-type": "custom", "notes": true - }, - "type": "php-extension" + } }, "ext-gmp": { + "type": "php-extension", "depends": [ "gmp" ], @@ -275,10 +276,10 @@ "BSD": "wip" }, "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-gmssl": { + "type": "php-extension", "artifact": "ext-gmssl", "depends": [ "gmssl" @@ -291,10 +292,10 @@ "support": { "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-grpc": { + "type": "php-extension", "artifact": "grpc", "depends": [ "grpc" @@ -310,10 +311,10 @@ "BSD": "wip" }, "arg-type": "enable-path" - }, - "type": "php-extension" + } }, "ext-iconv": { + "type": "php-extension", "depends": [ "libiconv" ], @@ -326,11 +327,15 @@ }, "arg-type": "with-path", "arg-type@windows": "with" - }, - "type": "php-extension" + } }, "ext-igbinary": { + "type": "php-extension", "artifact": "igbinary", + "suggests": [ + "ext-session", + "ext-apcu" + ], "license": { "type": "file", "path": "COPYING" @@ -339,14 +344,10 @@ "support": { "BSD": "wip" } - }, - "suggests": [ - "ext-session", - "ext-apcu" - ], - "type": "php-extension" + } }, "ext-imagick": { + "type": "php-extension", "artifact": "ext-imagick", "depends": [ "imagemagick" @@ -362,14 +363,17 @@ }, "arg-type": "custom", "notes": true - }, - "type": "php-extension" + } }, "ext-imap": { + "type": "php-extension", "artifact": "ext-imap", "depends": [ "imap" ], + "suggests": [ + "ext-openssl" + ], "license": { "type": "file", "path": [ @@ -383,13 +387,10 @@ }, "arg-type": "custom", "notes": true - }, - "suggests": [ - "ext-openssl" - ], - "type": "php-extension" + } }, "ext-inotify": { + "type": "php-extension", "artifact": "inotify", "license": { "type": "file", @@ -401,10 +402,10 @@ "BSD": "wip", "Darwin": "no" } - }, - "type": "php-extension" + } }, "ext-intl": { + "type": "php-extension", "depends": [ "icu" ], @@ -415,28 +416,28 @@ "support": { "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-ldap": { + "type": "php-extension", "depends": [ "ldap" ], + "suggests": [ + "gmp", + "libsodium", + "ext-openssl" + ], "php-extension": { "support": { "Windows": "wip", "BSD": "wip" }, "arg-type": "with-path" - }, - "suggests": [ - "gmp", - "libsodium", - "ext-openssl" - ], - "type": "php-extension" + } }, "ext-libxml": { + "type": "php-extension", "depends": [ "ext-xml" ], @@ -448,10 +449,10 @@ "build-shared": false, "build-static": true, "build-with-php": true - }, - "type": "php-extension" + } }, "ext-lz4": { + "type": "php-extension", "artifact": "ext-lz4", "depends": [ "liblz4" @@ -468,10 +469,10 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-mbregex": { + "type": "php-extension", "depends": [ "onig", "ext-mbstring" @@ -480,16 +481,16 @@ "arg-type": "custom", "build-shared": false, "build-static": true - }, - "type": "php-extension" + } }, "ext-mbstring": { + "type": "php-extension", "php-extension": { "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-mcrypt": { + "type": "php-extension", "php-extension": { "support": { "Windows": "no", @@ -498,10 +499,10 @@ "Linux": "no" }, "notes": true - }, - "type": "php-extension" + } }, "ext-memcache": { + "type": "php-extension", "artifact": "ext-memcache", "depends": [ "ext-zlib", @@ -518,10 +519,10 @@ }, "arg-type": "custom", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-memcached": { + "type": "php-extension", "artifact": "memcached", "depends": [ "libmemcached", @@ -529,6 +530,12 @@ "ext-session", "ext-zlib" ], + "suggests": [ + "zstd", + "ext-igbinary", + "ext-msgpack", + "ext-session" + ], "lang": "cpp", "license": { "type": "file", @@ -540,17 +547,17 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "suggests": [ - "zstd", - "ext-igbinary", - "ext-msgpack", - "ext-session" - ], - "type": "php-extension" + } }, "ext-mongodb": { + "type": "php-extension", "artifact": "mongodb", + "suggests": [ + "icu", + "openssl", + "zstd", + "zlib" + ], "frameworks": [ "CoreFoundation", "Security" @@ -565,16 +572,10 @@ "Windows": "wip" }, "arg-type": "custom" - }, - "suggests": [ - "icu", - "openssl", - "zstd", - "zlib" - ], - "type": "php-extension" + } }, "ext-msgpack": { + "type": "php-extension", "artifact": "msgpack", "depends": [ "ext-session" @@ -589,30 +590,30 @@ }, "arg-type@windows": "enable", "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-mysqli": { + "type": "php-extension", "depends": [ "ext-mysqlnd" ], "php-extension": { "arg-type": "with", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-mysqlnd": { + "type": "php-extension", "depends": [ "zlib" ], "php-extension": { "arg-type@windows": "with", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-oci8": { + "type": "php-extension", "php-extension": { "support": { "Windows": "wip", @@ -621,10 +622,10 @@ "Linux": "no" }, "notes": true - }, - "type": "php-extension" + } }, "ext-odbc": { + "type": "php-extension", "depends": [ "unixodbc" ], @@ -634,18 +635,18 @@ "Windows": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-opcache": { + "type": "php-extension", "php-extension": { "arg-type@windows": "enable", "arg-type": "custom", "zend-extension": true - }, - "type": "php-extension" + } }, "ext-openssl": { + "type": "php-extension", "depends": [ "openssl", "zlib", @@ -656,10 +657,10 @@ "arg-type@windows": "with", "build-with-php": true, "notes": true - }, - "type": "php-extension" + } }, "ext-opentelemetry": { + "type": "php-extension", "artifact": "opentelemetry", "license": { "type": "file", @@ -669,10 +670,10 @@ "support": { "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-parallel": { + "type": "php-extension", "artifact": "parallel", "depends@windows": [ "pthreads4w" @@ -687,10 +688,10 @@ }, "arg-type@windows": "with", "notes": true - }, - "type": "php-extension" + } }, "ext-password-argon2": { + "type": "php-extension", "depends": [ "libargon2", "openssl" @@ -702,31 +703,31 @@ }, "arg-type": "custom", "notes": true - }, - "type": "php-extension" + } }, "ext-pcntl": { + "type": "php-extension", "php-extension": { "support": { "Windows": "no" } - }, - "type": "php-extension" + } }, "ext-pdo": { "type": "php-extension" }, "ext-pdo_mysql": { + "type": "php-extension", "depends": [ "ext-pdo", "ext-mysqlnd" ], "php-extension": { "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-pdo_odbc": { + "type": "php-extension", "depends": [ "unixodbc", "ext-pdo", @@ -737,10 +738,10 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-pdo_pgsql": { + "type": "php-extension", "depends": [ "postgresql", "ext-pdo", @@ -755,10 +756,10 @@ }, "arg-type": "with-path", "arg-type@windows": "custom" - }, - "type": "php-extension" + } }, "ext-pdo_sqlite": { + "type": "php-extension", "depends": [ "sqlite", "ext-pdo", @@ -769,10 +770,10 @@ "BSD": "wip" }, "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-pdo_sqlsrv": { + "type": "php-extension", "artifact": "pdo_sqlsrv", "depends": [ "ext-pdo", @@ -787,10 +788,10 @@ "BSD": "wip" }, "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-pgsql": { + "type": "php-extension", "depends": [ "postgresql" ], @@ -803,24 +804,24 @@ }, "arg-type": "custom", "notes": true - }, - "type": "php-extension" + } }, "ext-phar": { + "type": "php-extension", "depends": [ "ext-zlib" - ], - "type": "php-extension" + ] }, "ext-posix": { + "type": "php-extension", "php-extension": { "support": { "Windows": "no" } - }, - "type": "php-extension" + } }, "ext-protobuf": { + "type": "php-extension", "artifact": "protobuf", "license": { "type": "file", @@ -831,10 +832,10 @@ "Windows": "wip", "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-rar": { + "type": "php-extension", "artifact": "rar", "lang": "cpp", "license": { @@ -847,10 +848,10 @@ "Darwin": "partial" }, "notes": true - }, - "type": "php-extension" + } }, "ext-rdkafka": { + "type": "php-extension", "artifact": "ext-rdkafka", "depends": [ "librdkafka" @@ -866,10 +867,10 @@ "Windows": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-readline": { + "type": "php-extension", "depends": [ "libedit" ], @@ -881,11 +882,18 @@ "arg-type": "with-path", "build-shared": false, "build-static": true - }, - "type": "php-extension" + } }, "ext-redis": { + "type": "php-extension", "artifact": "redis", + "suggests": [ + "zstd", + "liblz4", + "ext-session", + "ext-igbinary", + "ext-msgpack" + ], "license": { "type": "file", "path": [ @@ -898,38 +906,31 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "suggests": [ - "zstd", - "liblz4", - "ext-session", - "ext-igbinary", - "ext-msgpack" - ], - "type": "php-extension" + } }, "ext-session": { + "type": "php-extension", "php-extension": { "build-with-php": true - }, - "type": "php-extension" + } }, "ext-shmop": { + "type": "php-extension", "php-extension": { "build-with-php": true - }, - "type": "php-extension" + } }, "ext-simdjson": { + "type": "php-extension", "artifact": "ext-simdjson", "lang": "cpp", "license": { "type": "file", "path": "LICENSE" - }, - "type": "php-extension" + } }, "ext-simplexml": { + "type": "php-extension", "depends": [ "libxml2" ], @@ -942,14 +943,17 @@ }, "arg-type": "custom", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-snappy": { + "type": "php-extension", "artifact": "ext-snappy", "depends": [ "snappy" ], + "suggests": [ + "ext-apcu" + ], "lang": "cpp", "license": { "type": "file", @@ -961,13 +965,10 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "suggests": [ - "ext-apcu" - ], - "type": "php-extension" + } }, "ext-snmp": { + "type": "php-extension", "depends": [ "net-snmp" ], @@ -978,10 +979,10 @@ }, "arg-type@windows": "with", "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-soap": { + "type": "php-extension", "depends": [ "ext-libxml", "ext-session" @@ -991,13 +992,13 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-sockets": { "type": "php-extension" }, "ext-sodium": { + "type": "php-extension", "depends": [ "libsodium" ], @@ -1006,10 +1007,10 @@ "BSD": "wip" }, "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-spx": { + "type": "php-extension", "artifact": "spx", "depends": [ "zlib" @@ -1025,10 +1026,10 @@ }, "arg-type": "custom", "notes": true - }, - "type": "php-extension" + } }, "ext-sqlite3": { + "type": "php-extension", "depends": [ "sqlite" ], @@ -1039,10 +1040,10 @@ "arg-type": "with-path", "arg-type@windows": "with", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-sqlsrv": { + "type": "php-extension", "artifact": "sqlsrv", "depends": [ "unixodbc" @@ -1059,10 +1060,10 @@ "support": { "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-ssh2": { + "type": "php-extension", "artifact": "ext-ssh2", "depends": [ "libssh2", @@ -1079,10 +1080,10 @@ }, "arg-type": "with-path", "arg-type@windows": "with" - }, - "type": "php-extension" + } }, "ext-swoole": { + "type": "php-extension", "artifact": "swoole", "depends": [ "libcares", @@ -1092,19 +1093,6 @@ "ext-openssl", "ext-curl" ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - }, "suggests": [ "zstd", "ext-sockets", @@ -1117,15 +1105,31 @@ "zstd", "liburing" ], - "type": "php-extension" + "lang": "cpp", + "license": { + "type": "file", + "path": "LICENSE" + }, + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + } }, "ext-swoole-hook-mysql": { + "type": "php-extension", "depends": [ "ext-mysqlnd", "ext-pdo", "ext-pdo_mysql", "ext-swoole" ], + "suggests": [ + "ext-mysqli" + ], "php-extension": { "support": { "Windows": "no", @@ -1133,13 +1137,10 @@ }, "arg-type": "none", "notes": true - }, - "suggests": [ - "ext-mysqli" - ], - "type": "php-extension" + } }, "ext-swoole-hook-odbc": { + "type": "php-extension", "depends": [ "unixodbc", "ext-pdo", @@ -1152,10 +1153,10 @@ }, "arg-type": "none", "notes": true - }, - "type": "php-extension" + } }, "ext-swoole-hook-pgsql": { + "type": "php-extension", "depends": [ "ext-pgsql", "ext-pdo", @@ -1169,10 +1170,10 @@ }, "arg-type": "none", "notes": true - }, - "type": "php-extension" + } }, "ext-swoole-hook-sqlite": { + "type": "php-extension", "depends": [ "ext-sqlite3", "ext-pdo", @@ -1185,11 +1186,17 @@ }, "arg-type": "none", "notes": true - }, - "type": "php-extension" + } }, "ext-swow": { + "type": "php-extension", "artifact": "swow", + "suggests": [ + "openssl", + "curl", + "ext-openssl", + "ext-curl" + ], "license": { "type": "file", "path": "LICENSE" @@ -1200,42 +1207,36 @@ }, "arg-type": "custom", "notes": true - }, - "suggests": [ - "openssl", - "curl", - "ext-openssl", - "ext-curl" - ], - "type": "php-extension" + } }, "ext-sysvmsg": { + "type": "php-extension", "php-extension": { "support": { "Windows": "no", "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-sysvsem": { + "type": "php-extension", "php-extension": { "support": { "Windows": "no", "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-sysvshm": { + "type": "php-extension", "php-extension": { "support": { "BSD": "wip" } - }, - "type": "php-extension" + } }, "ext-tidy": { + "type": "php-extension", "depends": [ "tidy" ], @@ -1245,16 +1246,16 @@ "BSD": "wip" }, "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-tokenizer": { + "type": "php-extension", "php-extension": { "build-with-php": true - }, - "type": "php-extension" + } }, "ext-trader": { + "type": "php-extension", "artifact": "ext-trader", "license": { "type": "file", @@ -1265,10 +1266,10 @@ "BSD": "wip", "Windows": "wip" } - }, - "type": "php-extension" + } }, "ext-uuid": { + "type": "php-extension", "artifact": "ext-uuid", "depends": [ "libuuid" @@ -1283,10 +1284,10 @@ "BSD": "wip" }, "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-uv": { + "type": "php-extension", "artifact": "ext-uv", "depends": [ "libuv", @@ -1302,10 +1303,10 @@ "BSD": "wip" }, "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-xdebug": { + "type": "php-extension", "artifact": "xdebug", "license": { "type": "file", @@ -1322,10 +1323,10 @@ "build-static": false, "notes": true, "zend-extension": true - }, - "type": "php-extension" + } }, "ext-xhprof": { + "type": "php-extension", "artifact": "xhprof", "depends": [ "ext-ctype" @@ -1341,15 +1342,18 @@ }, "build-with-php": true, "notes": true - }, - "type": "php-extension" + } }, "ext-xlswriter": { + "type": "php-extension", "artifact": "xlswriter", "depends": [ "ext-zlib", "ext-zip" ], + "suggests": [ + "openssl" + ], "license": { "type": "file", "path": "LICENSE" @@ -1359,13 +1363,10 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "suggests": [ - "openssl" - ], - "type": "php-extension" + } }, "ext-xml": { + "type": "php-extension", "depends": [ "libxml2" ], @@ -1380,10 +1381,10 @@ "arg-type@windows": "with", "build-with-php": true, "notes": true - }, - "type": "php-extension" + } }, "ext-xmlreader": { + "type": "php-extension", "depends": [ "libxml2" ], @@ -1397,10 +1398,10 @@ }, "arg-type": "custom", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-xmlwriter": { + "type": "php-extension", "depends": [ "libxml2" ], @@ -1413,10 +1414,10 @@ }, "arg-type": "custom", "build-with-php": true - }, - "type": "php-extension" + } }, "ext-xsl": { + "type": "php-extension", "depends": [ "libxslt", "ext-xml", @@ -1428,10 +1429,10 @@ "BSD": "wip" }, "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-xz": { + "type": "php-extension", "artifact": "ext-xz", "depends": [ "xz" @@ -1442,10 +1443,10 @@ }, "php-extension": { "arg-type": "with" - }, - "type": "php-extension" + } }, "ext-yac": { + "type": "php-extension", "artifact": "yac", "depends": [ "fastlz", @@ -1460,10 +1461,10 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } }, "ext-yaml": { + "type": "php-extension", "artifact": "yaml", "depends": [ "libyaml" @@ -1478,10 +1479,10 @@ }, "arg-type@windows": "with", "arg-type": "with-path" - }, - "type": "php-extension" + } }, "ext-zip": { + "type": "php-extension", "artifact": "ext-zip", "depends": [ "libzip" @@ -1504,10 +1505,10 @@ }, "arg-type": "custom", "arg-type@windows": "enable" - }, - "type": "php-extension" + } }, "ext-zlib": { + "type": "php-extension", "depends": [ "zlib" ], @@ -1517,10 +1518,10 @@ "build-shared": false, "build-static": true, "build-with-php": true - }, - "type": "php-extension" + } }, "ext-zstd": { + "type": "php-extension", "artifact": "ext-zstd", "depends": [ "zstd" @@ -1535,7 +1536,6 @@ "BSD": "wip" }, "arg-type": "custom" - }, - "type": "php-extension" + } } } diff --git a/config/pkg.target.json b/config/pkg.target.json index 8e04df905..a5c20b78c 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -1,5 +1,6 @@ { "frankenphp": { + "type": "virtual-target", "artifact": "frankenphp", "depends": [ "php-embed", @@ -9,90 +10,78 @@ "php-embed", "go-xcaddy", "libxml2" - ], - "type": "virtual-target" + ] }, "go-xcaddy": { + "type": "target", "artifact": "go-xcaddy", "static-bins": [ "xcaddy" - ], - "type": "target" + ] }, "musl-toolchain": { - "artifact": "musl-toolchain", - "type": "target" + "type": "target", + "artifact": "musl-toolchain" }, "nasm": { - "artifact": "nasm", - "type": "target" + "type": "target", + "artifact": "nasm" }, "php": { + "type": "target", "artifact": "php-src", "depends@macos": [ "libxml2" - ], - "type": "target" + ] }, "php-cgi": { + "type": "virtual-target", "depends": [ "php" - ], - "type": "virtual-target" + ] }, "php-cli": { + "type": "virtual-target", "depends": [ "php" - ], - "type": "virtual-target" + ] }, "php-embed": { + "type": "virtual-target", "depends": [ "php" - ], - "type": "virtual-target" + ] }, "php-fpm": { + "type": "virtual-target", "depends": [ "php" - ], - "type": "virtual-target" + ] }, "php-micro": { + "type": "virtual-target", "artifact": "micro", "depends": [ "php" - ], - "type": "virtual-target" + ] }, "php-sdk-binary-tools": { - "artifact": "php-sdk-binary-tools", - "type": "target" - }, - "pkg-config": { - "artifact": "pkg-config", - "static-bins": [ - "pkg-config" - ], - "type": "target" + "type": "target", + "artifact": "php-sdk-binary-tools" }, "strawberry-perl": { - "artifact": "strawberry-perl", - "type": "target" + "type": "target", + "artifact": "strawberry-perl" }, "upx": { - "artifact": "upx", - "type": "target" + "type": "target", + "artifact": "upx" }, "vswhere": { + "type": "target", "artifact": "vswhere", "static-bins@windows": [ "vswhere.exe" - ], - "type": "target" - }, - "zig": { - "artifact": "zig", - "type": "target" + ] } } diff --git a/config/pkg/lib/curl.yml b/config/pkg/lib/curl.yml index 38d632ce8..f183b21e0 100644 --- a/config/pkg/lib/curl.yml +++ b/config/pkg/lib/curl.yml @@ -4,15 +4,11 @@ curl: source: type: ghrel repo: curl/curl - match: 'curl.+\.tar\.xz' + match: curl.+\.tar\.xz prefer-stable: true metadata: license-files: [COPYING] license: curl - static-libs@unix: - - libcurl.a - headers: - - curl depends@unix: - openssl - zlib @@ -31,3 +27,7 @@ curl: - CoreFoundation - CoreServices - SystemConfiguration + headers: + - curl + static-libs@unix: + - libcurl.a diff --git a/config/pkg/lib/gettext.yml b/config/pkg/lib/gettext.yml index 5b1f57a63..58f541f81 100644 --- a/config/pkg/lib/gettext.yml +++ b/config/pkg/lib/gettext.yml @@ -10,10 +10,10 @@ gettext: license: LGPL-2.1-or-later depends: - libiconv + suggests: + - ncurses + - libxml2 frameworks: - CoreFoundation static-libs@unix: - libintl.a - suggests: - - ncurses - - libxml2 diff --git a/config/pkg/lib/idn2.yml b/config/pkg/lib/idn2.yml index 5d2e20439..cef2a1496 100644 --- a/config/pkg/lib/idn2.yml +++ b/config/pkg/lib/idn2.yml @@ -11,11 +11,11 @@ idn2: depends@macos: - libiconv - gettext - headers: - - idn2.h - pkg-configs: - - libidn2 suggests@unix: - libiconv - gettext - libunistring + headers: + - idn2.h + pkg-configs: + - libidn2 diff --git a/config/pkg/lib/krb5.yml b/config/pkg/lib/krb5.yml index 305a95a18..07fa3327f 100644 --- a/config/pkg/lib/krb5.yml +++ b/config/pkg/lib/krb5.yml @@ -11,6 +11,9 @@ krb5: source-root: src depends: - openssl + suggests: + - ldap + - libedit frameworks: - Kerberos headers: @@ -18,6 +21,3 @@ krb5: - gssapi/gssapi.h pkg-configs: - krb5-gssapi - suggests: - - ldap - - libedit diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index 8ba0f8e18..db88e8b14 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -10,10 +10,10 @@ libxml2: license: MIT depends@unix: - libiconv + suggests@unix: + - xz + - zlib headers: - libxml2 pkg-configs: - libxml-2.0 - suggests@unix: - - xz - - zlib diff --git a/config/pkg/lib/nghttp2.yml b/config/pkg/lib/nghttp2.yml index 20a1840db..166c33ac5 100644 --- a/config/pkg/lib/nghttp2.yml +++ b/config/pkg/lib/nghttp2.yml @@ -11,14 +11,14 @@ nghttp2: depends: - zlib - openssl + suggests: + - libxml2 + - nghttp3 + - ngtcp2 + - brotli headers: - nghttp2 pkg-configs: - libnghttp2 static-libs@unix: - libnghttp2.a - suggests: - - libxml2 - - nghttp3 - - ngtcp2 - - brotli diff --git a/config/pkg/lib/ngtcp2.yml b/config/pkg/lib/ngtcp2.yml index a609d3ca5..c864739a7 100644 --- a/config/pkg/lib/ngtcp2.yml +++ b/config/pkg/lib/ngtcp2.yml @@ -11,6 +11,9 @@ ngtcp2: license: MIT depends: - openssl + suggests: + - nghttp3 + - brotli headers: - ngtcp2 pkg-configs: @@ -19,6 +22,3 @@ ngtcp2: static-libs@unix: - libngtcp2.a - libngtcp2_crypto_ossl.a - suggests: - - nghttp3 - - brotli diff --git a/config/pkg/target/pkg-config.yml b/config/pkg/target/pkg-config.yml new file mode 100644 index 000000000..9b6d16035 --- /dev/null +++ b/config/pkg/target/pkg-config.yml @@ -0,0 +1,9 @@ +pkg-config: + type: target + artifact: + source: 'https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz' + binary: + linux-x86_64: { type: ghrel, repo: static-php/static-php-cli-hosted, match: pkg-config-x86_64-linux-musl-1.2.5.txz, extract: { bin/pkg-config: '{pkg_root_path}/bin/pkg-config' } } + linux-aarch64: { type: ghrel, repo: static-php/static-php-cli-hosted, match: pkg-config-aarch64-linux-musl-1.2.5.txz, extract: { bin/pkg-config: '{pkg_root_path}/bin/pkg-config' } } + macos-x86_64: { type: ghrel, repo: static-php/static-php-cli-hosted, match: pkg-config-x86_64-darwin.txz, extract: { bin/pkg-config: '{pkg_root_path}/bin/pkg-config' } } + macos-aarch64: { type: ghrel, repo: static-php/static-php-cli-hosted, match: pkg-config-aarch64-darwin.txz, extract: { bin/pkg-config: '{pkg_root_path}/bin/pkg-config' } } diff --git a/config/pkg/target/zig.yml b/config/pkg/target/zig.yml new file mode 100644 index 000000000..dda72b55b --- /dev/null +++ b/config/pkg/target/zig.yml @@ -0,0 +1,4 @@ +zig: + type: target + artifact: + binary: custom diff --git a/spc.registry.json b/spc.registry.json index e8be4b5d8..b3d34939a 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -13,6 +13,7 @@ "config": [ "config/pkg.ext.json", "config/pkg/lib", + "config/pkg/target", "config/pkg.target.json" ] }, diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index 1b08bb85c..8149af14d 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -56,7 +56,7 @@ public function artifactSortKey(string $a, string $b): int public function packageSortKey(string $a, string $b): int { // sort by predefined order, other not matching keys go to the end alphabetically - $order = ['type', 'artifact']; + $order = ['type', 'artifact', 'depends', 'suggests', 'frameworks']; // Handle suffix patterns (e.g., 'depends@unix', 'static-libs@windows') $base_a = preg_replace('/@(unix|windows|macos|linux|freebsd|bsd)$/', '', $a); diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index 4d7a86bee..b64df85b2 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -27,25 +27,25 @@ class LinuxMuslCheck public static function optionalCheck(): bool { $toolchain = ApplicationContext::get(ToolchainInterface::class); - return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist(); + return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist() && !str_contains(getenv('SPC_TARGET') ?: '', 'gnu'); } /** @noinspection PhpUnused */ #[CheckItem('if musl-wrapper is installed', limit_os: 'Linux', level: 800)] - public function checkMusl(): CheckResult + public function checkMusl(): ?CheckResult { $musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m')); if (file_exists($musl_wrapper_lib) && (file_exists('/usr/local/musl/lib/libc.a') || getenv('SPC_TOOLCHAIN') === ZigToolchain::class)) { - return CheckResult::ok(); + return null; } return CheckResult::fail('musl-wrapper is not installed on your system', 'fix-musl-wrapper'); } #[CheckItem('if musl-cross-make is installed', limit_os: 'Linux', level: 799)] - public function checkMuslCrossMake(): CheckResult + public function checkMuslCrossMake(): ?CheckResult { if (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist()) { - return CheckResult::ok(); + return null; } $arch = arch2gnu(php_uname('m')); $cross_compile_lib = "/usr/local/musl/{$arch}-linux-musl/lib/libc.a"; diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 9ceb21078..2469159af 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -259,7 +259,7 @@ public function isPackageInstalled(Package|string $package_name): bool if ($this->isBuildPackage($package)) { return $package->isInstalled(); } - if ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) { + if ($package->getArtifact() !== null && $package->getArtifact()->shouldUseBinary()) { $artifact = $package->getArtifact(); return $artifact->isBinaryExtracted(); } diff --git a/src/StaticPHP/Toolchain/ClangNativeToolchain.php b/src/StaticPHP/Toolchain/ClangNativeToolchain.php index 2513dd714..27ae2b65b 100644 --- a/src/StaticPHP/Toolchain/ClangNativeToolchain.php +++ b/src/StaticPHP/Toolchain/ClangNativeToolchain.php @@ -18,10 +18,10 @@ class ClangNativeToolchain implements UnixToolchainInterface { public function initEnv(): void { - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CC=clang'); - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CXX=clang++'); - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_AR=ar'); - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_LD=ld'); + GlobalEnvManager::putenv('SPC_DEFAULT_CC=clang'); + GlobalEnvManager::putenv('SPC_DEFAULT_CXX=clang++'); + GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); } public function afterInit(): void diff --git a/src/StaticPHP/Toolchain/GccNativeToolchain.php b/src/StaticPHP/Toolchain/GccNativeToolchain.php index dbf9925ec..7c339e69e 100644 --- a/src/StaticPHP/Toolchain/GccNativeToolchain.php +++ b/src/StaticPHP/Toolchain/GccNativeToolchain.php @@ -15,10 +15,10 @@ class GccNativeToolchain implements UnixToolchainInterface { public function initEnv(): void { - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CC=gcc'); - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CXX=g++'); - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_AR=ar'); - GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_LD=ld'); + GlobalEnvManager::putenv('SPC_DEFAULT_CC=gcc'); + GlobalEnvManager::putenv('SPC_DEFAULT_CXX=g++'); + GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); } public function afterInit(): void diff --git a/src/StaticPHP/Toolchain/MuslToolchain.php b/src/StaticPHP/Toolchain/MuslToolchain.php index c7b39dbf6..6963a3645 100644 --- a/src/StaticPHP/Toolchain/MuslToolchain.php +++ b/src/StaticPHP/Toolchain/MuslToolchain.php @@ -14,10 +14,10 @@ public function initEnv(): void { $arch = getenv('GNU_ARCH'); // Set environment variables for musl toolchain - GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_CC={$arch}-linux-musl-gcc"); - GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_CXX={$arch}-linux-musl-g++"); - GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_AR={$arch}-linux-musl-ar"); - GlobalEnvManager::putenv("SPC_LINUX_DEFAULT_LD={$arch}-linux-musl-ld"); + GlobalEnvManager::putenv("SPC_DEFAULT_CC={$arch}-linux-musl-gcc"); + GlobalEnvManager::putenv("SPC_DEFAULT_CXX={$arch}-linux-musl-g++"); + GlobalEnvManager::putenv("SPC_DEFAULT_AR={$arch}-linux-musl-ar"); + GlobalEnvManager::putenv("SPC_DEFAULT_LD={$arch}-linux-musl-ld"); GlobalEnvManager::addPathIfNotExists('/usr/local/musl/bin'); GlobalEnvManager::addPathIfNotExists("/usr/local/musl/{$arch}-linux-musl/bin"); @@ -40,7 +40,7 @@ public function afterInit(): void public function getCompilerInfo(): ?string { - $compiler = getenv('CC') ?: getenv('SPC_LINUX_DEFAULT_CC'); + $compiler = getenv('CC') ?: getenv('SPC_DEFAULT_CC'); $version = shell(false)->execWithResult("{$compiler} --version", false); $head = pathinfo($compiler, PATHINFO_BASENAME); if ($version[0] === 0 && preg_match('/linux-musl-cc.*(\d+.\d+.\d+)/', $version[1][0], $match)) { diff --git a/src/StaticPHP/Util/PkgConfigUtil.php b/src/StaticPHP/Util/PkgConfigUtil.php index 5efd33de9..d5b03757f 100644 --- a/src/StaticPHP/Util/PkgConfigUtil.php +++ b/src/StaticPHP/Util/PkgConfigUtil.php @@ -28,7 +28,12 @@ public static function findPkgConfig(): ?string ]; $found = null; foreach ($find_list as $file) { - if (file_exists($file) && is_executable($file)) { + $exists = file_exists($file); + $executable = is_executable($file); + if (!$exists) { + continue; + } + if (!$executable && chmod($file, 0755) || $executable) { $found = $file; break; } From b89ff3c0835ddd55a684ecb5ebfa23149af41731 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Tue, 3 Feb 2026 19:08:19 +0800 Subject: [PATCH 184/682] Add com_dotnet extension (#1023) --- config/ext.json | 8 ++++++++ src/SPC/builder/extension/com_dotnet.php | 17 +++++++++++++++++ src/SPC/command/SwitchPhpVersionCommand.php | 6 +++--- src/globals/test-extensions.php | 16 ++++++++-------- 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 src/SPC/builder/extension/com_dotnet.php diff --git a/config/ext.json b/config/ext.json index 566974b92..4352f2e2c 100644 --- a/config/ext.json +++ b/config/ext.json @@ -43,6 +43,14 @@ "calendar": { "type": "builtin" }, + "com_dotnet": { + "support": { + "BSD": "no", + "Linux": "no", + "Darwin": "no" + }, + "type": "builtin" + }, "ctype": { "type": "builtin" }, diff --git a/src/SPC/builder/extension/com_dotnet.php b/src/SPC/builder/extension/com_dotnet.php new file mode 100644 index 000000000..7a8f6a4e4 --- /dev/null +++ b/src/SPC/builder/extension/com_dotnet.php @@ -0,0 +1,17 @@ +addArgument( 'php-major-version', InputArgument::REQUIRED, - 'PHP major version (supported: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4)', + 'PHP major version (supported: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5)', null, - fn () => ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + fn () => ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] ); $this->no_motd = true; @@ -32,7 +32,7 @@ public function configure() public function handle(): int { $php_ver = $this->input->getArgument('php-major-version'); - if (!in_array($php_ver, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'])) { + if (!in_array($php_ver, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'])) { // match x.y.z preg_match('/^\d+\.\d+\.\d+$/', $php_ver, $matches); if (!$matches) { diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index e2186c8e4..8b22658ca 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,7 +13,7 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - // '8.1', + '8.1', '8.2', '8.3', '8.4', @@ -23,15 +23,15 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-15-intel', // bin/spc for x86_64 - 'macos-15', // bin/spc for arm64 - 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - 'ubuntu-24.04', // bin/spc for x86_64 + // 'macos-15-intel', // bin/spc for x86_64 + // 'macos-15', // bin/spc for arm64 + // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 + // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + // 'ubuntu-24.04', // bin/spc for x86_64 // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - // 'windows-2025', + 'windows-2025', ]; // whether enable thread safe @@ -51,7 +51,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { 'Linux', 'Darwin' => 'mysqli,gmp', - 'Windows' => 'bcmath', + 'Windows' => 'com_dotnet', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). From 0d4cde79fac478a460c66fe181c8aa32c65d3546 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 20:23:32 +0800 Subject: [PATCH 185/682] Add download options for build:libs command --- src/StaticPHP/Command/BuildLibsCommand.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index a415ca3e1..a637fe7ce 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -4,6 +4,7 @@ namespace StaticPHP\Command; +use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Package\PackageInstaller; use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Attribute\AsCommand; @@ -23,6 +24,8 @@ public function configure(): void new InputOption('no-download', null, null, 'Skip downloading artifacts (use existing cached files)'), ...V2CompatLayer::getLegacyBuildOptions(), ]); + // Downloader options (with 'dl-' prefix to avoid conflicts) + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions('dl')); } public function handle(): int From a02f287d9706cffce5d194c19d20d0daf4c0a452 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 3 Feb 2026 20:23:59 +0800 Subject: [PATCH 186/682] Fix macOS wrong patch file directory --- src/Package/Library/curl.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Package/Library/curl.php b/src/Package/Library/curl.php index a2856ad4b..283c765a1 100644 --- a/src/Package/Library/curl.php +++ b/src/Package/Library/curl.php @@ -22,9 +22,9 @@ public function patchBeforeBuild(LibraryPackage $lib): bool { shell()->cd($lib->getSourceDir())->exec('sed -i.save s@\${CMAKE_C_IMPLICIT_LINK_LIBRARIES}@@ ./CMakeLists.txt'); if (SystemTarget::getTargetOS() === 'Darwin') { - FileSystem::replaceFileRegex("{$lib->getSourceDir()}/curl/CMakeLists.txt", '/NOT COREFOUNDATION_FRAMEWORK/m', 'FALSE'); - FileSystem::replaceFileRegex("{$lib->getSourceDir()}/curl/CMakeLists.txt", '/NOT SYSTEMCONFIGURATION_FRAMEWORK/m', 'FALSE'); - FileSystem::replaceFileRegex("{$lib->getSourceDir()}/curl/CMakeLists.txt", '/NOT CORESERVICES_FRAMEWORK/m', 'FALSE'); + FileSystem::replaceFileRegex("{$lib->getSourceDir()}/CMakeLists.txt", '/NOT COREFOUNDATION_FRAMEWORK/m', 'FALSE'); + FileSystem::replaceFileRegex("{$lib->getSourceDir()}/CMakeLists.txt", '/NOT SYSTEMCONFIGURATION_FRAMEWORK/m', 'FALSE'); + FileSystem::replaceFileRegex("{$lib->getSourceDir()}/CMakeLists.txt", '/NOT CORESERVICES_FRAMEWORK/m', 'FALSE'); } return true; } From b5c14d6f26555ac7695687c34475080429a393ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:12:19 +0800 Subject: [PATCH 187/682] Fix golang download website hash match pattern --- src/Package/Artifact/go_xcaddy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 293f7c62b..5fa7327e3 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -47,7 +47,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult } $version_regex = str_replace('.', '\.', $version); - $pattern = "/href=\"\\/dl\\/{$version_regex}\\.{$os}-{$arch}\\.tar\\.gz\">.*?([a-f0-9]{64})<\\/tt>/s"; + $pattern = "/class=\"download\" href=\"\\/dl\\/{$version_regex}\\.{$os}-{$arch}\\.tar\\.gz\">.*?([a-f0-9]{64})<\\/tt>/s"; if (preg_match($pattern, $page, $matches)) { $hash = $matches[1]; } else { From c40eaeef5d82768e205fb4934db672fc72d71020 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:14:30 +0800 Subject: [PATCH 188/682] Fix custom artifact binary download `is-installed` check --- src/StaticPHP/Artifact/Artifact.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index b3e27371f..dc602538b 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -131,7 +131,9 @@ public function isSourceExtracted(bool $compare_hash = false): bool public function isBinaryExtracted(?string $target_os = null, bool $compare_hash = false): bool { $target_os = $target_os ?? SystemTarget::getCurrentPlatformString(); - $extract_config = $this->getBinaryExtractConfig(); + // Get cache info first for custom binary support (extract path may be stored in cache) + $cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os); + $extract_config = $this->getBinaryExtractConfig($cache_info ?? []); $mode = $extract_config['mode']; // For merge mode, check marker file From 08d20205a70349e0ff9f9e0269724f01bd4e03a2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:14:44 +0800 Subject: [PATCH 189/682] Allow all artifact configs --- src/StaticPHP/Config/ArtifactConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index 49bfec233..83f079202 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -18,7 +18,7 @@ public static function loadFromDir(string $dir, string $registry_name): array throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config."); } $loaded = []; - $files = glob("{$dir}/artifact.*.json"); + $files = glob("{$dir}/*"); if (is_array($files)) { foreach ($files as $file) { self::loadFromFile($file, $registry_name); From e2011e1c18e39cda7153408d4bed9ec9cf31e48c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:14:57 +0800 Subject: [PATCH 190/682] Verbose message --- src/StaticPHP/Artifact/ArtifactDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b07c56617..27cd6adc5 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -438,7 +438,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, break; } } - $vvv = !ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, or disable parallel downloading for more details." : ''; + $vvv = !ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-v`, `-vv` or `-vvv` to enable verbose mode, or disable parallel downloading for more details." : ''; throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}"); } From 0afa1dd80c2395af3ab2f3b138216f836847c728 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:15:20 +0800 Subject: [PATCH 191/682] Use new brand name --- config/env.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/env.ini b/config/env.ini index c77f6bcd0..9e295d796 100644 --- a/config/env.ini +++ b/config/env.ini @@ -48,7 +48,7 @@ SPC_SKIP_DOCTOR_CHECK_ITEMS="" ; extra modules that xcaddy will include in the FrankenPHP build SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" ; The display message for php version output (PHP >= 8.4 available) -PHP_BUILD_PROVIDER="static-php-cli ${SPC_VERSION}" +PHP_BUILD_PROVIDER="StaticPHP ${SPC_VERSION}" ; Whether to enable log file (if you are using vendor mode) SPC_ENABLE_LOG_FILE="yes" ; The LOG DIR for spc logs From e9c27dee1fe44029b16d78aca2d8aa4d63f903fe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:28:10 +0800 Subject: [PATCH 192/682] Add go-xcaddy,musl-toolchain,php,upx, and also glfw linux support --- config/artifact/glfw.yml | 9 +++ config/artifact/php-src.yml | 7 ++ config/pkg.ext.json | 21 ------ config/pkg.target.json | 66 ---------------- config/pkg/ext/ext-glfw.yml | 5 ++ config/pkg/lib/glfw.yml | 6 ++ config/pkg/target/go-xcaddy.yml | 6 ++ config/pkg/target/musl-toolchain.yml | 6 ++ config/pkg/target/php.yml | 44 +++++++++++ config/pkg/target/upx.yml | 7 ++ spc.registry.json | 8 +- src/Package/Artifact/php_src.php | 75 +++++++++++++++++++ src/Package/Extension/glfw.php | 59 +++++++++++++++ src/Package/Library/glfw.php | 100 +++++++++++++++++++++++++ src/Package/Target/php/unix.php | 42 +++++++++-- src/StaticPHP/Doctor/Item/ZigCheck.php | 6 +- 16 files changed, 365 insertions(+), 102 deletions(-) create mode 100644 config/artifact/glfw.yml create mode 100644 config/artifact/php-src.yml create mode 100644 config/pkg/ext/ext-glfw.yml create mode 100644 config/pkg/lib/glfw.yml create mode 100644 config/pkg/target/go-xcaddy.yml create mode 100644 config/pkg/target/musl-toolchain.yml create mode 100644 config/pkg/target/php.yml create mode 100644 config/pkg/target/upx.yml create mode 100644 src/Package/Artifact/php_src.php create mode 100644 src/Package/Extension/glfw.php create mode 100644 src/Package/Library/glfw.php diff --git a/config/artifact/glfw.yml b/config/artifact/glfw.yml new file mode 100644 index 000000000..05e9f18d4 --- /dev/null +++ b/config/artifact/glfw.yml @@ -0,0 +1,9 @@ +glfw: + metadata: + license-files: + - LICENSE + license: MIT + source: + type: git + url: 'https://github.com/mario-deluna/php-glfw' + rev: master diff --git a/config/artifact/php-src.yml b/config/artifact/php-src.yml new file mode 100644 index 000000000..32bcb6cf0 --- /dev/null +++ b/config/artifact/php-src.yml @@ -0,0 +1,7 @@ +php-src: + metadata: + license-files: + - LICENSE + license: PHP-3.01 + source: + type: php-release diff --git a/config/pkg.ext.json b/config/pkg.ext.json index d1ca0b3f0..cbcf5ad58 100644 --- a/config/pkg.ext.json +++ b/config/pkg.ext.json @@ -244,27 +244,6 @@ "arg-type": "with-path" } }, - "ext-glfw": { - "type": "php-extension", - "artifact": "ext-glfw", - "depends": [ - "glfw" - ], - "depends@windows": [], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "no", - "Linux": "no" - }, - "arg-type": "custom", - "notes": true - } - }, "ext-gmp": { "type": "php-extension", "depends": [ diff --git a/config/pkg.target.json b/config/pkg.target.json index a5c20b78c..59c6c65ac 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -1,70 +1,8 @@ { - "frankenphp": { - "type": "virtual-target", - "artifact": "frankenphp", - "depends": [ - "php-embed", - "go-xcaddy" - ], - "depends@macos": [ - "php-embed", - "go-xcaddy", - "libxml2" - ] - }, - "go-xcaddy": { - "type": "target", - "artifact": "go-xcaddy", - "static-bins": [ - "xcaddy" - ] - }, - "musl-toolchain": { - "type": "target", - "artifact": "musl-toolchain" - }, "nasm": { "type": "target", "artifact": "nasm" }, - "php": { - "type": "target", - "artifact": "php-src", - "depends@macos": [ - "libxml2" - ] - }, - "php-cgi": { - "type": "virtual-target", - "depends": [ - "php" - ] - }, - "php-cli": { - "type": "virtual-target", - "depends": [ - "php" - ] - }, - "php-embed": { - "type": "virtual-target", - "depends": [ - "php" - ] - }, - "php-fpm": { - "type": "virtual-target", - "depends": [ - "php" - ] - }, - "php-micro": { - "type": "virtual-target", - "artifact": "micro", - "depends": [ - "php" - ] - }, "php-sdk-binary-tools": { "type": "target", "artifact": "php-sdk-binary-tools" @@ -73,10 +11,6 @@ "type": "target", "artifact": "strawberry-perl" }, - "upx": { - "type": "target", - "artifact": "upx" - }, "vswhere": { "type": "target", "artifact": "vswhere", diff --git a/config/pkg/ext/ext-glfw.yml b/config/pkg/ext/ext-glfw.yml new file mode 100644 index 000000000..dc8844a8f --- /dev/null +++ b/config/pkg/ext/ext-glfw.yml @@ -0,0 +1,5 @@ +ext-glfw: + type: php-extension + artifact: glfw + depends: + - glfw diff --git a/config/pkg/lib/glfw.yml b/config/pkg/lib/glfw.yml new file mode 100644 index 000000000..13fba596e --- /dev/null +++ b/config/pkg/lib/glfw.yml @@ -0,0 +1,6 @@ +glfw: + type: library + artifact: glfw + lang: cpp + static-libs@unix: + - libglfw3.a diff --git a/config/pkg/target/go-xcaddy.yml b/config/pkg/target/go-xcaddy.yml new file mode 100644 index 000000000..89cb4cd02 --- /dev/null +++ b/config/pkg/target/go-xcaddy.yml @@ -0,0 +1,6 @@ +go-xcaddy: + type: target + artifact: + binary: custom + static-bins: + - xcaddy diff --git a/config/pkg/target/musl-toolchain.yml b/config/pkg/target/musl-toolchain.yml new file mode 100644 index 000000000..038059e27 --- /dev/null +++ b/config/pkg/target/musl-toolchain.yml @@ -0,0 +1,6 @@ +musl-toolchain: + type: target + artifact: + binary: + linux-x86_64: { type: url, url: 'https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz', extract: '{pkg_root_path}/musl-toolchain' } + linux-aarch64: { type: url, url: 'https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz', extract: '{pkg_root_path}/musl-toolchain' } diff --git a/config/pkg/target/php.yml b/config/pkg/target/php.yml new file mode 100644 index 000000000..8f88cb581 --- /dev/null +++ b/config/pkg/target/php.yml @@ -0,0 +1,44 @@ +frankenphp: + type: virtual-target + artifact: + source: + type: ghtar + repo: php/frankenphp + prefer-stable: true + metadata: + license-files: [LICENSE] + license: MIT + depends: + - php-embed + - go-xcaddy +php: + type: target + artifact: php-src + depends@macos: + - libxml2 +php-cgi: + type: virtual-target + depends: + - php +php-cli: + type: virtual-target + depends: + - php +php-embed: + type: virtual-target + depends: + - php +php-fpm: + type: virtual-target + depends: + - php +php-micro: + type: virtual-target + artifact: + source: + type: git + extract: php-src/sapi/micro + rev: master + url: 'https://github.com/static-php/phpmicro' + depends: + - php diff --git a/config/pkg/target/upx.yml b/config/pkg/target/upx.yml new file mode 100644 index 000000000..29506c39a --- /dev/null +++ b/config/pkg/target/upx.yml @@ -0,0 +1,7 @@ +upx: + type: target + artifact: + binary: + linux-x86_64: { type: ghrel, repo: upx/upx, match: upx.+-amd64_linux\.tar\.xz, extract: { upx: '{pkg_root_path}/bin/upx' } } + linux-aarch64: { type: ghrel, repo: upx/upx, match: upx.+-arm64_linux\.tar\.xz, extract: { upx: '{pkg_root_path}/bin/upx' } } + windows-x86_64: { type: ghrel, repo: upx/upx, match: upx.+-win64\.zip, extract: { upx.exe: '{pkg_root_path}/bin/upx.exe' } } diff --git a/spc.registry.json b/spc.registry.json index b3d34939a..b731249ba 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -12,12 +12,16 @@ }, "config": [ "config/pkg.ext.json", - "config/pkg/lib", - "config/pkg/target", + "config/pkg/lib/", + "config/pkg/target/", + "config/pkg/ext/", "config/pkg.target.json" ] }, "artifact": { + "config": [ + "config/artifact/" + ], "psr-4": { "Package\\Artifact": "src/Package/Artifact" } diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php new file mode 100644 index 000000000..498abc66e --- /dev/null +++ b/src/Package/Artifact/php_src.php @@ -0,0 +1,75 @@ += 80100 ? file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32') : file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32'); + file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32.bak', file_get_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32')); + file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32', $origin); + } + } + + #[AfterSourceExtract('php-src')] + #[PatchDescription('Patch FFI extension on CentOS 7 with -O3 optimization (strncmp issue)')] + public function patchFfiCentos7FixO3strncmp(): void + { + spc_skip_if(!($ver = SystemTarget::getLibcVersion()) || version_compare($ver, '2.17', '>')); + spc_skip_if(!file_exists(SOURCE_PATH . '/php-src/main/php_version.h')); + $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); + spc_skip_if(preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0 && intval($match[1]) < 80316); + SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); + } + + #[AfterSourceExtract('php-src')] + #[PatchDescription('Add LICENSE file to IMAP extension if missing')] + public function patchImapLicense(): void + { + if (!file_exists(SOURCE_PATH . '/php-src/ext/imap/LICENSE') && is_dir(SOURCE_PATH . '/php-src/ext/imap')) { + file_put_contents(SOURCE_PATH . '/php-src/ext/imap/LICENSE', file_get_contents(ROOT_DIR . '/src/globals/extra/Apache_LICENSE')); + } + } +} diff --git a/src/Package/Extension/glfw.php b/src/Package/Extension/glfw.php new file mode 100644 index 000000000..61d722e14 --- /dev/null +++ b/src/Package/Extension/glfw.php @@ -0,0 +1,59 @@ +getSourceDir(), SOURCE_PATH . '/php-src/ext/glfw'); + } + } + + #[BeforeStage('php', [php::class, 'configureForUnix'], 'ext-glfw')] + #[PatchDescription('Patch glfw extension before configure')] + public function patchBeforeConfigure(): void + { + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/configure', + '-lglfw ', + '-lglfw3 ' + ); + + // add X11 shared libs for linux + if (SystemTarget::getTargetOS() === 'Linux') { + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + $extra_libs .= ' -lX11 -lXrandr -lXinerama -lXcursor -lXi'; + putenv('SPC_EXTRA_LIBS=' . trim($extra_libs)); + $extra_cflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + + $extra_cflags .= ' -idirafter /usr/include'; + putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags)); + $extra_ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; + $extra_ldflags .= ' -L/usr/lib/' . SystemTarget::getTargetArch() . '-linux-gnu '; + putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=' . $extra_ldflags); + } + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared = false): string + { + return '--enable-glfw --with-glfw-dir=' . BUILD_ROOT_PATH; + } +} diff --git a/src/Package/Library/glfw.php b/src/Package/Library/glfw.php new file mode 100644 index 000000000..9348489cd --- /dev/null +++ b/src/Package/Library/glfw.php @@ -0,0 +1,100 @@ +isStatic()) { + throw new ValidationException('glfw library does not support full-static linking on Linux, please build with dynamic target specified.'); + } + // detect X11 dev packages + $required_headers = [ + '/usr/include/X11', + '/usr/include/X11/extensions/Xrandr.h', + '/usr/include/X11/extensions/Xinerama.h', + '/usr/include/X11/Xcursor/Xcursor.h', + ]; + foreach ($required_headers as $header) { + if (!file_exists($header)) { + throw new ValidationException("glfw requires X11 development headers. Missing: {$header}. Please confirm that your system has the necessary X11 packages installed."); + } + } + } + } + + #[BuildFor('Linux')] + public function buildForLinux(LibraryPackage $lib): void + { + $x11_lib_find = [ + '/usr/lib/' . SystemTarget::getTargetArch() . '-linux-gnu/libX11.so', + '/usr/lib64/libX11.so', + '/usr/lib/libX11.so', + ]; + $found = false; + foreach ($x11_lib_find as $path) { + if (file_exists($path)) { + $found = $path; + break; + } + } + if (!$found) { + throw new BuildFailureException('Cannot find X11 library files in standard locations. Please ensure that the X11 development libraries are installed.'); + } + $base_path = pathinfo($found, PATHINFO_DIRNAME); + UnixCMakeExecutor::create($lib) + ->setBuildDir("{$lib->getSourceDir()}/vendor/glfw") + ->setReset(false) + ->addConfigureArgs( + '-DGLFW_BUILD_EXAMPLES=OFF', + '-DGLFW_BUILD_TESTS=OFF', + '-DGLFW_BUILD_DOCS=OFF', + '-DX11_X11_INCLUDE_PATH=/usr/include', + '-DX11_Xrandr_INCLUDE_PATH=/usr/include/X11/extensions', + '-DX11_Xinerama_INCLUDE_PATH=/usr/include/X11/extensions', + '-DX11_Xkb_INCLUDE_PATH=/usr/include/X11', + '-DX11_Xcursor_INCLUDE_PATH=/usr/include/X11/Xcursor', + '-DX11_Xi_INCLUDE_PATH=/usr/include/X11/extensions', + "-DX11_X11_LIB={$base_path}/libX11.so", + "-DX11_Xrandr_LIB={$base_path}/libXrandr.so", + "-DX11_Xinerama_LIB={$base_path}/libXinerama.so", + "-DX11_Xcursor_LIB={$base_path}/libXcursor.so", + "-DX11_Xi_LIB={$base_path}/libXi.so" + ) + ->build('.'); + // patch pkgconf + $lib->patchPkgconfPrefix(['glfw3.pc']); + } + + #[BuildFor('Darwin')] + public function buildForMac(LibraryPackage $lib): void + { + UnixCMakeExecutor::create($lib) + ->setBuildDir("{$lib->getSourceDir()}/vendor/glfw") + ->setReset(false) + ->addConfigureArgs( + '-DGLFW_BUILD_EXAMPLES=OFF', + '-DGLFW_BUILD_TESTS=OFF', + '-DGLFW_BUILD_DOCS=OFF', + ) + ->build('.'); + // patch pkgconf + $lib->patchPkgconfPrefix(['glfw3.pc']); + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 1ce2ff95a..edbc6dd23 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -134,9 +134,11 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); $concurrency = $builder->concurrency; + $vars = $this->makeVars($installer); + $makeArgs = $this->makeVarsToArgs($vars); shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("make -j{$concurrency} cli"); + ->setEnv($vars) + ->exec("make -j{$concurrency} {$makeArgs} cli"); $builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php'); $package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php'); @@ -147,9 +149,11 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); $concurrency = $builder->concurrency; + $vars = $this->makeVars($installer); + $makeArgs = $this->makeVarsToArgs($vars); shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("make -j{$concurrency} cgi"); + ->setEnv($vars) + ->exec("make -j{$concurrency} {$makeArgs} 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'); @@ -160,9 +164,11 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); $concurrency = $builder->concurrency; + $vars = $this->makeVars($installer); + $makeArgs = $this->makeVarsToArgs($vars); shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) - ->exec("make -j{$concurrency} fpm"); + ->setEnv($vars) + ->exec("make -j{$concurrency} {$makeArgs} 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'); @@ -182,10 +188,11 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta // apply --with-micro-fake-cli option $vars = $this->makeVars($installer); $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $makeArgs = $this->makeVarsToArgs($vars); // build shell()->cd($package->getSourceDir()) ->setEnv($vars) - ->exec("make -j{$builder->concurrency} micro"); + ->exec("make -j{$builder->concurrency} {$makeArgs} 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'); @@ -440,11 +447,30 @@ private function makeVars(PackageInstaller $installer): array $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; + // Append SPC_EXTRA_LIBS to libs for dynamic linking support (e.g., X11) + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + $libs = trim($config['libs'] . ' ' . $extra_libs); + 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'], + 'EXTRA_LIBS' => $libs, ]); } + + /** + * Convert make variables array to command line argument string. + * This is needed because make command line arguments have higher priority than environment variables. + */ + private function makeVarsToArgs(array $vars): string + { + $args = []; + foreach ($vars as $key => $value) { + if (trim($value) !== '') { + $args[] = $key . '=' . escapeshellarg($value); + } + } + return implode(' ', $args); + } } diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index 4157e9d60..c3a6aa9f3 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -25,11 +25,7 @@ public static function optionalCheck(): bool #[CheckItem('if zig is installed', level: 800)] public function checkZig(): CheckResult { - $installer = new PackageInstaller(); - $package = 'zig'; - $installer->addInstallPackage($package); - $installed = $installer->isPackageInstalled($package); - if ($installed) { + if (new PackageInstaller()->addInstallPackage('zig')->isPackageInstalled('zig')) { return CheckResult::ok(); } return CheckResult::fail('zig is not installed', 'install-zig'); From 0652d4aa034550b512560493ea207e5fe2f450ba Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 15:31:05 +0800 Subject: [PATCH 193/682] Just in case source dir have not been created --- src/StaticPHP/Package/PackageInstaller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 2469159af..7fff8a144 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -325,6 +325,7 @@ public function getArtifacts(): array */ public function extractSourceArtifacts(bool $interactive = true): void { + FileSystem::createDir(SOURCE_PATH); $packages = array_values($this->packages); $cache = ApplicationContext::get(ArtifactCache::class); From 3fa2d69813662e495af801e9f3b51cee87c6634a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 16:24:57 +0800 Subject: [PATCH 194/682] Add ext-mbstring,ext-mbregex,onig --- config/pkg/ext/ext-mbregex.yml | 9 +++++++++ config/pkg/ext/ext-mbstring.yml | 4 ++++ config/pkg/lib/onig.yml | 15 +++++++++++++++ src/Package/Extension/mbregex.php | 19 +++++++++++++++++++ src/Package/Extension/mbstring.php | 22 ++++++++++++++++++++++ src/Package/Library/onig.php | 9 +++++++++ 6 files changed, 78 insertions(+) create mode 100644 config/pkg/ext/ext-mbregex.yml create mode 100644 config/pkg/ext/ext-mbstring.yml create mode 100644 config/pkg/lib/onig.yml create mode 100644 src/Package/Extension/mbregex.php create mode 100644 src/Package/Extension/mbstring.php diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml new file mode 100644 index 000000000..eaa0481a4 --- /dev/null +++ b/config/pkg/ext/ext-mbregex.yml @@ -0,0 +1,9 @@ +ext-mbregex: + type: php-extension + depends: + - onig + - ext-mbstring + php-extension: + arg-type: custom + build-shared: false + build-static: true diff --git a/config/pkg/ext/ext-mbstring.yml b/config/pkg/ext/ext-mbstring.yml new file mode 100644 index 000000000..6583ca616 --- /dev/null +++ b/config/pkg/ext/ext-mbstring.yml @@ -0,0 +1,4 @@ +ext-mbstring: + type: php-extension + php-extension: + arg-type: custom diff --git a/config/pkg/lib/onig.yml b/config/pkg/lib/onig.yml new file mode 100644 index 000000000..fa06524dc --- /dev/null +++ b/config/pkg/lib/onig.yml @@ -0,0 +1,15 @@ +onig: + type: library + artifact: + source: + type: ghrel + repo: kkos/oniguruma + match: onig-.+\.tar\.gz + metadata: + license-files: [COPYING] + license: Custom + headers: + - oniggnu.h + - oniguruma.h + static-libs@unix: + - libonig.a diff --git a/src/Package/Extension/mbregex.php b/src/Package/Extension/mbregex.php new file mode 100644 index 000000000..f01c7c787 --- /dev/null +++ b/src/Package/Extension/mbregex.php @@ -0,0 +1,19 @@ +isPackageResolved('ext-mbregex') === false ? ' --disable-mbregex' : ' --enable-mbregex'; + return $arg; + } +} diff --git a/src/Package/Library/onig.php b/src/Package/Library/onig.php index 2cea572bf..af289636f 100644 --- a/src/Package/Library/onig.php +++ b/src/Package/Library/onig.php @@ -7,6 +7,7 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Util\FileSystem; @@ -21,4 +22,12 @@ public function buildWin(LibraryPackage $package): void ->build(); FileSystem::copy("{$package->getLibDir()}\\onig.lib", "{$package->getLibDir()}\\onig_a.lib"); } + + #[BuildFor('Linux')] + #[BuildFor('Darwin')] + public function buildUnix(LibraryPackage $lib): void + { + UnixAutoconfExecutor::create($lib)->configure()->make(); + $lib->patchPkgconfPrefix(['oniguruma.pc']); + } } From 16f94466fdef21f720fdeea58878214dd76bbe89 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 16:25:23 +0800 Subject: [PATCH 195/682] Add artifact name suggestions for download and install commands --- src/StaticPHP/Command/DownloadCommand.php | 14 +++++++++++++- .../Command/InstallPackageCommand.php | 19 +++++++++++++++++-- src/StaticPHP/Registry/ArtifactLoader.php | 11 +++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 277585e51..270f55385 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -6,11 +6,13 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\DownloaderOptions; +use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -19,7 +21,17 @@ class DownloadCommand extends BaseCommand { public function configure(): void { - $this->addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to download, comma separated, e.g "php-src,openssl,curl"'); + $this->addArgument( + 'artifacts', + InputArgument::OPTIONAL, + 'Specific artifacts to download, comma separated, e.g "php-src,openssl,curl"', + suggestedValues: function (CompletionInput $input) { + $input_val = $input->getCompletionValue(); + $all_names = ArtifactLoader::getLoadedArtifactNames(); + // filter by input value + return array_filter($all_names, fn ($name) => str_starts_with($name, $input_val)); + }, + ); // 2.x compatible options $this->addOption('shallow-clone', null, null, '(deprecated) Clone shallowly repositories when downloading sources'); diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 89814f013..230322618 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -6,14 +6,29 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Registry\PackageLoader; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Input\InputArgument; #[AsCommand('install-pkg', 'Install additional package', ['i', 'install-package'])] class InstallPackageCommand extends BaseCommand { - public function configure() + public function configure(): void { - $this->addArgument('package', null, 'The package to install (name or path)'); + $this->addArgument( + 'package', + InputArgument::REQUIRED, + 'The package to install (name or path)', + suggestedValues: function (CompletionInput $input) { + $packages = []; + foreach (PackageLoader::getPackages(['target', 'virtual-target']) as $name => $_) { + $packages[] = $name; + } + $val = $input->getCompletionValue(); + return array_filter($packages, fn ($name) => str_starts_with($name, $val)); + } + ); } public function handle(): int diff --git a/src/StaticPHP/Registry/ArtifactLoader.php b/src/StaticPHP/Registry/ArtifactLoader.php index 22942452f..d53d9be04 100644 --- a/src/StaticPHP/Registry/ArtifactLoader.php +++ b/src/StaticPHP/Registry/ArtifactLoader.php @@ -69,6 +69,17 @@ public static function loadFromClass(string $class): void } } + /** + * Get names of all loaded artifacts. + * + * @return string[] + */ + public static function getLoadedArtifactNames(): array + { + self::initArtifactInstances(); + return array_keys(self::$artifacts ?? []); + } + /** * Process #[CustomSource] attribute. */ From b9af9ba05677186c24473f9230642f716f7a7d22 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Feb 2026 16:25:34 +0800 Subject: [PATCH 196/682] Chore --- src/StaticPHP/Package/PackageBuilder.php | 3 +++ src/StaticPHP/Package/PackageInstaller.php | 3 +-- src/bootstrap.php | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 94c55a66e..42bf5c6f7 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -100,6 +100,9 @@ public function deployBinary(string $src, string $dst, bool $executable = true): // ignore copy to self if (realpath($src) !== realpath($dst)) { FileSystem::copy($src, $dst); + if ($executable) { + chmod($dst, 0755); + } } // file exist diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 7fff8a144..b9a3debb7 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -526,8 +526,7 @@ private function handlePhpTargetPackage(TargetPackage $package): void { // process 'php' target if ($package->getName() === 'php') { - logger()->warning("Building 'php' target is deprecated, please use specific targets like 'build:php-cli' instead."); - + // logger()->warning("Building 'php' target is deprecated, please use specific targets like 'build:php-cli' instead."); $added = false; if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) { diff --git a/src/bootstrap.php b/src/bootstrap.php index 6af814e8f..15e30b593 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -58,7 +58,7 @@ }); } -// load internal registry +// load core registry Registry::loadRegistry(ROOT_DIR . '/spc.registry.json'); // load registries from environment variable SPC_REGISTRIES Registry::loadFromEnvOrOption(); From 4d4b1a334fe050fa3f46af7da74fb40928052d19 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 16:10:16 +0800 Subject: [PATCH 197/682] Add ext-readline,freetype,gmssl,grpc,icu --- config/pkg/ext/ext-mbregex.yml | 1 + config/pkg/ext/ext-readline.yml | 11 ++++++ config/pkg/lib/attr.yml | 8 +--- config/pkg/lib/freetype.yml | 22 +++++++++++ config/pkg/lib/gmssl.yml | 15 +++++++ config/pkg/lib/grpc.yml | 19 +++++++++ config/pkg/lib/icu.yml | 16 ++++++++ spc.registry.json | 4 +- src/Package/Library/freetype.php | 36 +++++++++++++++++ src/Package/Library/gmssl.php | 21 ++++++++++ src/Package/Library/grpc.php | 68 ++++++++++++++++++++++++++++++++ src/Package/Library/icu.php | 66 +++++++++++++++++++++++++++++++ 12 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 config/pkg/ext/ext-readline.yml create mode 100644 config/pkg/lib/freetype.yml create mode 100644 config/pkg/lib/gmssl.yml create mode 100644 config/pkg/lib/grpc.yml create mode 100644 config/pkg/lib/icu.yml create mode 100644 src/Package/Library/freetype.php create mode 100644 src/Package/Library/gmssl.php create mode 100644 src/Package/Library/grpc.php create mode 100644 src/Package/Library/icu.php diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml index eaa0481a4..ae59f0235 100644 --- a/config/pkg/ext/ext-mbregex.yml +++ b/config/pkg/ext/ext-mbregex.yml @@ -7,3 +7,4 @@ ext-mbregex: arg-type: custom build-shared: false build-static: true + display-name: mbstring diff --git a/config/pkg/ext/ext-readline.yml b/config/pkg/ext/ext-readline.yml new file mode 100644 index 000000000..19b1886c8 --- /dev/null +++ b/config/pkg/ext/ext-readline.yml @@ -0,0 +1,11 @@ +ext-readline: + type: php-extension + depends: + - libedit + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path + build-shared: false + build-static: true diff --git a/config/pkg/lib/attr.yml b/config/pkg/lib/attr.yml index 289bcd67b..cbf181c9d 100644 --- a/config/pkg/lib/attr.yml +++ b/config/pkg/lib/attr.yml @@ -1,12 +1,8 @@ attr: type: library artifact: - source: - type: url - url: 'https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz' - source-mirror: - type: url - url: 'https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz' + source: 'https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz' + source-mirror: 'https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz' metadata: license-files: [doc/COPYING.LGPL] license: LGPL-2.1-or-later diff --git a/config/pkg/lib/freetype.yml b/config/pkg/lib/freetype.yml new file mode 100644 index 000000000..c101a174b --- /dev/null +++ b/config/pkg/lib/freetype.yml @@ -0,0 +1,22 @@ +freetype: + type: library + artifact: + source: + type: ghtagtar + repo: freetype/freetype + match: VER-2-\d+-\d+ + metadata: + license-files: [LICENSE.TXT] + license: FTL + depends: + - zlib + suggests: + - bzip2 + - brotli + headers@unix: + - freetype2/freetype/freetype.h + - freetype2/ft2build.h + static-libs@unix: + - libfreetype.a + static-libs@windows: + - libfreetype_a.lib diff --git a/config/pkg/lib/gmssl.yml b/config/pkg/lib/gmssl.yml new file mode 100644 index 000000000..076234353 --- /dev/null +++ b/config/pkg/lib/gmssl.yml @@ -0,0 +1,15 @@ +gmssl: + type: library + artifact: + source: + type: ghtar + repo: guanzhi/GmSSL + metadata: + license-files: [LICENSE] + license: Apache-2.0 + frameworks: + - Security + static-libs@unix: + - libgmssl.a + static-libs@windows: + - gmssl.lib diff --git a/config/pkg/lib/grpc.yml b/config/pkg/lib/grpc.yml new file mode 100644 index 000000000..feb6068bb --- /dev/null +++ b/config/pkg/lib/grpc.yml @@ -0,0 +1,19 @@ +grpc: + type: library + artifact: + source: + type: git + rev: v1.75.x + url: 'https://github.com/grpc/grpc.git' + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - zlib + - openssl + - libcares + frameworks: + - CoreFoundation + lang: cpp + pkg-configs: + - grpc diff --git a/config/pkg/lib/icu.yml b/config/pkg/lib/icu.yml new file mode 100644 index 000000000..43328228d --- /dev/null +++ b/config/pkg/lib/icu.yml @@ -0,0 +1,16 @@ +icu: + type: library + artifact: + source: + type: ghrel + repo: unicode-org/icu + match: icu4c.+-src\.tgz + prefer-stable: true + metadata: + license-files: [LICENSE] + license: ICU + lang: cpp + pkg-configs: + - icu-uc + - icu-i18n + - icu-io diff --git a/spc.registry.json b/spc.registry.json index b731249ba..cf49c6c83 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -1,6 +1,9 @@ { "name": "core", "autoload": "vendor/autoload.php", + "scripts": [ + "ext/" + ], "doctor": { "psr-4": { "StaticPHP\\Doctor\\Item": "src/StaticPHP/Doctor/Item" @@ -11,7 +14,6 @@ "Package": "src/Package" }, "config": [ - "config/pkg.ext.json", "config/pkg/lib/", "config/pkg/target/", "config/pkg/ext/", diff --git a/src/Package/Library/freetype.php b/src/Package/Library/freetype.php new file mode 100644 index 000000000..6cb05a90a --- /dev/null +++ b/src/Package/Library/freetype.php @@ -0,0 +1,36 @@ +optionalPackage('libpng', ...cmake_boolean_args('FT_DISABLE_PNG', true)) + ->optionalPackage('bzip2', ...cmake_boolean_args('FT_DISABLE_BZIP2', true)) + ->optionalPackage('brotli', ...cmake_boolean_args('FT_DISABLE_BROTLI', true)) + ->addConfigureArgs('-DFT_DISABLE_HARFBUZZ=ON'); + + // fix cmake 4.0 compatibility + if (version_compare(get_cmake_version(), '4.0.0', '>=')) { + $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.12'); + } + + $cmake->build(); + + $lib->patchPkgconfPrefix(['freetype2.pc']); + FileSystem::replaceFileStr("{$lib->getBuildRootPath()}/lib/pkgconfig/freetype2.pc", ' -L/lib ', " -L{$lib->getBuildRootPath()}/lib "); + } +} diff --git a/src/Package/Library/gmssl.php b/src/Package/Library/gmssl.php new file mode 100644 index 000000000..21b4ea668 --- /dev/null +++ b/src/Package/Library/gmssl.php @@ -0,0 +1,21 @@ +build(); + } +} diff --git a/src/Package/Library/grpc.php b/src/Package/Library/grpc.php new file mode 100644 index 000000000..86cddcc06 --- /dev/null +++ b/src/Package/Library/grpc.php @@ -0,0 +1,68 @@ +getSourceDir()}/third_party/re2/util/pcre.h", + ["#define UTIL_PCRE_H_\n#include ", '#define UTIL_PCRE_H_'], + ['#define UTIL_PCRE_H_', "#define UTIL_PCRE_H_\n#include "], + ); + return true; + } + + #[BuildFor('Linux')] + #[BuildFor('Darwin')] + public function buildUnix(ToolchainInterface $toolchain, LibraryPackage $lib): void + { + $cmake = UnixCMakeExecutor::create($lib) + ->setBuildDir("{$lib->getSourceDir()}/avoid_BUILD_file_conflict") + ->addConfigureArgs( + "-DgRPC_INSTALL_BINDIR={$lib->getBinDir()}", + "-DgRPC_INSTALL_LIBDIR={$lib->getLibDir()}", + "-DgRPC_INSTALL_SHAREDIR={$lib->getBuildRootPath()}/share/grpc", + "-DCMAKE_C_FLAGS=\"-DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK -L{$lib->getLibDir()} -I{$lib->getIncludeDir()}\"", + "-DCMAKE_CXX_FLAGS=\"-DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK -L{$lib->getLibDir()} -I{$lib->getIncludeDir()}\"", + '-DgRPC_BUILD_CODEGEN=OFF', + '-DgRPC_DOWNLOAD_ARCHIVES=OFF', + '-DgRPC_BUILD_TESTS=OFF', + // providers + '-DgRPC_ZLIB_PROVIDER=package', + '-DgRPC_CARES_PROVIDER=package', + '-DgRPC_SSL_PROVIDER=package', + ); + + if (PHP_OS_FAMILY === 'Linux' && $toolchain->isStatic() && !LinuxUtil::isMuslDist()) { + $cmake->addConfigureArgs( + '-DCMAKE_EXE_LINKER_FLAGS="-static-libgcc -static-libstdc++"', + '-DCMAKE_SHARED_LINKER_FLAGS="-static-libgcc -static-libstdc++"', + '-DCMAKE_CXX_STANDARD_LIBRARIES="-static-libgcc -static-libstdc++"', + ); + } + + $cmake->build(); + + $re2Content = file_get_contents("{$lib->getSourceDir()}/third_party/re2/re2.pc"); + $re2Content = "prefix={$lib->getBuildRootPath()}\nexec_prefix=\${prefix}\n{$re2Content}"; + file_put_contents("{$lib->getLibDir()}/pkgconfig/re2.pc", $re2Content); + $lib->patchPkgconfPrefix(['grpc++.pc', 'grpc.pc', 'grpc++_unsecure.pc', 'grpc_unsecure.pc', 're2.pc']); + } +} diff --git a/src/Package/Library/icu.php b/src/Package/Library/icu.php new file mode 100644 index 000000000..7364e94ab --- /dev/null +++ b/src/Package/Library/icu.php @@ -0,0 +1,66 @@ +getBinDir()}/icu-config", '/default_prefix=.*/m', 'default_prefix="{BUILD_ROOT_PATH}"'); + } + + #[BuildFor('Linux')] + public function buildLinux(LibraryPackage $lib, ToolchainInterface $toolchain, PackageBuilder $builder): void + { + $cppflags = 'CPPFLAGS="-DU_CHARSET_IS_UTF8=1 -DU_USING_ICU_NAMESPACE=1 -DU_STATIC_IMPLEMENTATION=1 -DPIC -fPIC"'; + $cxxflags = 'CXXFLAGS="-std=c++17 -DPIC -fPIC -fno-ident"'; + $ldflags = $toolchain->isStatic() ? 'LDFLAGS="-static"' : ''; + shell()->cd($lib->getSourceDir() . '/source')->initializeEnv($lib) + ->exec( + "{$cppflags} {$cxxflags} {$ldflags} " . + './runConfigureICU Linux ' . + '--enable-static ' . + '--disable-shared ' . + '--with-data-packaging=static ' . + '--enable-release=yes ' . + '--enable-extras=no ' . + '--enable-icuio=yes ' . + '--enable-dyload=no ' . + '--enable-tools=yes ' . + '--enable-tests=no ' . + '--enable-samples=no ' . + '--prefix=' . $lib->getBuildRootPath() + ) + ->exec('make clean') + ->exec("make -j{$builder->concurrency}") + ->exec('make install'); + + $lib->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX); + FileSystem::removeDir("{$lib->getLibDir()}/icu"); + } + + #[BuildFor('Darwin')] + public function buildDarwin(LibraryPackage $lib, PackageBuilder $builder): void + { + shell()->cd($lib->getSourceDir() . '/source') + ->exec("./runConfigureICU MacOSX --enable-static --disable-shared --disable-extras --disable-samples --disable-tests --prefix={$lib->getBuildRootPath()}") + ->exec('make clean') + ->exec("make -j{$builder->concurrency}") + ->exec('make install'); + + $lib->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX); + FileSystem::removeDir("{$lib->getLibDir()}/icu"); + } +} From 2a4959d9736405b245411f6390a3ae1f56f01ccf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 16:11:16 +0800 Subject: [PATCH 198/682] Chore --- src/StaticPHP/Config/ConfigValidator.php | 2 + src/StaticPHP/Package/PackageBuilder.php | 2 +- .../Package/PackageCallbacksTrait.php | 2 +- src/StaticPHP/Registry/PackageLoader.php | 4 +- src/StaticPHP/Registry/Registry.php | 51 +++++++++++++++++++ src/StaticPHP/Util/DependencyResolver.php | 8 +-- 6 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 1f3d5b9d9..0c3966d1e 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -32,6 +32,7 @@ class ConfigValidator 'build-static' => ConfigType::BOOL, 'build-with-php' => ConfigType::BOOL, 'notes' => ConfigType::BOOL, + 'display-name' => ConfigType::STRING, // library and target fields 'headers' => ConfigType::LIST_ARRAY, // @ @@ -75,6 +76,7 @@ class ConfigValidator 'build-static' => false, 'build-with-php' => false, 'notes' => false, + 'display-name' => false, ]; public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 42bf5c6f7..5d603625c 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -58,7 +58,7 @@ public function buildPackage(Package $package, bool $force = false): int if ($package->getType() !== 'virtual-target') { // patch before build - $package->patchBeforeBuild(); + $package->emitPatchBeforeBuild(); } // build diff --git a/src/StaticPHP/Package/PackageCallbacksTrait.php b/src/StaticPHP/Package/PackageCallbacksTrait.php index 6c6b87382..9556a4b41 100644 --- a/src/StaticPHP/Package/PackageCallbacksTrait.php +++ b/src/StaticPHP/Package/PackageCallbacksTrait.php @@ -53,7 +53,7 @@ public function addPatchBeforeBuildCallback(callable $callback): void $this->patch_before_build_callbacks[] = $callback; } - public function patchBeforeBuild(): void + public function emitPatchBeforeBuild(): void { if (file_exists("{$this->getSourceDir()}/.spc-patched")) { return; diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 424363eb3..ca195ff0e 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -159,7 +159,7 @@ public static function loadFromClass(mixed $class): void } $package_type = PackageConfig::get($attribute_instance->name, 'type'); if ($package_type === null) { - throw new RegistryException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); + throw new RegistryException("Package [{$attribute_instance->name}] not defined in config, but referenced from class {$class}, please check your config files."); } // if class has parent class and matches the attribute instance, use custom class @@ -277,6 +277,8 @@ public static function registerAllDefaultStages(): void foreach (self::$packages as $pkg) { if ($pkg instanceof PhpExtensionPackage) { $pkg->registerDefaultStages(); + } elseif ($pkg instanceof LibraryPackage) { + $pkg->registerDefaultStages(); } } } diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 68aa766d2..a753c122c 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -237,6 +237,9 @@ public static function resolve(): void // check BeforeStage, AfterStage is valid PackageLoader::checkLoadedStageEvents(); + + // Validate package dependencies + self::validatePackageDependencies(); } /** @@ -310,6 +313,54 @@ public static function getCurrentRegistryName(): ?string return self::$current_registry_name; } + /** + * Validate package dependencies to ensure all referenced dependencies exist. + * This helps catch configuration errors early in the registry loading process. + * + * @throws RegistryException + */ + private static function validatePackageDependencies(): void + { + $all_packages = PackageConfig::getAll(); + $errors = []; + + foreach ($all_packages as $pkg_name => $pkg_config) { + // Check depends field + $depends = PackageConfig::get($pkg_name, 'depends', []); + if (!is_array($depends)) { + $errors[] = "Package '{$pkg_name}' has invalid 'depends' field (expected array, got " . gettype($depends) . ')'; + continue; + } + + foreach ($depends as $dep) { + if (!isset($all_packages[$dep])) { + $config_info = self::getPackageConfigInfo($pkg_name); + $location = $config_info ? " (defined in {$config_info['config']})" : ''; + $errors[] = "Package '{$pkg_name}'{$location} depends on '{$dep}' which does not exist in any loaded registry"; + } + } + + // Check suggests field + $suggests = PackageConfig::get($pkg_name, 'suggests', []); + if (!is_array($suggests)) { + $errors[] = "Package '{$pkg_name}' has invalid 'suggests' field (expected array, got " . gettype($suggests) . ')'; + continue; + } + + foreach ($suggests as $suggest) { + if (!isset($all_packages[$suggest])) { + $config_info = self::getPackageConfigInfo($pkg_name); + $location = $config_info ? " (defined in {$config_info['config']})" : ''; + $errors[] = "Package '{$pkg_name}'{$location} suggests '{$suggest}' which does not exist in any loaded registry"; + } + } + } + + if (!empty($errors)) { + throw new RegistryException("Package dependency validation failed:\n - " . implode("\n - ", $errors)); + } + } + /** * Parse a class entry from the classes array. * Supports two formats: diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index b03f80892..2db74abd5 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -219,8 +219,8 @@ private static function visitPlatAllDeps(string $pkg_name, array $dep_list, arra return; } $visited[$pkg_name] = true; - // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) - foreach (array_merge($dep_list[$pkg_name]['depends'], $dep_list[$pkg_name]['suggests']) as $dep) { + // 遍历该依赖的所有依赖 + foreach (array_merge($dep_list[$pkg_name]['depends'] ?? [], $dep_list[$pkg_name]['suggests'] ?? []) as $dep) { self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted); } $sorted[] = $pkg_name; @@ -233,11 +233,11 @@ private static function visitPlatDeps(string $pkg_name, array $dep_list, array & return; } $visited[$pkg_name] = true; - // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) + // 遍历该依赖的所有依赖 if (!isset($dep_list[$pkg_name])) { throw new WrongUsageException("{$pkg_name} not exist !"); } - foreach ($dep_list[$pkg_name]['depends'] as $dep) { + foreach ($dep_list[$pkg_name]['depends'] ?? [] as $dep) { self::visitPlatDeps($dep, $dep_list, $visited, $sorted); } $sorted[] = $pkg_name; From 9f2132c0018ac3eeea6e95df05d5f47efe022c22 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 16:11:28 +0800 Subject: [PATCH 199/682] Add pack lib command --- src/StaticPHP/Command/Dev/PackLibCommand.php | 33 +++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Package/LibraryPackage.php | 140 +++++++++++++++++++ src/StaticPHP/Package/PackageInstaller.php | 12 ++ 4 files changed, 187 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/PackLibCommand.php diff --git a/src/StaticPHP/Command/Dev/PackLibCommand.php b/src/StaticPHP/Command/Dev/PackLibCommand.php new file mode 100644 index 000000000..6f69cb053 --- /dev/null +++ b/src/StaticPHP/Command/Dev/PackLibCommand.php @@ -0,0 +1,33 @@ +addArgument('library', InputArgument::REQUIRED, 'The library will be compiled'); + $this->addOption('show-libc-ver', null, null); + } + + public function handle(): int + { + $library = $this->getArgument('library'); + $show_libc_ver = $this->getOption('show-libc-ver'); + + $installer = new PackageInstaller(['pack-mode' => true]); + $installer->addBuildPackage($library); + + $installer->run(); + + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index e65617053..22404b6d3 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -9,6 +9,7 @@ use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; +use StaticPHP\Command\Dev\PackLibCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; @@ -63,6 +64,7 @@ public function __construct() new IsInstalledCommand(), new EnvCommand(), new LintConfigCommand(), + new PackLibCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 1cdd229e0..8a96f85ed 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -5,7 +5,13 @@ namespace StaticPHP\Package; use StaticPHP\Config\PackageConfig; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\PatchException; +use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Exception\ValidationException; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\DependencyResolver; +use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SPCConfigUtil; @@ -160,6 +166,114 @@ public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCON } } + /** + * Register default stages if not already defined by attributes. + * This is called after all attributes have been loaded. + * + * @internal Called by PackageLoader after loading attributes + */ + public function registerDefaultStages(): void + { + if (!$this->hasStage('packPrebuilt')) { + $this->addStage('packPrebuilt', [$this, 'packPrebuilt']); + } + // counting files before build stage + } + + /** + * Pack the prebuilt library into an archive. + * + * @internal this function is intended to be called by the dev:pack-lib command only + */ + public function packPrebuilt(): void + { + $target_dir = WORKING_DIR . '/dist'; + $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; + + if (!ApplicationContext::has(DirDiff::class)) { + throw new SPCInternalException('pack-dirdiff context not found for packPrebuilt stage. You cannot call "packPrebuilt" function manually.'); + } + // check whether this library has correctly installed files + if (!$this->isInstalled()) { + throw new ValidationException("Cannot pack prebuilt library [{$this->getName()}] because it is not fully installed."); + } + // get after-build buildroot file list + $increase_files = ApplicationContext::get(DirDiff::class)->getIncrementFiles(true); + + FileSystem::createDir($target_dir); + + // before pack, check if the dependency tree contains lib-suggests + $libraries = DependencyResolver::resolve([$this], include_suggests: true); + foreach ($libraries as $lib) { + if (PackageConfig::get($lib, 'suggests', []) !== []) { + throw new ValidationException("The library {$lib} has lib-suggests, packing [{$this->name}] is not safe, abort !"); + } + } + + $origin_files = []; + + // get pack placehoder defines + $placehoder = get_pack_replace(); + + // patch pkg-config and la files with absolute path + foreach ($increase_files as $file) { + if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) { + $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); + $origin_files[$file] = $content; + // replace relative paths with absolute paths + $content = str_replace( + array_keys($placehoder), + array_values($placehoder), + $content + ); + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + } + } + + // add .spc-extract-placeholder.json in BUILD_ROOT_PATH + file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT)); + $increase_files[] = '.spc-extract-placeholder.json'; + + // every file mapped with BUILD_ROOT_PATH + // get BUILD_ROOT_PATH last dir part + $buildroot_part = basename(BUILD_ROOT_PATH); + $increase_files = array_map(fn ($file) => $buildroot_part . '/' . $file, $increase_files); + // write list to packlib_files.txt + FileSystem::writeFile(WORKING_DIR . '/packlib_files.txt', implode("\n", $increase_files)); + // pack + $filename = match (SystemTarget::getTargetOS()) { + 'Windows' => '{name}-{arch}-{os}.tgz', + 'Darwin' => '{name}-{arch}-{os}.txz', + 'Linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', + }; + $replace = [ + '{name}' => $this->getName(), + '{arch}' => arch2gnu(php_uname('m')), + '{os}' => strtolower(PHP_OS_FAMILY), + '{libc}' => SystemTarget::getLibc() ?? 'default', + '{libcver}' => SystemTarget::getLibcVersion() ?? 'default', + ]; + // detect suffix, for proper tar option + $tar_option = $this->getTarOptionFromSuffix($filename); + $filename = str_replace(array_keys($replace), array_values($replace), $filename); + $filename = $target_dir . '/' . $filename; + f_passthru("tar {$tar_option} {$filename} -T " . WORKING_DIR . '/packlib_files.txt'); + logger()->info('Pack library ' . $this->getName() . ' to ' . $filename . ' complete.'); + + // remove temp files + unlink($placeholder_file); + + foreach ($origin_files as $file => $content) { + // restore original files + if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + } + } + + // remove dirdiff + ApplicationContext::set(DirDiff::class, null); + } + /** * Get static library files for current package and its dependencies. */ @@ -215,4 +329,30 @@ public function getBinDir(): string { return BUILD_BIN_PATH; } + + /** + * Get tar compress options from suffix + * + * @param string $name Package file name + * @return string Tar options for packaging libs + */ + private function getTarOptionFromSuffix(string $name): string + { + if (str_ends_with($name, '.tar')) { + return '-cf'; + } + if (str_ends_with($name, '.tar.gz') || str_ends_with($name, '.tgz')) { + return '-czf'; + } + if (str_ends_with($name, '.tar.bz2') || str_ends_with($name, '.tbz2')) { + return '-cjf'; + } + if (str_ends_with($name, '.tar.xz') || str_ends_with($name, '.txz')) { + return '-cJf'; + } + if (str_ends_with($name, '.tar.lz') || str_ends_with($name, '.tlz')) { + return '-c --lzma -f'; + } + return '-cf'; + } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index b9a3debb7..a01d29fcd 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -14,6 +14,7 @@ use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\DependencyResolver; +use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; @@ -71,6 +72,9 @@ public function addBuildPackage(LibraryPackage|string|TargetPackage $package): s if (!$package->hasStage('build')) { throw new WrongUsageException("Target package '{$package->getName()}' does not define build process for current OS: " . PHP_OS_FAMILY . '.'); } + if (($this->options['pack-mode'] ?? false) === true && !empty($this->build_packages)) { + throw new WrongUsageException("In 'pack-mode', only one package can be built at a time. Cannot add package '{$package->getName()}' to build list."); + } $this->build_packages[$package->getName()] = $package; return $this; } @@ -195,8 +199,16 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { + if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { + $dirdiff = new DirDiff(BUILD_ROOT_PATH, false); + ApplicationContext::set(DirDiff::class, $dirdiff); + } /** @var LibraryPackage $package */ $status = $builder->buildPackage($package, $this->isBuildPackage($package)); + + if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { + $package->runStage('packPrebuilt'); + } } catch (\Throwable $e) { if ($interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); From 81ce777bf2f8a9c00173bf9fc871d3defb29cd7a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 16:30:58 +0800 Subject: [PATCH 200/682] phpstan fix --- src/StaticPHP/Package/LibraryPackage.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 8a96f85ed..610e3550d 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -9,6 +9,7 @@ use StaticPHP\Exception\PatchException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\ValidationException; +use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\DirDiff; @@ -245,6 +246,7 @@ public function packPrebuilt(): void 'Windows' => '{name}-{arch}-{os}.tgz', 'Darwin' => '{name}-{arch}-{os}.txz', 'Linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', + default => throw new WrongUsageException('Unsupported OS for packing prebuilt library: ' . SystemTarget::getTargetOS()), }; $replace = [ '{name}' => $this->getName(), From 0d32b7bfdbb967da427d39438ae4b8795b689d68 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 16:42:49 +0800 Subject: [PATCH 201/682] Refactor lib packing to v3 postinstall action --- src/StaticPHP/Package/LibraryPackage.php | 39 ++++++++++++++++-------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 610e3550d..0567f92b8 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -189,7 +189,7 @@ public function registerDefaultStages(): void public function packPrebuilt(): void { $target_dir = WORKING_DIR . '/dist'; - $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; + $postinstall_file = BUILD_ROOT_PATH . '/.package.' . $this->getName() . '.postinstall.json'; if (!ApplicationContext::has(DirDiff::class)) { throw new SPCInternalException('pack-dirdiff context not found for packPrebuilt stage. You cannot call "packPrebuilt" function manually.'); @@ -212,28 +212,39 @@ public function packPrebuilt(): void } $origin_files = []; + $postinstall_files = []; - // get pack placehoder defines - $placehoder = get_pack_replace(); + // get pack placeholder defines + $placeholder = get_pack_replace(); - // patch pkg-config and la files with absolute path + // patch pkg-config and la files with placeholder paths foreach ($increase_files as $file) { if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) { $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); $origin_files[$file] = $content; - // replace relative paths with absolute paths + // replace actual paths with placeholders $content = str_replace( - array_keys($placehoder), - array_values($placehoder), + array_keys($placeholder), + array_values($placeholder), $content ); FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + // record files that need postinstall path replacement + $postinstall_files[] = $file; } } - // add .spc-extract-placeholder.json in BUILD_ROOT_PATH - file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT)); - $increase_files[] = '.spc-extract-placeholder.json'; + // generate postinstall action file if there are files to process + if ($postinstall_files !== []) { + $postinstall_actions = [ + [ + 'action' => 'replace-path', + 'files' => $postinstall_files, + ], + ]; + FileSystem::writeFile($postinstall_file, json_encode($postinstall_actions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $increase_files[] = '.package.' . $this->getName() . '.postinstall.json'; + } // every file mapped with BUILD_ROOT_PATH // get BUILD_ROOT_PATH last dir part @@ -262,11 +273,13 @@ public function packPrebuilt(): void f_passthru("tar {$tar_option} {$filename} -T " . WORKING_DIR . '/packlib_files.txt'); logger()->info('Pack library ' . $this->getName() . ' to ' . $filename . ' complete.'); - // remove temp files - unlink($placeholder_file); + // remove postinstall temp file + if (file_exists($postinstall_file)) { + unlink($postinstall_file); + } + // restore original files foreach ($origin_files as $file => $content) { - // restore original files if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); } From b3bbe0a75146a7d65ae2ae64ca8d0f98451cb46e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 19:19:09 +0800 Subject: [PATCH 202/682] Add libjpeg,libpng --- config/pkg/lib/libjpeg.yml | 12 +++++++++ config/pkg/lib/libpng.yml | 16 +++++++++++ src/Package/Library/libjpeg.php | 28 ++++++++++++++++++++ src/Package/Library/libpng.php | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 config/pkg/lib/libjpeg.yml create mode 100644 config/pkg/lib/libpng.yml create mode 100644 src/Package/Library/libjpeg.php create mode 100644 src/Package/Library/libpng.php diff --git a/config/pkg/lib/libjpeg.yml b/config/pkg/lib/libjpeg.yml new file mode 100644 index 000000000..9171ad388 --- /dev/null +++ b/config/pkg/lib/libjpeg.yml @@ -0,0 +1,12 @@ +libjpeg: + type: library + artifact: + source: + type: ghtar + repo: libjpeg-turbo/libjpeg-turbo + metadata: + license-files: [LICENSE.md] + license: IJG + static-libs@unix: + - libjpeg.a + - libturbojpeg.a diff --git a/config/pkg/lib/libpng.yml b/config/pkg/lib/libpng.yml new file mode 100644 index 000000000..e8831d60c --- /dev/null +++ b/config/pkg/lib/libpng.yml @@ -0,0 +1,16 @@ +libpng: + type: library + artifact: + source: + type: ghtagtar + repo: pnggroup/libpng + match: v1\.6\.\d+ + query: '?per_page=150' + binary: hosted + metadata: + license-files: [LICENSE] + license: PNG + depends: + - zlib + static-libs@unix: + - libpng16.a diff --git a/src/Package/Library/libjpeg.php b/src/Package/Library/libjpeg.php new file mode 100644 index 000000000..04f4fd254 --- /dev/null +++ b/src/Package/Library/libjpeg.php @@ -0,0 +1,28 @@ +addConfigureArgs( + '-DENABLE_STATIC=ON', + '-DENABLE_SHARED=OFF', + ) + ->build(); + // patch pkgconfig + $lib->patchPkgconfPrefix(['libjpeg.pc', 'libturbojpeg.pc']); + } +} diff --git a/src/Package/Library/libpng.php b/src/Package/Library/libpng.php new file mode 100644 index 000000000..1d02fdd69 --- /dev/null +++ b/src/Package/Library/libpng.php @@ -0,0 +1,47 @@ +getBuildRootPath()}", + ]; + + // Enable architecture-specific optimizations + match (getenv('SPC_ARCH')) { + 'x86_64' => $args[] = '--enable-intel-sse', + 'aarch64' => $args[] = '--enable-arm-neon', + default => null, + }; + + UnixAutoconfExecutor::create($lib) + ->exec('chmod +x ./configure') + ->exec('chmod +x ./install-sh') + ->appendEnv(['LDFLAGS' => "-L{$lib->getLibDir()}"]) + ->addConfigureArgs(...$args) + ->configure() + ->make( + 'libpng16.la', + 'install-libLTLIBRARIES install-data-am', + after_env_vars: ['DEFAULT_INCLUDES' => "-I{$lib->getSourceDir()} -I{$lib->getIncludeDir()}"] + ); + + // patch pkgconfig + $lib->patchPkgconfPrefix(['libpng16.pc']); + $lib->patchLaDependencyPrefix(); + } +} From 8fc2da9acff46a6e495b5a98124b566315619112 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 19:19:26 +0800 Subject: [PATCH 203/682] Use OS release definition for openssl --- src/Package/Library/openssl.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php index e0ee9edc3..541b6145f 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\System\LinuxUtil; #[Library('openssl')] class openssl @@ -48,8 +49,7 @@ public function build(LibraryPackage $lib): void '--with-zlib-lib=' . BUILD_LIB_PATH . ' '; $openssl_dir = getenv('OPENSSLDIR') ?: null; - // TODO: in v3 use the following: $openssl_dir ??= SystemUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; - $openssl_dir ??= '/etc/ssl'; + $openssl_dir ??= LinuxUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; $ex_lib = trim($ex_lib); shell()->cd($lib->getSourceDir())->initializeEnv($lib) From 97634b009f9c81e40d2fbd08c486192d5b007934 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 19:21:13 +0800 Subject: [PATCH 204/682] Forward-port #1006 changes --- src/Package/Artifact/zig.php | 6 +++-- .../Downloader/Type/GitHubRelease.php | 8 +++--- .../Downloader/Type/GitHubTarball.php | 10 ++++--- src/StaticPHP/Config/ConfigValidator.php | 6 ++--- src/StaticPHP/Toolchain/ZigToolchain.php | 3 ++- src/StaticPHP/Util/FileSystem.php | 12 ++++----- src/StaticPHP/Util/SPCConfigUtil.php | 26 ++++++++++++++++--- 7 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 2ac7b454b..9a1430637 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -25,8 +25,10 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $index_json = json_decode($index_json ?: '', true); $latest_version = null; foreach ($index_json as $version => $data) { - $latest_version = $version; - break; + if ($version !== 'master') { + $latest_version = $version; + break; + } } if (!$latest_version) { diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 731e8297e..7b0412886 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -21,10 +21,11 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface private ?string $version = null; - public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true): array + public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null): array { logger()->debug("Fetching {$name} GitHub releases from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); + $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); $data2 = default_shell()->executeCurl($url, headers: $headers); $data = json_decode($data2 ?: '', true); @@ -45,9 +46,10 @@ public function getGitHubReleases(string $name, string $repo, bool $prefer_stabl * Get the latest GitHub release assets for a given repository. * match_asset is provided, only return the asset that matches the regex. */ - public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset): array + public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array { $url = str_replace('{repo}', $repo, self::API_URL); + $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); $data2 = default_shell()->executeCurl($url, headers: $headers); $data = json_decode($data2 ?: '', true); @@ -81,7 +83,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (!isset($config['match'])) { throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); } - $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match']); + $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); // download file using curl $asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL); diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 7917e4c01..8aa1ac694 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -23,9 +23,10 @@ class GitHubTarball implements DownloadTypeInterface * If match_url is provided, only return the tarball that matches the regex. * Otherwise, return the first tarball found. */ - public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null): array + public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null): array { $url = str_replace(['{repo}', '{rel_type}'], [$repo, $rel_type], self::API_URL); + $url .= ($query ?? ''); $data = default_shell()->executeCurl($url, headers: $this->getGitHubTokenHeaders()); $data = json_decode($data ?: '', true); if (!is_array($data)) { @@ -33,7 +34,10 @@ public function getGitHubTarballInfo(string $name, string $repo, string $rel_typ } $url = null; foreach ($data as $rel) { - if (($rel['prerelease'] ?? false) === true && $prefer_stable) { + $prerelease = $rel['prerelease'] ?? false; + $draft = $rel['draft'] ?? false; + $tarball_url = $rel['tarball_url'] ?? null; + if ($prerelease && $prefer_stable || $draft && $prefer_stable || !$tarball_url) { continue; } if ($match_url === null) { @@ -70,7 +74,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo 'ghtagtar' => 'tags', default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), }; - [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name); + [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 0c3966d1e..fbf883213 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -82,9 +82,9 @@ class ConfigValidator public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] 'filelist' => [['url', 'regex'], ['extract']], 'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']], - 'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']], - 'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']], - 'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']], + 'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match', 'query']], + 'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match', 'query']], + 'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable', 'query']], 'url' => [['url'], ['filename', 'extract', 'version']], 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php index 2d7c71b0c..344ce3e9c 100644 --- a/src/StaticPHP/Toolchain/ZigToolchain.php +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -64,7 +64,8 @@ public function afterInit(): void $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); if (!$has_avx512) { - GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no'); + $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no {$extra_vars}"); } } diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 1c21a92b5..c8da5353d 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -314,12 +314,7 @@ public static function removeDir(string $dir): bool continue; } $sub_file = self::convertPath($dir . '/' . $v); - if (is_dir($sub_file)) { - # 如果是 目录 且 递推 , 则递推添加下级文件 - if (!self::removeDir($sub_file)) { - return false; - } - } elseif (is_link($sub_file) || is_file($sub_file)) { + if (is_link($sub_file) || is_file($sub_file)) { if (!unlink($sub_file)) { $cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f'; f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret); @@ -328,6 +323,11 @@ public static function removeDir(string $dir): bool return false; } } + } elseif (is_dir($sub_file)) { + # 如果是 目录 且 递推 , 则递推添加下级文件 + if (!self::removeDir($sub_file)) { + return false; + } } } if (is_link($dir)) { diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index a31b771f8..32ef3bc64 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -291,9 +291,18 @@ private function getIncludesString(array $packages): string // parse pkg-configs foreach ($packages as $package) { $pc = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(SystemTarget::isUnix() ? ':' : ';', $pkg_config_path)); foreach ($pc as $file) { - if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$file}.pc")) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$file}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); } } $pc_cflags = implode(' ', $pc); @@ -324,9 +333,18 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st if (SystemTarget::isUnix()) { // add pkg-configs libs $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); foreach ($pkg_configs as $pkg_config) { - if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$pkg_config}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); } } $pkg_configs = implode(' ', $pkg_configs); From a75060e5f6f4478660b71446cd81a1a881fa4747 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 20:56:25 +0800 Subject: [PATCH 205/682] Update exit code in ArtifactDownloader to reflect termination signal --- src/StaticPHP/Artifact/ArtifactDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 27cd6adc5..b0cbfeb83 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -294,7 +294,7 @@ public function download(bool $interactive = true): void FileSystem::removeFileIfExists($path); } } - exit(2); + exit(130); }); } From a07265787b5fc9f9f560bca538af043178aaf022 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 20:56:34 +0800 Subject: [PATCH 206/682] Update license file path for bzip2 in configuration --- config/pkg/lib/bzip2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/lib/bzip2.yml b/config/pkg/lib/bzip2.yml index 132a24ffa..1cd36bd7e 100644 --- a/config/pkg/lib/bzip2.yml +++ b/config/pkg/lib/bzip2.yml @@ -10,7 +10,7 @@ bzip2: regex: '/href="(?bzip2-(?[^"]+)\.tar\.gz)"/' binary: hosted metadata: - license-files: ['{registry_root}/src/globals/licenses/bzip2.txt'] + license-files: ['@/bzip2.txt'] license: bzip2-1.0.6 headers: - bzlib.h From 807b90b1828cc70acee439713c61321e6f2b8977 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 20:56:50 +0800 Subject: [PATCH 207/682] Fix incorrect variable name for working directory in submodule update command --- src/StaticPHP/Runtime/Shell/DefaultShell.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index a6421bdb9..5b50d1528 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -104,7 +104,7 @@ public function executeGitClone(string $url, string $branch, string $path, bool $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, cwd: $path_arg); + $this->passthru($submodule_cmd, $this->console_putput, cwd: $path); } } } From 7ae16e5be8a440c6476f52b562e470935d5d5fa7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Feb 2026 20:59:23 +0800 Subject: [PATCH 208/682] Add imagemagick,jbig,lerc,libaom,libde265,libheif,libjxl,libtiff,libwebp,libzip --- config/pkg/lib/imagemagick.yml | 32 +++++++++++ config/pkg/lib/jbig.yml | 15 ++++++ config/pkg/lib/lerc.yml | 12 +++++ config/pkg/lib/libaom.yml | 12 +++++ config/pkg/lib/libde265.yml | 13 +++++ config/pkg/lib/libheif.yml | 18 +++++++ config/pkg/lib/libjxl.yml | 21 ++++++++ config/pkg/lib/libtiff.yml | 21 ++++++++ config/pkg/lib/libwebp.yml | 16 ++++++ config/pkg/lib/libzip.yml | 23 ++++++++ src/Package/Library/imagemagick.php | 83 +++++++++++++++++++++++++++++ src/Package/Library/jbig.php | 46 ++++++++++++++++ src/Package/Library/lerc.php | 21 ++++++++ src/Package/Library/libaom.php | 33 ++++++++++++ src/Package/Library/libde265.php | 24 +++++++++ src/Package/Library/libheif.php | 45 ++++++++++++++++ src/Package/Library/libjxl.php | 52 ++++++++++++++++++ src/Package/Library/libtiff.php | 50 +++++++++++++++++ src/Package/Library/libwebp.php | 44 +++++++++++++++ src/Package/Library/libzip.php | 36 +++++++++++++ 20 files changed, 617 insertions(+) create mode 100644 config/pkg/lib/imagemagick.yml create mode 100644 config/pkg/lib/jbig.yml create mode 100644 config/pkg/lib/lerc.yml create mode 100644 config/pkg/lib/libaom.yml create mode 100644 config/pkg/lib/libde265.yml create mode 100644 config/pkg/lib/libheif.yml create mode 100644 config/pkg/lib/libjxl.yml create mode 100644 config/pkg/lib/libtiff.yml create mode 100644 config/pkg/lib/libwebp.yml create mode 100644 config/pkg/lib/libzip.yml create mode 100644 src/Package/Library/imagemagick.php create mode 100644 src/Package/Library/jbig.php create mode 100644 src/Package/Library/lerc.php create mode 100644 src/Package/Library/libaom.php create mode 100644 src/Package/Library/libde265.php create mode 100644 src/Package/Library/libheif.php create mode 100644 src/Package/Library/libjxl.php create mode 100644 src/Package/Library/libtiff.php create mode 100644 src/Package/Library/libwebp.php create mode 100644 src/Package/Library/libzip.php diff --git a/config/pkg/lib/imagemagick.yml b/config/pkg/lib/imagemagick.yml new file mode 100644 index 000000000..17fc03e81 --- /dev/null +++ b/config/pkg/lib/imagemagick.yml @@ -0,0 +1,32 @@ +imagemagick: + type: library + artifact: + source: { + "type": "ghtar", + "repo": "ImageMagick/ImageMagick" + } + metadata: + license-files: [LICENSE] + lang: cpp + pkg-configs: [ + "Magick++-7.Q16HDRI", + "MagickCore-7.Q16HDRI", + "MagickWand-7.Q16HDRI" + ] + depends: + - zlib + - libjpeg + - libjxl + - libpng + - libwebp + - freetype + - libtiff + - libheif + - bzip2 + suggests: + - zstd + - xz + - libzip + - libxml2 + + diff --git a/config/pkg/lib/jbig.yml b/config/pkg/lib/jbig.yml new file mode 100644 index 000000000..f96ecf461 --- /dev/null +++ b/config/pkg/lib/jbig.yml @@ -0,0 +1,15 @@ +jbig: + type: library + artifact: + source: 'https://dl.static-php.dev/static-php-cli/deps/jbig/jbigkit-2.1.tar.gz' + source-mirror: 'https://www.cl.cam.ac.uk/~mgk25/jbigkit/download/jbigkit-2.1.tar.gz' + metadata: + license-files: [COPYING] + license: GPL-2.0-or-later + headers: + - jbig.h + - jbig85.h + - jbig_ar.h + static-libs@unix: + - libjbig.a + - libjbig85.a diff --git a/config/pkg/lib/lerc.yml b/config/pkg/lib/lerc.yml new file mode 100644 index 000000000..330ca7952 --- /dev/null +++ b/config/pkg/lib/lerc.yml @@ -0,0 +1,12 @@ +lerc: + type: library + artifact: + source: + type: ghtar + repo: Esri/lerc + prefer-stable: true + metadata: + license-files: [LICENSE] + lang: cpp + static-libs@unix: + - libLerc.a diff --git a/config/pkg/lib/libaom.yml b/config/pkg/lib/libaom.yml new file mode 100644 index 000000000..6a2dbe3cb --- /dev/null +++ b/config/pkg/lib/libaom.yml @@ -0,0 +1,12 @@ +libaom: + type: library + artifact: + source: + type: git + rev: main + url: 'https://aomedia.googlesource.com/aom' + metadata: + license-files: [LICENSE] + lang: cpp + static-libs@unix: + - libaom.a diff --git a/config/pkg/lib/libde265.yml b/config/pkg/lib/libde265.yml new file mode 100644 index 000000000..679c875a4 --- /dev/null +++ b/config/pkg/lib/libde265.yml @@ -0,0 +1,13 @@ +libde265: + type: library + artifact: + source: + type: ghrel + repo: strukturag/libde265 + match: libde265-.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [COPYING] + lang: cpp + static-libs@unix: + - libde265.a diff --git a/config/pkg/lib/libheif.yml b/config/pkg/lib/libheif.yml new file mode 100644 index 000000000..4265f2c2e --- /dev/null +++ b/config/pkg/lib/libheif.yml @@ -0,0 +1,18 @@ +libheif: + type: library + artifact: + source: + type: ghrel + repo: strukturag/libheif + match: libheif-.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [COPYING] + depends: + - libde265 + - libwebp + - libaom + - zlib + - brotli + static-libs@unix: + - libheif.a diff --git a/config/pkg/lib/libjxl.yml b/config/pkg/lib/libjxl.yml new file mode 100644 index 000000000..f2f3d5516 --- /dev/null +++ b/config/pkg/lib/libjxl.yml @@ -0,0 +1,21 @@ +libjxl: + type: library + artifact: + source: + type: git + url: 'https://github.com/libjxl/libjxl' + rev: main + submodules: [third_party/highway, third_party/libjpeg-turbo, third_party/sjpeg, third_party/skcms] + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - brotli + - libjpeg + - libpng + - libwebp + pkg-configs: + - libjxl + - libjxl_cms + - libjxl_threads + - libhwy diff --git a/config/pkg/lib/libtiff.yml b/config/pkg/lib/libtiff.yml new file mode 100644 index 000000000..e3f345f13 --- /dev/null +++ b/config/pkg/lib/libtiff.yml @@ -0,0 +1,21 @@ +libtiff: + type: library + artifact: + source: + type: filelist + url: 'https://download.osgeo.org/libtiff/' + regex: '/href="(?tiff-(?[^"]+)\.tar\.xz)"/' + metadata: + license-files: [LICENSE.md] + license: libtiff + depends: + - zlib + - libjpeg + suggests@unix: + - lerc + - libwebp + - jbig + - xz + - zstd + static-libs@unix: + - libtiff.a diff --git a/config/pkg/lib/libwebp.yml b/config/pkg/lib/libwebp.yml new file mode 100644 index 000000000..62ddddc13 --- /dev/null +++ b/config/pkg/lib/libwebp.yml @@ -0,0 +1,16 @@ +libwebp: + type: library + artifact: + source: + type: ghtagtar + repo: webmproject/libwebp + match: v1\.\d+\.\d+$ + metadata: + license-files: [COPYING] + license: BSD-3-Clause + pkg-configs: + - libwebp + - libwebpdecoder + - libwebpdemux + - libwebpmux + - libsharpyuv diff --git a/config/pkg/lib/libzip.yml b/config/pkg/lib/libzip.yml new file mode 100644 index 000000000..3d8c02f8f --- /dev/null +++ b/config/pkg/lib/libzip.yml @@ -0,0 +1,23 @@ +libzip: + type: library + artifact: + source: { + "type": "ghrel", + "repo": "nih-at/libzip", + "match": "libzip.+\\.tar\\.xz", + "prefer-stable": true + } + metadata: + license-files: [LICENSE] + static-libs@unix: + - libzip.a + headers: + - zip.h + - zipconf.h + depends@unix: + - zlib + suggests@unix: + - bzip2 + - xz + - zstd + - openssl diff --git a/src/Package/Library/imagemagick.php b/src/Package/Library/imagemagick.php new file mode 100644 index 000000000..941f1580e --- /dev/null +++ b/src/Package/Library/imagemagick.php @@ -0,0 +1,83 @@ +optionalPackage('libzip', ...ac_with_args('zip')) + ->optionalPackage('libjpeg', ...ac_with_args('jpeg')) + ->optionalPackage('libpng', ...ac_with_args('png')) + ->optionalPackage('libwebp', ...ac_with_args('webp')) + ->optionalPackage('libxml2', ...ac_with_args('xml')) + ->optionalPackage('libheif', ...ac_with_args('heic')) + ->optionalPackage('zlib', ...ac_with_args('zlib')) + ->optionalPackage('xz', ...ac_with_args('lzma')) + ->optionalPackage('zstd', ...ac_with_args('zstd')) + ->optionalPackage('freetype', ...ac_with_args('freetype')) + ->optionalPackage('bzip2', ...ac_with_args('bzlib')) + ->optionalPackage('libjxl', ...ac_with_args('jxl')) + ->optionalPackage('jbig', ...ac_with_args('jbig')) + ->addConfigureArgs( + '--disable-openmp', + '--without-x', + ); + + // special: linux-static target needs `-static` + $ldflags = $toolchain->isStatic() ? '-static -ldl' : '-ldl'; + + // special: macOS needs -iconv + $libs = SystemTarget::getTargetOS() === 'Darwin' ? '-liconv' : ''; + + $ac->appendEnv([ + 'LDFLAGS' => $ldflags, + 'LIBS' => $libs, + 'PKG_CONFIG' => '$PKG_CONFIG --static', + ]); + + $ac->configure()->make(); + + f_putenv("SPC_DEFAULT_LD_FLAGS={$original_ldflags}"); + + $filelist = [ + 'ImageMagick.pc', + 'ImageMagick-7.Q16HDRI.pc', + 'Magick++.pc', + 'Magick++-7.Q16HDRI.pc', + 'MagickCore.pc', + 'MagickCore-7.Q16HDRI.pc', + 'MagickWand.pc', + 'MagickWand-7.Q16HDRI.pc', + ]; + $lib->patchPkgconfPrefix($filelist); + foreach ($filelist as $file) { + FileSystem::replaceFileRegex( + "{$lib->getLibDir()}/pkgconfig/{$file}", + '#includearchdir=/include/ImageMagick-7#m', + 'includearchdir=${prefix}/include/ImageMagick-7' + ); + } + $lib->patchLaDependencyPrefix(); + } +} diff --git a/src/Package/Library/jbig.php b/src/Package/Library/jbig.php new file mode 100644 index 000000000..1cfe60b74 --- /dev/null +++ b/src/Package/Library/jbig.php @@ -0,0 +1,46 @@ +getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', 'CFLAGS = -O2 -W -Wno-unused-result -fPIC'); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(LibraryPackage $lib, PackageBuilder $builder): void + { + $ccenv = [ + 'CC' => getenv('CC'), + 'CXX' => getenv('CXX'), + 'AR' => getenv('AR'), + 'LD' => getenv('LD'), + ]; + $env = []; + foreach ($ccenv as $k => $v) { + $env[] = "{$k}={$v}"; + } + $env_str = implode(' ', $env); + shell()->cd($lib->getSourceDir())->initializeEnv($lib) + ->exec("make -j{$builder->concurrency} {$env_str} lib") + ->exec("cp libjbig/libjbig.a {$lib->getLibDir()}") + ->exec("cp libjbig/libjbig85.a {$lib->getLibDir()}") + ->exec("cp libjbig/jbig.h {$lib->getIncludeDir()}") + ->exec("cp libjbig/jbig85.h {$lib->getIncludeDir()}") + ->exec("cp libjbig/jbig_ar.h {$lib->getIncludeDir()}"); + } +} diff --git a/src/Package/Library/lerc.php b/src/Package/Library/lerc.php new file mode 100644 index 000000000..fc1e8bada --- /dev/null +++ b/src/Package/Library/lerc.php @@ -0,0 +1,21 @@ +build(); + } +} diff --git a/src/Package/Library/libaom.php b/src/Package/Library/libaom.php new file mode 100644 index 000000000..167ef0766 --- /dev/null +++ b/src/Package/Library/libaom.php @@ -0,0 +1,33 @@ +setBuildDir("{$this->getSourceDir()}/builddir") + ->addConfigureArgs('-DAOM_TARGET_CPU=generic') + ->build(); + f_putenv("SPC_COMPILER_EXTRA={$extra}"); + $this->patchPkgconfPrefix(['aom.pc']); + } +} diff --git a/src/Package/Library/libde265.php b/src/Package/Library/libde265.php new file mode 100644 index 000000000..b3e8f62f8 --- /dev/null +++ b/src/Package/Library/libde265.php @@ -0,0 +1,24 @@ +addConfigureArgs('-DENABLE_SDL=OFF') + ->build(); + $this->patchPkgconfPrefix(['libde265.pc']); + } +} diff --git a/src/Package/Library/libheif.php b/src/Package/Library/libheif.php new file mode 100644 index 000000000..65545f365 --- /dev/null +++ b/src/Package/Library/libheif.php @@ -0,0 +1,45 @@ +getSourceDir() . '/CMakeLists.txt'), 'libbrotlienc')) { + FileSystem::replaceFileStr( + $lib->getSourceDir() . '/CMakeLists.txt', + 'list(APPEND REQUIRES_PRIVATE "libbrotlidec")', + 'list(APPEND REQUIRES_PRIVATE "libbrotlidec")' . "\n" . ' list(APPEND REQUIRES_PRIVATE "libbrotlienc")' + ); + } + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(LibraryPackage $lib): void + { + UnixCMakeExecutor::create($lib) + ->addConfigureArgs( + '--preset=release', + '-DWITH_EXAMPLES=OFF', + '-DWITH_GDK_PIXBUF=OFF', + '-DBUILD_TESTING=OFF', + '-DWITH_LIBSHARPYUV=ON', // optional: libwebp + '-DENABLE_PLUGIN_LOADING=OFF', + ) + ->build(); + $lib->patchPkgconfPrefix(['libheif.pc']); + } +} diff --git a/src/Package/Library/libjxl.php b/src/Package/Library/libjxl.php new file mode 100644 index 000000000..48e9a239a --- /dev/null +++ b/src/Package/Library/libjxl.php @@ -0,0 +1,52 @@ +addConfigureArgs( + '-DJPEGXL_ENABLE_TOOLS=OFF', + '-DJPEGXL_ENABLE_EXAMPLES=OFF', + '-DJPEGXL_ENABLE_MANPAGES=OFF', + '-DJPEGXL_ENABLE_BENCHMARK=OFF', + '-DJPEGXL_ENABLE_PLUGINS=OFF', + '-DJPEGXL_ENABLE_SJPEG=ON', + '-DJPEGXL_ENABLE_JNI=OFF', + '-DJPEGXL_ENABLE_TRANSCODE_JPEG=ON', + '-DJPEGXL_STATIC=' . ($toolchain->isStatic() ? 'ON' : 'OFF'), + '-DJPEGXL_FORCE_SYSTEM_BROTLI=ON', + '-DBUILD_TESTING=OFF' + ); + + if ($toolchain instanceof ZigToolchain) { + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); + if (!$has_avx512) { + $cmake->addConfigureArgs( + '-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE', + '-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE' + ); + } + } + + $cmake->build(); + } +} diff --git a/src/Package/Library/libtiff.php b/src/Package/Library/libtiff.php new file mode 100644 index 000000000..385ff3ca4 --- /dev/null +++ b/src/Package/Library/libtiff.php @@ -0,0 +1,50 @@ +getSourceDir()}/configure", '-lwebp', '-lwebp -lsharpyuv'); + FileSystem::replaceFileStr("{$lib->getSourceDir()}/configure", '-l"$lerc_lib_name"', "-l\"\$lerc_lib_name\" {$libcpp}"); + UnixAutoconfExecutor::create($lib) + ->optionalPackage('lerc', '--enable-lerc', '--disable-lerc') + ->optionalPackage('zstd', '--enable-zstd', '--disable-zstd') + ->optionalPackage('libwebp', '--enable-webp', '--disable-webp') + ->optionalPackage('xz', '--enable-lzma', '--disable-lzma') + ->optionalPackage('jbig', '--enable-jbig', '--disable-jbig') + ->configure( + // zlib deps + '--enable-zlib', + "--with-zlib-include-dir={$lib->getIncludeDir()}", + "--with-zlib-lib-dir={$lib->getLibDir()}", + // libjpeg deps + '--enable-jpeg', + "--with-jpeg-include-dir={$lib->getIncludeDir()}", + "--with-jpeg-lib-dir={$lib->getLibDir()}", + '--disable-old-jpeg', + '--disable-jpeg12', + '--disable-libdeflate', + '--disable-tools', + '--disable-contrib', + '--disable-cxx', + '--without-x', + ) + ->make(); + $lib->patchPkgconfPrefix(['libtiff-4.pc']); + } +} diff --git a/src/Package/Library/libwebp.php b/src/Package/Library/libwebp.php new file mode 100644 index 000000000..0ee4028d5 --- /dev/null +++ b/src/Package/Library/libwebp.php @@ -0,0 +1,44 @@ + +int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }'; + $cc = getenv('CC') ?: 'gcc'; + [$ret] = shell()->execWithResult("printf '%s' '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1"); + $disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64' && PHP_OS_FAMILY === 'Linux'; + + UnixCMakeExecutor::create($this) + ->addConfigureArgs( + '-DWEBP_BUILD_EXTRAS=OFF', + '-DWEBP_BUILD_ANIM_UTILS=OFF', + '-DWEBP_BUILD_CWEBP=OFF', + '-DWEBP_BUILD_DWEBP=OFF', + '-DWEBP_BUILD_GIF2WEBP=OFF', + '-DWEBP_BUILD_IMG2WEBP=OFF', + '-DWEBP_BUILD_VWEBP=OFF', + '-DWEBP_BUILD_WEBPINFO=OFF', + '-DWEBP_BUILD_WEBPMUX=OFF', + '-DWEBP_BUILD_FUZZTEST=OFF', + $disableAvx2 ? '-DWEBP_ENABLE_SIMD=OFF' : '' + ) + ->build(); + // patch pkgconfig + $this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR); + $this->patchPkgconfPrefix(['libsharpyuv.pc'], PKGCONF_PATCH_CUSTOM, ['/^includedir=.*$/m', 'includedir=${prefix}/include/webp']); + } +} diff --git a/src/Package/Library/libzip.php b/src/Package/Library/libzip.php new file mode 100644 index 000000000..f6ebdfea7 --- /dev/null +++ b/src/Package/Library/libzip.php @@ -0,0 +1,36 @@ +optionalPackage('bzip2', ...cmake_boolean_args('ENABLE_BZIP2')) + ->optionalPackage('xz', ...cmake_boolean_args('ENABLE_LZMA')) + ->optionalPackage('openssl', ...cmake_boolean_args('ENABLE_OPENSSL')) + ->optionalPackage('zstd', ...cmake_boolean_args('ENABLE_ZSTD')) + ->addConfigureArgs( + '-DENABLE_GNUTLS=OFF', + '-DENABLE_MBEDTLS=OFF', + '-DBUILD_DOC=OFF', + '-DBUILD_EXAMPLES=OFF', + '-DBUILD_REGRESS=OFF', + '-DBUILD_TOOLS=OFF', + '-DBUILD_OSSFUZZ=OFF', + ) + ->build(); + $lib->patchPkgconfPrefix(['libzip.pc'], PKGCONF_PATCH_PREFIX); + } +} From 8f798c9006553479722658c5bbf330d6bb9d1217 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 09:48:51 +0800 Subject: [PATCH 209/682] Add imap and BuildRootTracker --- config/pkg/lib/imap.yml | 14 ++ src/Package/Library/imap.php | 92 ++++++++++ src/StaticPHP/Package/PackageInstaller.php | 33 ++++ src/StaticPHP/Util/BuildRootTracker.php | 189 +++++++++++++++++++++ 4 files changed, 328 insertions(+) create mode 100644 config/pkg/lib/imap.yml create mode 100644 src/StaticPHP/Util/BuildRootTracker.php diff --git a/config/pkg/lib/imap.yml b/config/pkg/lib/imap.yml new file mode 100644 index 000000000..cb2ec4106 --- /dev/null +++ b/config/pkg/lib/imap.yml @@ -0,0 +1,14 @@ +imap: + type: library + artifact: + source: { + "type": "git", + "url": "https://github.com/static-php/imap.git", + "rev": "master" + } + metadata: + license-files: [LICENSE] + static-libs@unix: + - libc-client.a + suggests@unix: + - openssl diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index 69e6c8820..607d78ee2 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -6,10 +6,15 @@ use Package\Target\php; use StaticPHP\Attribute\Package\AfterStage; +use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\SourcePatcher; #[Library('imap')] class imap @@ -22,4 +27,91 @@ public function afterPatchScripts(): void FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"'); } } + + #[PatchBeforeBuild] + #[PatchDescription('Patch imap build system for Linux and macOS compatibility')] + public function patchBeforeBuild(LibraryPackage $lib): void + { + if (SystemTarget::getTargetOS() === 'Linux') { + $cc = getenv('CC') ?: 'gcc'; + // FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', '-DMAC_OSX_KLUDGE=1', ''); + FileSystem::replaceFileStr("{$lib->getSourceDir()}/src/osdep/unix/Makefile", 'CC=cc', "CC={$cc}"); + /* FileSystem::replaceFileStr($lib->getSourceDir() . '/src/osdep/unix/Makefile', '-lcrypto -lz', '-lcrypto'); + FileSystem::replaceFileStr($lib->getSourceDir() . '/src/osdep/unix/Makefile', '-lcrypto', '-lcrypto -lz'); + FileSystem::replaceFileStr( + $lib->getSourceDir() . '/src/osdep/unix/ssl_unix.c', + "#include \n#include ", + "#include \n#include " + ); + // SourcePatcher::patchFile('1006_openssl1.1_autoverify.patch', $lib->getSourceDir()); + SourcePatcher::patchFile('2014_openssl1.1.1_sni.patch', $lib->getSourceDir()); */ + FileSystem::replaceFileStr("{$lib->getSourceDir()}/Makefile", 'SSLINCLUDE=/usr/include/openssl', "SSLINCLUDE={$lib->getIncludeDir()}"); + FileSystem::replaceFileStr("{$lib->getSourceDir()}/Makefile", 'SSLLIB=/usr/lib', "SSLLIB={$lib->getLibDir()}"); + } elseif (SystemTarget::getTargetOS() === 'Darwin') { + $cc = getenv('CC') ?: 'clang'; + SourcePatcher::patchFile('0001_imap_macos.patch', $lib->getSourceDir()); + FileSystem::replaceFileStr($lib->getSourceDir() . '/src/osdep/unix/Makefile', 'CC=cc', "CC={$cc}"); + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'SSLINCLUDE=/usr/include/openssl', 'SSLINCLUDE=' . $lib->getIncludeDir()); + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'SSLLIB=/usr/lib', 'SSLLIB=' . $lib->getLibDir()); + } + } + + #[BuildFor('Linux')] + public function buildLinux(LibraryPackage $lib, PackageInstaller $installer): void + { + if ($installer->isPackageResolved('openssl')) { + $ssl_options = "SPECIALAUTHENTICATORS=ssl SSLTYPE=unix.nopwd SSLINCLUDE={$lib->getIncludeDir()} SSLLIB={$lib->getLibDir()}"; + } else { + $ssl_options = 'SSLTYPE=none'; + } + $libcVer = SystemTarget::getLibcVersion(); + $extraLibs = $libcVer && version_compare($libcVer, '2.17', '<=') ? 'EXTRALDFLAGS="-ldl -lrt -lpthread"' : ''; + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec("yes | make slx {$ssl_options} EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + try { + shell() + ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.c {$lib->getLibDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.h {$lib->getIncludeDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/src/osdep/unix/*.h {$lib->getIncludeDir()}/"); + } catch (\Throwable) { + // last command throws an exception, no idea why since it works + } + } + + #[BuildFor('Darwin')] + public function buildDarwin(LibraryPackage $lib, PackageInstaller $installer): void + { + if ($installer->isPackageResolved('openssl')) { + $ssl_options = "SPECIALAUTHENTICATORS=ssl SSLTYPE=unix.nopwd SSLINCLUDE={$lib->getIncludeDir()} SSLLIB={$lib->getLibDir()}"; + } else { + $ssl_options = 'SSLTYPE=none'; + } + $out = shell()->execWithResult('echo "-include $(xcrun --show-sdk-path)/usr/include/poll.h -include $(xcrun --show-sdk-path)/usr/include/time.h -include $(xcrun --show-sdk-path)/usr/include/utime.h"')[1][0]; + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec( + "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" + ); + try { + shell() + ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.c {$lib->getLibDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.h {$lib->getIncludeDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/src/osdep/unix/*.h {$lib->getIncludeDir()}/"); + } catch (\Throwable) { + // last command throws an exception, no idea why since it works + } + } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index a01d29fcd..80ea1aef7 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -13,6 +13,7 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\BuildRootTracker; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; @@ -42,6 +43,9 @@ class PackageInstaller /** @var bool Whether to download missing sources automatically */ protected bool $download = true; + /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */ + protected ?BuildRootTracker $tracker = null; + public function __construct(protected array $options = []) { ApplicationContext::set(PackageInstaller::class, $this); @@ -53,6 +57,11 @@ public function __construct(protected array $options = []) if (!empty($options['no-download'])) { $this->download = false; } + + // Initialize BuildRootTracker if tracking is enabled (default: enabled unless --no-tracker) + if (empty($options['no-tracker'])) { + $this->tracker = new BuildRootTracker(); + } } /** @@ -111,6 +120,16 @@ public function setDownload(bool $download = true): static return $this; } + /** + * Get the BuildRootTracker instance. + * + * @return null|BuildRootTracker The tracker instance or null if tracking is disabled + */ + public function getTracker(): ?BuildRootTracker + { + return $this->tracker; + } + public function printBuildPackageOutputs(): void { foreach ($this->build_packages as $package) { @@ -183,8 +202,14 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { + // Start tracking for binary installation + $this->tracker?->startTracking($package, 'install'); $status = $this->installBinary($package); + // Stop tracking and record changes + $this->tracker?->stopTracking(); } catch (\Throwable $e) { + // Stop tracking on error + $this->tracker?->stopTracking(); if ($interactive) { InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; @@ -199,6 +224,9 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { + // Start tracking for build + $this->tracker?->startTracking($package, 'build'); + if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { $dirdiff = new DirDiff(BUILD_ROOT_PATH, false); ApplicationContext::set(DirDiff::class, $dirdiff); @@ -209,7 +237,12 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { $package->runStage('packPrebuilt'); } + + // Stop tracking and record changes + $this->tracker?->stopTracking(); } catch (\Throwable $e) { + // Stop tracking on error + $this->tracker?->stopTracking(); if ($interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; diff --git a/src/StaticPHP/Util/BuildRootTracker.php b/src/StaticPHP/Util/BuildRootTracker.php new file mode 100644 index 000000000..306bf90ca --- /dev/null +++ b/src/StaticPHP/Util/BuildRootTracker.php @@ -0,0 +1,189 @@ +}> Tracking data */ + protected array $tracking_data = []; + + protected static string $tracker_file = BUILD_ROOT_PATH . '/.spc-tracker.json'; + + protected ?DirDiff $current_diff = null; + + protected ?string $current_package = null; + + protected ?string $current_type = null; + + public function __construct() + { + $this->loadTrackingData(); + } + + /** + * Start tracking for a package. + * + * @param Package $package The package to track + * @param string $type The operation type: 'build' or 'install' + */ + public function startTracking(Package $package, string $type = 'build'): void + { + $this->current_package = $package->getName(); + $this->current_type = $type; + $this->current_diff = new DirDiff(BUILD_ROOT_PATH, false); + } + + /** + * Stop tracking and record the changes. + */ + public function stopTracking(): void + { + if ($this->current_diff === null || $this->current_package === null) { + return; + } + + $increment_files = $this->current_diff->getIncrementFiles(true); + + if ($increment_files !== []) { + // Remove buildroot prefix if exists and normalize paths + $normalized_files = array_map(function ($file) { + // Remove leading slashes + return ltrim($file, '/\\'); + }, $increment_files); + + $this->tracking_data[$this->current_package] = [ + 'package' => $this->current_package, + 'type' => $this->current_type, + 'files' => array_values($normalized_files), + 'time' => date('Y-m-d H:i:s'), + ]; + + $this->saveTrackingData(); + } + + $this->current_diff = null; + $this->current_package = null; + $this->current_type = null; + } + + /** + * Get tracking data for a specific package. + * + * @param string $package_name Package name + * @return null|array Tracking data or null if not found + */ + public function getPackageTracking(string $package_name): ?array + { + return $this->tracking_data[$package_name] ?? null; + } + + /** + * Get all tracking data. + * + * @return array All tracking data + */ + public function getAllTracking(): array + { + return $this->tracking_data; + } + + /** + * Find which package introduced a specific file. + * + * @param string $file File path (relative to buildroot) + * @return null|string Package name or null if not found + */ + public function findFileSource(string $file): ?string + { + $file = ltrim($file, '/\\'); + foreach ($this->tracking_data as $package_name => $data) { + if (in_array($file, $data['files'], true)) { + return $package_name; + } + } + return null; + } + + /** + * Clear tracking data for a specific package. + * + * @param string $package_name Package name + */ + public function clearPackageTracking(string $package_name): void + { + unset($this->tracking_data[$package_name]); + $this->saveTrackingData(); + } + + /** + * Clear all tracking data. + */ + public function clearAllTracking(): void + { + $this->tracking_data = []; + $this->saveTrackingData(); + } + + /** + * Get tracking statistics. + * + * @return array{total_packages: int, total_files: int, by_type: array} + */ + public function getStatistics(): array + { + $total_files = 0; + $by_type = []; + + foreach ($this->tracking_data as $data) { + $total_files += count($data['files']); + $type = $data['type']; + $by_type[$type] = ($by_type[$type] ?? 0) + 1; + } + + return [ + 'total_packages' => count($this->tracking_data), + 'total_files' => $total_files, + 'by_type' => $by_type, + ]; + } + + /** + * Get the tracker file path. + */ + public static function getTrackerFilePath(): string + { + return self::$tracker_file; + } + + /** + * Load tracking data from file. + */ + protected function loadTrackingData(): void + { + if (is_file(self::$tracker_file)) { + $content = file_get_contents(self::$tracker_file); + $data = json_decode($content, true); + if (is_array($data)) { + $this->tracking_data = $data; + } + } + } + + /** + * Save tracking data to file. + */ + protected function saveTrackingData(): void + { + FileSystem::createDir(dirname(self::$tracker_file)); + $content = json_encode($this->tracking_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + FileSystem::writeFile(self::$tracker_file, $content); + } +} From 1eec88fd6c66bb67008ca0320d8001ac1dccbd79 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 09:59:29 +0800 Subject: [PATCH 210/682] Add reset command --- src/StaticPHP/Command/ResetCommand.php | 106 +++++++++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 2 files changed, 108 insertions(+) create mode 100644 src/StaticPHP/Command/ResetCommand.php diff --git a/src/StaticPHP/Command/ResetCommand.php b/src/StaticPHP/Command/ResetCommand.php new file mode 100644 index 000000000..4a55f792a --- /dev/null +++ b/src/StaticPHP/Command/ResetCommand.php @@ -0,0 +1,106 @@ +setDescription('Reset and clean build directories') + ->addOption('with-pkgroot', null, InputOption::VALUE_NONE, 'Also remove pkgroot directory') + ->addOption('with-download', null, InputOption::VALUE_NONE, 'Also remove downloads directory') + ->addOption('yes', 'y', InputOption::VALUE_NONE, 'Skip confirmation prompt'); + } + + public function handle(): int + { + $dirs_to_remove = [ + 'buildroot' => BUILD_ROOT_PATH, + 'source' => SOURCE_PATH, + ]; + + if ($this->input->getOption('with-pkgroot')) { + $dirs_to_remove['pkgroot'] = PKG_ROOT_PATH; + } + + if ($this->input->getOption('with-download')) { + $dirs_to_remove['downloads'] = DOWNLOAD_PATH; + } + + // Show warning + InteractiveTerm::notice('You are doing some operations that are not recoverable:'); + foreach ($dirs_to_remove as $name => $path) { + InteractiveTerm::notice("- Removing directory: {$path}"); + } + + // Confirm with user unless --yes is specified + if (!$this->input->getOption('yes')) { + if (!confirm('Are you sure you want to continue?', false)) { + InteractiveTerm::error(message: 'Reset operation cancelled.'); + return static::SUCCESS; + } + } + + // Remove directories + foreach ($dirs_to_remove as $name => $path) { + if (!is_dir($path)) { + InteractiveTerm::notice("Directory {$name} does not exist, skipping: {$path}"); + continue; + } + + InteractiveTerm::indicateProgress("Removing: {$path}"); + + if (PHP_OS_FAMILY === 'Windows') { + // Force delete on Windows to handle git directories + $this->removeDirectoryWindows($path); + } else { + // Use FileSystem::removeDir for Unix systems + FileSystem::removeDir($path); + } + + InteractiveTerm::finish("Removed: {$path}"); + } + + InteractiveTerm::notice('Reset completed.'); + return static::SUCCESS; + } + + /** + * Force remove directory on Windows + * Uses PowerShell to handle git directories and other problematic files + * + * @param string $path Directory path to remove + */ + private function removeDirectoryWindows(string $path): void + { + $path = FileSystem::convertPath($path); + + // Try using PowerShell for force deletion + $escaped_path = escapeshellarg($path); + + // Use PowerShell Remove-Item with -Force and -Recurse + $ps_cmd = "powershell -Command \"Remove-Item -Path {$escaped_path} -Recurse -Force -ErrorAction SilentlyContinue\""; + f_exec($ps_cmd, $output, $ret_code); + + // If PowerShell fails or directory still exists, try cmd rmdir + if ($ret_code !== 0 || is_dir($path)) { + $cmd_command = "rmdir /s /q {$escaped_path}"; + f_exec($cmd_command, $output, $ret_code); + } + + // Final fallback: use FileSystem::removeDir + if (is_dir($path)) { + FileSystem::removeDir($path); + } + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 22404b6d3..8608f7617 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -16,6 +16,7 @@ use StaticPHP\Command\DumpLicenseCommand; use StaticPHP\Command\ExtractCommand; use StaticPHP\Command\InstallPackageCommand; +use StaticPHP\Command\ResetCommand; use StaticPHP\Command\SPCConfigCommand; use StaticPHP\Package\TargetPackage; use StaticPHP\Registry\PackageLoader; @@ -58,6 +59,7 @@ public function __construct() new ExtractCommand(), new SPCConfigCommand(), new DumpLicenseCommand(), + new ResetCommand(), // dev commands new ShellCommand(), From 3cfab10f8517ebfe0c267d0258b0d7e8a61c2a09 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 10:05:35 +0800 Subject: [PATCH 211/682] Add libacl --- config/pkg/lib/libacl.yml | 12 ++++++++++ src/Package/Library/libacl.php | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 config/pkg/lib/libacl.yml create mode 100644 src/Package/Library/libacl.php diff --git a/config/pkg/lib/libacl.yml b/config/pkg/lib/libacl.yml new file mode 100644 index 000000000..16051c464 --- /dev/null +++ b/config/pkg/lib/libacl.yml @@ -0,0 +1,12 @@ +libacl: + type: library + artifact: + source: 'https://download.savannah.nongnu.org/releases/acl/acl-2.3.2.tar.gz' + source-mirror: 'https://mirror.souseiseki.middlendian.com/nongnu/acl/acl-2.3.2.tar.gz' + metadata: + license-files: [doc/COPYING.LGPL] + license: LGPL-2.1-or-later + static-libs@unix: + - libacl.a + depends: + - attr diff --git a/src/Package/Library/libacl.php b/src/Package/Library/libacl.php new file mode 100644 index 000000000..a74cb2d43 --- /dev/null +++ b/src/Package/Library/libacl.php @@ -0,0 +1,40 @@ +exec('libtoolize --force --copy') + ->exec('./autogen.sh || autoreconf -if') + ->configure('--disable-nls', '--disable-tests') + ->make('install-acl_h install-libacl_h install-data install-libLTLIBRARIES install-pkgincludeHEADERS install-sysincludeHEADERS install-pkgconfDATA', with_install: false); + $lib->patchPkgconfPrefix(['libacl.pc'], PKGCONF_PATCH_PREFIX); + } +} From 39a207076e51f35c9a67d56fdd32c699e67326ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 10:11:24 +0800 Subject: [PATCH 212/682] Add libargon2 --- config/pkg/lib/imagemagick.yml | 20 ++++++------- config/pkg/lib/imap.yml | 13 ++++----- config/pkg/lib/libacl.yml | 4 +-- config/pkg/lib/libargon2.yml | 14 +++++++++ config/pkg/lib/libzip.yml | 21 +++++++------- src/Package/Library/libargon2.php | 48 +++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 config/pkg/lib/libargon2.yml create mode 100644 src/Package/Library/libargon2.php diff --git a/config/pkg/lib/imagemagick.yml b/config/pkg/lib/imagemagick.yml index 17fc03e81..4c4a8e1c1 100644 --- a/config/pkg/lib/imagemagick.yml +++ b/config/pkg/lib/imagemagick.yml @@ -1,18 +1,11 @@ imagemagick: type: library artifact: - source: { - "type": "ghtar", - "repo": "ImageMagick/ImageMagick" - } + source: + type: ghtar + repo: ImageMagick/ImageMagick metadata: license-files: [LICENSE] - lang: cpp - pkg-configs: [ - "Magick++-7.Q16HDRI", - "MagickCore-7.Q16HDRI", - "MagickWand-7.Q16HDRI" - ] depends: - zlib - libjpeg @@ -28,5 +21,8 @@ imagemagick: - xz - libzip - libxml2 - - + lang: cpp + pkg-configs: + - Magick++-7.Q16HDRI + - MagickCore-7.Q16HDRI + - MagickWand-7.Q16HDRI diff --git a/config/pkg/lib/imap.yml b/config/pkg/lib/imap.yml index cb2ec4106..31e6c7e71 100644 --- a/config/pkg/lib/imap.yml +++ b/config/pkg/lib/imap.yml @@ -1,14 +1,13 @@ imap: type: library artifact: - source: { - "type": "git", - "url": "https://github.com/static-php/imap.git", - "rev": "master" - } + source: + type: git + url: 'https://github.com/static-php/imap.git' + rev: master metadata: license-files: [LICENSE] - static-libs@unix: - - libc-client.a suggests@unix: - openssl + static-libs@unix: + - libc-client.a diff --git a/config/pkg/lib/libacl.yml b/config/pkg/lib/libacl.yml index 16051c464..e67a33eca 100644 --- a/config/pkg/lib/libacl.yml +++ b/config/pkg/lib/libacl.yml @@ -6,7 +6,7 @@ libacl: metadata: license-files: [doc/COPYING.LGPL] license: LGPL-2.1-or-later - static-libs@unix: - - libacl.a depends: - attr + static-libs@unix: + - libacl.a diff --git a/config/pkg/lib/libargon2.yml b/config/pkg/lib/libargon2.yml new file mode 100644 index 000000000..dcd388819 --- /dev/null +++ b/config/pkg/lib/libargon2.yml @@ -0,0 +1,14 @@ +libargon2: + type: library + artifact: + source: + type: git + rev: master + url: 'https://github.com/static-php/phc-winner-argon2' + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + suggests: + - libsodium + static-libs@unix: + - libargon2.a diff --git a/config/pkg/lib/libzip.yml b/config/pkg/lib/libzip.yml index 3d8c02f8f..2de69d424 100644 --- a/config/pkg/lib/libzip.yml +++ b/config/pkg/lib/libzip.yml @@ -1,19 +1,13 @@ libzip: type: library artifact: - source: { - "type": "ghrel", - "repo": "nih-at/libzip", - "match": "libzip.+\\.tar\\.xz", - "prefer-stable": true - } + source: + type: ghrel + repo: nih-at/libzip + match: libzip.+\.tar\.xz + prefer-stable: true metadata: license-files: [LICENSE] - static-libs@unix: - - libzip.a - headers: - - zip.h - - zipconf.h depends@unix: - zlib suggests@unix: @@ -21,3 +15,8 @@ libzip: - xz - zstd - openssl + headers: + - zip.h + - zipconf.h + static-libs@unix: + - libzip.a diff --git a/src/Package/Library/libargon2.php b/src/Package/Library/libargon2.php new file mode 100644 index 000000000..49afceeb7 --- /dev/null +++ b/src/Package/Library/libargon2.php @@ -0,0 +1,48 @@ +getSourceDir()}/Makefile", 'LIBRARY_REL ?= lib/x86_64-linux-gnu', 'LIBRARY_REL ?= lib'); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(LibraryPackage $lib, PackageBuilder $builder): void + { + shell()->cd($lib->getSourceDir())->initializeEnv($lib) + ->exec("make PREFIX='' clean") + ->exec("make -j{$builder->concurrency} PREFIX=''") + ->exec("make install PREFIX='' DESTDIR={$lib->getBuildRootPath()}"); + + $lib->patchPkgconfPrefix(['libargon2.pc']); + + foreach (FileSystem::scanDirFiles("{$lib->getBuildRootPath()}/lib/", false, true) as $filename) { + if (str_starts_with($filename, 'libargon2') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { + unlink("{$lib->getBuildRootPath()}/lib/{$filename}"); + } + } + + if (file_exists("{$lib->getBinDir()}/argon2")) { + unlink("{$lib->getBinDir()}/argon2"); + } + } +} From fba2676d809b052b58706cee6017b536a1f053d7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 10:17:58 +0800 Subject: [PATCH 213/682] Add lint-config command to check and sort configuration files --- captainhook.json | 97 ++++++++++--------- composer.json | 1 + .../Command/Dev/LintConfigCommand.php | 50 ++++++++-- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/captainhook.json b/captainhook.json index 233e387eb..1057cb292 100644 --- a/captainhook.json +++ b/captainhook.json @@ -1,44 +1,53 @@ -{ - "pre-push": { - "enabled": true, - "actions": [ - { - "action": "php vendor/bin/phpstan analyse --memory-limit 300M" - } - ] - }, - "pre-commit": { - "enabled": true, - "actions": [ - { - "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", - "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": "php vendor/bin/phpstan analyse --memory-limit 300M" + } + ] + }, + "pre-commit": { + "enabled": true, + "actions": [ + { + "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", + "args": ["php"] + } + ] + }, + { + "action": "bin/spc dev:lint-config --check", + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\InDirectory", + "args": ["config"] + } + ] + } + ] + }, + "post-change": { + "enabled": true, + "actions": [ + { + "action": "composer install", + "options": [], + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any", + "args": [ + [ + "composer.json", + "composer.lock" + ] + ] + } + ] + } + ] + } +} diff --git a/composer.json b/composer.json index fbdbc94b8..c5470b54b 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "scripts": { "analyse": "phpstan analyse --memory-limit 300M", "cs-fix": "php-cs-fixer fix", + "lint-config": "bin/spc dev:lint-config", "test": "vendor/bin/phpunit tests/ --no-coverage", "build:phar": "vendor/bin/box compile" }, diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index 8149af14d..d0e4cfa1a 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -7,6 +7,7 @@ use StaticPHP\Command\BaseCommand; use StaticPHP\Registry\Registry; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Yaml\Yaml; #[AsCommand('dev:lint-config', 'Lint configuration file format', ['dev:sort-config'])] @@ -14,15 +15,28 @@ class LintConfigCommand extends BaseCommand { public function handle(): int { + $checkOnly = $this->input->getOption('check'); + $hasChanges = false; + // get loaded configs $loded_configs = Registry::getLoadedArtifactConfigs(); foreach ($loded_configs as $file) { - $this->sortConfigFile($file, 'artifact'); + if ($this->sortConfigFile($file, 'artifact', $checkOnly)) { + $hasChanges = true; + } } $loaded_pkg_configs = Registry::getLoadedPackageConfigs(); foreach ($loaded_pkg_configs as $file) { - $this->sortConfigFile($file, 'package'); + if ($this->sortConfigFile($file, 'package', $checkOnly)) { + $hasChanges = true; + } } + + if ($checkOnly && $hasChanges) { + $this->output->writeln('Some config files need sorting. Run "bin/spc dev:lint-config" to fix them.'); + return static::FAILURE; + } + return static::SUCCESS; } @@ -88,22 +102,27 @@ public function packageSortKey(string $a, string $b): int return $a <=> $b; } - private function sortConfigFile(mixed $file, string $config_type): void + protected function configure(): void + { + $this->addOption('check', null, InputOption::VALUE_NONE, 'Check if config files need sorting without modifying them'); + } + + private function sortConfigFile(mixed $file, string $config_type, bool $checkOnly): bool { // read file content with different extensions $content = file_get_contents($file); if ($content === false) { - $this->output->writeln("Failed to read artifact config file: {$file}"); - return; + $this->output->writeln("Failed to read config file: {$file}"); + return false; } $data = match (pathinfo($file, PATHINFO_EXTENSION)) { 'json' => json_decode($content, true), - 'yml', 'yaml' => Yaml::parse($content), // skip yaml files for now + 'yml', 'yaml' => Yaml::parse($content), default => null, }; if (!is_array($data)) { - $this->output->writeln("Invalid JSON format in artifact config file: {$file}"); - return; + $this->output->writeln("Invalid format in config file: {$file}"); + return false; } ksort($data); foreach ($data as $artifact_name => &$config) { @@ -115,7 +134,18 @@ private function sortConfigFile(mixed $file, string $config_type): void 'yml', 'yaml' => Yaml::dump($data, 4, 2), default => null, }; - file_put_contents($file, $new_content); - $this->output->writeln("Sorted artifact config file: {$file}"); + + // Check if content has changed + if ($content !== $new_content) { + if ($checkOnly) { + $this->output->writeln("File needs sorting: {$file}"); + return true; + } + file_put_contents($file, $new_content); + $this->output->writeln("Sorted config file: {$file}"); + return true; + } + + return false; } } From d999bfcd11dcfaf81d9ec192858eef14a704e482 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 10:25:43 +0800 Subject: [PATCH 214/682] Add libavif --- config/pkg/lib/libavif.yml | 11 +++++++++++ src/Package/Library/libavif.php | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 config/pkg/lib/libavif.yml create mode 100644 src/Package/Library/libavif.php diff --git a/config/pkg/lib/libavif.yml b/config/pkg/lib/libavif.yml new file mode 100644 index 000000000..0d7ae151d --- /dev/null +++ b/config/pkg/lib/libavif.yml @@ -0,0 +1,11 @@ +libavif: + type: library + artifact: + source: + type: ghtar + repo: AOMediaCodec/libavif + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + static-libs@unix: + - libavif.a diff --git a/src/Package/Library/libavif.php b/src/Package/Library/libavif.php new file mode 100644 index 000000000..87e6c650f --- /dev/null +++ b/src/Package/Library/libavif.php @@ -0,0 +1,25 @@ +addConfigureArgs('-DAVIF_LIBYUV=OFF') + ->build(); + // patch pkgconfig + $lib->patchPkgconfPrefix(['libavif.pc']); + } +} From 880bb8799b012710fbe857900b8139db38e40550 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 10:55:59 +0800 Subject: [PATCH 215/682] Add libevent and postinstall action adder for library package --- config/pkg/lib/libevent.yml | 18 ++++++ src/Package/Library/libevent.php | 81 ++++++++++++++++++++++++ src/StaticPHP/Package/LibraryPackage.php | 71 +++++++++++++++++++-- 3 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 config/pkg/lib/libevent.yml create mode 100644 src/Package/Library/libevent.php diff --git a/config/pkg/lib/libevent.yml b/config/pkg/lib/libevent.yml new file mode 100644 index 000000000..aa4d0d525 --- /dev/null +++ b/config/pkg/lib/libevent.yml @@ -0,0 +1,18 @@ +libevent: + type: library + artifact: + source: + type: ghrel + repo: libevent/libevent + match: libevent.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends@unix: + - openssl + static-libs@unix: + - libevent.a + - libevent_core.a + - libevent_extra.a + - libevent_openssl.a diff --git a/src/Package/Library/libevent.php b/src/Package/Library/libevent.php new file mode 100644 index 000000000..0e3dffe60 --- /dev/null +++ b/src/Package/Library/libevent.php @@ -0,0 +1,81 @@ +addPostinstallAction([ + 'action' => 'replace-path', + 'files' => [$cmake_file], + ]); + } + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(LibraryPackage $lib): void + { + $cmake = UnixCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DEVENT__LIBRARY_TYPE=STATIC', + '-DEVENT__DISABLE_BENCHMARK=ON', + '-DEVENT__DISABLE_THREAD_SUPPORT=ON', + '-DEVENT__DISABLE_TESTS=ON', + '-DEVENT__DISABLE_SAMPLES=ON', + '-DEVENT__DISABLE_MBEDTLS=ON ', + ); + if (version_compare(get_cmake_version(), '4.0.0', '>=')) { + $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.10'); + } + $cmake->build(); + + $lib->patchPkgconfPrefix(['libevent.pc', 'libevent_core.pc', 'libevent_extra.pc', 'libevent_openssl.pc']); + + $lib->patchPkgconfPrefix( + ['libevent_openssl.pc'], + PKGCONF_PATCH_CUSTOM, + [ + '/Libs.private:.*/m', + 'Libs.private: -lssl -lcrypto', + ] + ); + } +} diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 0567f92b8..8b392bab3 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -21,6 +21,12 @@ */ class LibraryPackage extends Package { + /** + * Custom postinstall actions for this package. + * @var array + */ + private array $customPostinstallActions = []; + public function isInstalled(): bool { foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) { @@ -52,6 +58,24 @@ public function isInstalled(): bool return true; } + /** + * Add a custom postinstall action for this package. + * Available actions: + * - replace-path: Replace placeholders with actual paths + * Example: ['action' => 'replace-path', 'files' => ['lib/cmake/xxx.cmake']] + * - replace-to-env: Replace string with environment variable value + * Example: ['action' => 'replace-to-env', 'file' => 'bin/xxx-config', 'search' => 'XXX', 'replace-env' => 'BUILD_ROOT_PATH'] + * + * @param array $action Action array with 'action' key and other required keys + */ + public function addPostinstallAction(array $action): void + { + if (!isset($action['action'])) { + throw new WrongUsageException('Postinstall action must have "action" key.'); + } + $this->customPostinstallActions[] = $action; + } + public function patchLaDependencyPrefix(?array $files = null): void { logger()->info("Patching library {$this->name} la files"); @@ -234,14 +258,49 @@ public function packPrebuilt(): void } } - // generate postinstall action file if there are files to process + // collect all postinstall actions + $postinstall_actions = []; + + // add default replace-path action if there are .pc/.la files if ($postinstall_files !== []) { - $postinstall_actions = [ - [ - 'action' => 'replace-path', - 'files' => $postinstall_files, - ], + $postinstall_actions[] = [ + 'action' => 'replace-path', + 'files' => $postinstall_files, ]; + } + + // merge custom postinstall actions and handle files for replace-path actions + foreach ($this->customPostinstallActions as $action) { + // if action is replace-path, process the files with placeholder replacement + if ($action['action'] === 'replace-path') { + $files = $action['files'] ?? []; + if (!is_array($files)) { + $files = [$files]; + } + foreach ($files as $file) { + if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { + $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); + $origin_files[$file] = $content; + // replace actual paths with placeholders + $content = str_replace( + array_keys($placeholder), + array_values($placeholder), + $content + ); + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + // ensure this file is included in the package + if (!in_array($file, $increase_files, true)) { + $increase_files[] = $file; + } + } + } + } + // add custom action to postinstall actions + $postinstall_actions[] = $action; + } + + // generate postinstall action file if there are actions to process + if ($postinstall_actions !== []) { FileSystem::writeFile($postinstall_file, json_encode($postinstall_actions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $increase_files[] = '.package.' . $this->getName() . '.postinstall.json'; } From a832cc2114d106ca6f02382ed50153ab663b7ada Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:08:16 +0800 Subject: [PATCH 216/682] Add libffi --- config/pkg/lib/libffi.yml | 16 ++++++++++++++ src/Package/Library/libffi.php | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 config/pkg/lib/libffi.yml create mode 100644 src/Package/Library/libffi.php diff --git a/config/pkg/lib/libffi.yml b/config/pkg/lib/libffi.yml new file mode 100644 index 000000000..a33956844 --- /dev/null +++ b/config/pkg/lib/libffi.yml @@ -0,0 +1,16 @@ +libffi: + type: library + artifact: + source: + type: ghrel + repo: libffi/libffi + match: libffi.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [LICENSE] + license: MIT + headers@unix: + - ffi.h + - ffitarget.h + static-libs@unix: + - libffi.a diff --git a/src/Package/Library/libffi.php b/src/Package/Library/libffi.php new file mode 100644 index 000000000..351b9076c --- /dev/null +++ b/src/Package/Library/libffi.php @@ -0,0 +1,40 @@ +configure()->make(); + + if (is_file("{$this->getBuildRootPath()}/lib64/libffi.a")) { + copy("{$this->getBuildRootPath()}/lib64/libffi.a", "{$this->getBuildRootPath()}/lib/libffi.a"); + unlink("{$this->getBuildRootPath()}/lib64/libffi.a"); + } + $this->patchPkgconfPrefix(['libffi.pc']); + } + + #[BuildFor('Darwin')] + public function buildDarwin(): void + { + $arch = getenv('SPC_ARCH'); + UnixAutoconfExecutor::create($this) + ->configure( + "--host={$arch}-apple-darwin", + "--target={$arch}-apple-darwin", + ) + ->make(); + $this->patchPkgconfPrefix(['libffi.pc']); + } +} From 272338775ef0d411da596bb97a9b44dd08081e5d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:17:37 +0800 Subject: [PATCH 217/682] Add liblz4 --- config/pkg/lib/liblz4.yml | 13 ++++++++++ src/Package/Library/liblz4.php | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 config/pkg/lib/liblz4.yml create mode 100644 src/Package/Library/liblz4.php diff --git a/config/pkg/lib/liblz4.yml b/config/pkg/lib/liblz4.yml new file mode 100644 index 000000000..298b3abf3 --- /dev/null +++ b/config/pkg/lib/liblz4.yml @@ -0,0 +1,13 @@ +liblz4: + type: library + artifact: + source: + type: ghrel + repo: lz4/lz4 + match: lz4-.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + static-libs@unix: + - liblz4.a diff --git a/src/Package/Library/liblz4.php b/src/Package/Library/liblz4.php new file mode 100644 index 000000000..fb52a4fba --- /dev/null +++ b/src/Package/Library/liblz4.php @@ -0,0 +1,46 @@ +getSourceDir() . '/programs/Makefile', 'install: lz4', "install: lz4\n\ninstallewfwef: lz4"); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(LibraryPackage $lib, PackageBuilder $builder): void + { + shell()->cd($lib->getSourceDir())->initializeEnv($lib) + ->exec("make PREFIX='' clean") + ->exec("make lib -j{$builder->concurrency} PREFIX=''"); + + FileSystem::replaceFileStr("{$lib->getSourceDir()}/Makefile", '$(MAKE) -C $(PRGDIR) $@', ''); + + shell()->cd($lib->getSourceDir()) + ->exec("make install PREFIX='' DESTDIR={$lib->getBuildRootPath()}"); + + $lib->patchPkgconfPrefix(['liblz4.pc']); + + foreach (FileSystem::scanDirFiles($lib->getLibDir(), false, true) as $filename) { + if (str_starts_with($filename, 'liblz4') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { + unlink("{$lib->getLibDir()}/{$filename}"); + } + } + } +} From e9a411cc66aa4e28b40553a41d09379e5743545c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:21:03 +0800 Subject: [PATCH 218/682] Add libmaxminddb --- config/pkg/lib/libmaxminddb.yml | 16 ++++++++++++++++ src/Package/Library/libmaxminddb.php | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 config/pkg/lib/libmaxminddb.yml create mode 100644 src/Package/Library/libmaxminddb.php diff --git a/config/pkg/lib/libmaxminddb.yml b/config/pkg/lib/libmaxminddb.yml new file mode 100644 index 000000000..a0c3a307f --- /dev/null +++ b/config/pkg/lib/libmaxminddb.yml @@ -0,0 +1,16 @@ +libmaxminddb: + type: library + artifact: + source: + type: ghrel + repo: maxmind/libmaxminddb + match: libmaxminddb-.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [LICENSE] + license: Apache-2.0 + headers: + - maxminddb.h + - maxminddb_config.h + static-libs@unix: + - libmaxminddb.a diff --git a/src/Package/Library/libmaxminddb.php b/src/Package/Library/libmaxminddb.php new file mode 100644 index 000000000..a045e4f12 --- /dev/null +++ b/src/Package/Library/libmaxminddb.php @@ -0,0 +1,26 @@ +addConfigureArgs( + '-DBUILD_TESTING=OFF', + '-DMAXMINDDB_BUILD_BINARIES=OFF', + ) + ->build(); + } +} From bd1153386512894b900ad212dfb37b54927398f8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:25:19 +0800 Subject: [PATCH 219/682] Add libmemcached --- config/pkg/lib/libmemcached.yml | 16 ++++++++++++++++ src/Package/Library/libmemcached.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 config/pkg/lib/libmemcached.yml create mode 100644 src/Package/Library/libmemcached.php diff --git a/config/pkg/lib/libmemcached.yml b/config/pkg/lib/libmemcached.yml new file mode 100644 index 000000000..5f56f215d --- /dev/null +++ b/config/pkg/lib/libmemcached.yml @@ -0,0 +1,16 @@ +libmemcached: + type: library + artifact: + source: + type: ghtagtar + repo: awesomized/libmemcached + match: 1.\d.\d + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + lang: cpp + static-libs@unix: + - libmemcached.a + - libmemcachedprotocol.a + - libmemcachedutil.a + - libhashkit.a diff --git a/src/Package/Library/libmemcached.php b/src/Package/Library/libmemcached.php new file mode 100644 index 000000000..ea632192a --- /dev/null +++ b/src/Package/Library/libmemcached.php @@ -0,0 +1,28 @@ +addConfigureArgs('-DCMAKE_INSTALL_RPATH=""') + ->build(); + } + + #[BuildFor('Darwin')] + public function buildDarwin(): void + { + UnixCMakeExecutor::create($this)->build(); + } +} From f2d389d89a14ad28597fb10fb1202bfeef40e357 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:28:16 +0800 Subject: [PATCH 220/682] Add librabbitmq --- config/pkg/lib/librabbitmq.yml | 12 ++++++++++++ src/Package/Library/librabbitmq.php | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 config/pkg/lib/librabbitmq.yml create mode 100644 src/Package/Library/librabbitmq.php diff --git a/config/pkg/lib/librabbitmq.yml b/config/pkg/lib/librabbitmq.yml new file mode 100644 index 000000000..c58b13a03 --- /dev/null +++ b/config/pkg/lib/librabbitmq.yml @@ -0,0 +1,12 @@ +librabbitmq: + type: library + artifact: + source: + type: ghtar + repo: alanxz/rabbitmq-c + prefer-stable: true + metadata: + license-files: [LICENSE] + license: MIT + depends: + - openssl diff --git a/src/Package/Library/librabbitmq.php b/src/Package/Library/librabbitmq.php new file mode 100644 index 000000000..2350ea5e8 --- /dev/null +++ b/src/Package/Library/librabbitmq.php @@ -0,0 +1,21 @@ +addConfigureArgs('-DBUILD_STATIC_LIBS=ON')->build(); + } +} From 4cfd8f4ca32b641227d61efba5140e5ea652d7b8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:45:14 +0800 Subject: [PATCH 221/682] Add librdkafka --- config/pkg/lib/librdkafka.yml | 19 ++++++++++ src/Package/Library/librdkafka.php | 59 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 config/pkg/lib/librdkafka.yml create mode 100644 src/Package/Library/librdkafka.php diff --git a/config/pkg/lib/librdkafka.yml b/config/pkg/lib/librdkafka.yml new file mode 100644 index 000000000..cb83fb43c --- /dev/null +++ b/config/pkg/lib/librdkafka.yml @@ -0,0 +1,19 @@ +librdkafka: + type: library + artifact: + source: + type: ghtar + repo: confluentinc/librdkafka + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + suggests: + - curl + - liblz4 + - openssl + - zlib + - zstd + lang: cpp + pkg-configs: + - rdkafka++-static + - rdkafka-static diff --git a/src/Package/Library/librdkafka.php b/src/Package/Library/librdkafka.php new file mode 100644 index 000000000..29571c7da --- /dev/null +++ b/src/Package/Library/librdkafka.php @@ -0,0 +1,59 @@ +getSourceDir() . '/lds-gen.py', + "funcs.append('rd_ut_coverage_check')", + '' + ); + FileSystem::replaceFileStr( + $this->getSourceDir() . '/src/rd.h', + '#error "IOV_MAX not defined"', + "#define IOV_MAX 1024\n#define __GNU__" + ); + // Fix OAuthBearer OIDC flag + FileSystem::replaceFileStr( + $this->getSourceDir() . '/src/rdkafka_conf.c', + '#ifdef WITH_OAUTHBEARER_OIDC', + '#if WITH_OAUTHBEARER_OIDC' + ); + return true; + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(): void + { + UnixCMakeExecutor::create($this) + ->optionalPackage('zstd', ...cmake_boolean_args('WITH_ZSTD')) + ->optionalPackage('curl', ...cmake_boolean_args('WITH_CURL')) + ->optionalPackage('openssl', ...cmake_boolean_args('WITH_SSL')) + ->optionalPackage('zlib', ...cmake_boolean_args('WITH_ZLIB')) + ->optionalPackage('liblz4', ...cmake_boolean_args('ENABLE_LZ4_EXT')) + ->addConfigureArgs( + '-DWITH_SASL=OFF', + '-DRDKAFKA_BUILD_STATIC=ON', + '-DRDKAFKA_BUILD_EXAMPLES=OFF', + '-DRDKAFKA_BUILD_TESTS=OFF', + ) + ->build(); + } +} From 127697b814db5f0a5714eafe36c785c50746d20f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 11:54:15 +0800 Subject: [PATCH 222/682] Add liburing --- config/pkg/lib/liburing.yml | 19 ++++++++++ src/Package/Library/liburing.php | 61 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 config/pkg/lib/liburing.yml create mode 100644 src/Package/Library/liburing.php diff --git a/config/pkg/lib/liburing.yml b/config/pkg/lib/liburing.yml new file mode 100644 index 000000000..58bda5240 --- /dev/null +++ b/config/pkg/lib/liburing.yml @@ -0,0 +1,19 @@ +liburing: + type: library + artifact: + source: + type: ghtar + repo: axboe/liburing + prefer-stable: true + metadata: + license-files: [COPYING] + license: LGPL-2.1-or-later + headers@linux: + - liburing/ + - liburing.h + pkg-configs: + - liburing + - liburing-ffi + static-libs@linux: + - liburing.a + - liburing-ffi.a diff --git a/src/Package/Library/liburing.php b/src/Package/Library/liburing.php new file mode 100644 index 000000000..ad396eac7 --- /dev/null +++ b/src/Package/Library/liburing.php @@ -0,0 +1,61 @@ +getSourceDir()}/configure", 'realpath -s', 'realpath'); + return true; + } + return false; + } + + #[BuildFor('Linux')] + public function buildLinux(ToolchainInterface $toolchain): void + { + $use_libc = !$toolchain instanceof GccNativeToolchain || version_compare(SystemTarget::getLibcVersion(), '2.30', '>='); + $make = UnixAutoconfExecutor::create($this); + + if ($use_libc) { + $make->appendEnv([ + 'CFLAGS' => '-D_GNU_SOURCE', + ]); + } + + $make + ->removeConfigureArgs( + '--disable-shared', + '--enable-static', + '--with-pic', + '--enable-pic', + ) + ->addConfigureArgs( + $use_libc ? '--use-libc' : '', + ) + ->configure() + ->make('library ENABLE_SHARED=0', 'install ENABLE_SHARED=0', with_clean: false); + + $this->patchPkgconfPrefix(); + } +} From fa1b71bebf49ce29a9375947f2112f4ff88324bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 12:26:35 +0800 Subject: [PATCH 223/682] Add libuuid --- config/pkg/lib/libuuid.yml | 14 ++++++++++++ src/Package/Library/libuuid.php | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 config/pkg/lib/libuuid.yml create mode 100644 src/Package/Library/libuuid.php diff --git a/config/pkg/lib/libuuid.yml b/config/pkg/lib/libuuid.yml new file mode 100644 index 000000000..65af3bc77 --- /dev/null +++ b/config/pkg/lib/libuuid.yml @@ -0,0 +1,14 @@ +libuuid: + type: library + artifact: + source: + type: git + url: 'https://github.com/static-php/libuuid.git' + rev: master + metadata: + license-files: [COPYING] + license: MIT + headers: + - uuid/uuid.h + static-libs@unix: + - libuuid.a diff --git a/src/Package/Library/libuuid.php b/src/Package/Library/libuuid.php new file mode 100644 index 000000000..3e49ad21f --- /dev/null +++ b/src/Package/Library/libuuid.php @@ -0,0 +1,40 @@ +toStep(2)->build(); + copy($this->getSourceDir() . '/build/libuuid.a', BUILD_LIB_PATH . '/libuuid.a'); + FileSystem::createDir(BUILD_INCLUDE_PATH . '/uuid'); + copy($this->getSourceDir() . '/uuid.h', BUILD_INCLUDE_PATH . '/uuid/uuid.h'); + $pc = FileSystem::readFile($this->getSourceDir() . '/uuid.pc.in'); + $pc = str_replace([ + '@prefix@', + '@exec_prefix@', + '@libdir@', + '@includedir@', + '@LIBUUID_VERSION@', + ], [ + BUILD_ROOT_PATH, + '${prefix}', + '${prefix}/lib', + '${prefix}/include', + '1.0.3', + ], $pc); + FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/uuid.pc', $pc); + } +} From d6af728e790420931dbf3e497e3991b6af279456 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 12:32:04 +0800 Subject: [PATCH 224/682] Add libuv --- config/pkg/lib/libuv.yml | 11 +++++++++++ src/Package/Library/libuv.php | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 config/pkg/lib/libuv.yml create mode 100644 src/Package/Library/libuv.php diff --git a/config/pkg/lib/libuv.yml b/config/pkg/lib/libuv.yml new file mode 100644 index 000000000..3c41906dc --- /dev/null +++ b/config/pkg/lib/libuv.yml @@ -0,0 +1,11 @@ +libuv: + type: library + artifact: + source: + type: ghtar + repo: libuv/libuv + metadata: + license-files: [LICENSE, LICENSE-extra] + license: MIT + static-libs@unix: + - libuv.a diff --git a/src/Package/Library/libuv.php b/src/Package/Library/libuv.php new file mode 100644 index 000000000..ed8c58381 --- /dev/null +++ b/src/Package/Library/libuv.php @@ -0,0 +1,25 @@ +addConfigureArgs('-DLIBUV_BUILD_SHARED=OFF') + ->build(); + // patch pkgconfig + $lib->patchPkgconfPrefix(['libuv-static.pc']); + } +} From 0c386e967afa29f56257c641104744a21f986ceb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 12:37:02 +0800 Subject: [PATCH 225/682] Allow shell completion for build:libs command --- src/StaticPHP/Command/BuildLibsCommand.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index a637fe7ce..63a3ad0f9 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -6,8 +6,10 @@ use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -16,7 +18,19 @@ class BuildLibsCommand extends BaseCommand { public function configure(): void { - $this->addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated'); + $this->addArgument( + 'libraries', + InputArgument::REQUIRED, + 'The library packages will be compiled, comma separated', + suggestedValues: function (CompletionInput $input) { + $packages = []; + foreach (PackageLoader::getPackages(['target', 'library']) as $name => $_) { + $packages[] = $name; + } + $val = $input->getCompletionValue(); + return array_filter($packages, fn ($name) => str_starts_with($name, $val)); + } + ); // Builder options $this->getDefinition()->addOptions([ new InputOption('with-suggests', ['L', 'E'], null, 'Resolve and install suggested packages as well'), From 017fabd55675bd42b03acf2f5a3f1afd11c52ac8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 12:46:26 +0800 Subject: [PATCH 226/682] Add libxslt --- config/pkg/lib/libxslt.yml | 15 ++++++++++ src/Package/Library/libxslt.php | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 config/pkg/lib/libxslt.yml create mode 100644 src/Package/Library/libxslt.php diff --git a/config/pkg/lib/libxslt.yml b/config/pkg/lib/libxslt.yml new file mode 100644 index 000000000..07955333a --- /dev/null +++ b/config/pkg/lib/libxslt.yml @@ -0,0 +1,15 @@ +libxslt: + type: library + artifact: + source: + type: filelist + url: 'https://download.gnome.org/sources/libxslt/1.1/' + regex: '/href="(?libxslt-(?[^"]+)\.tar\.xz)"/' + metadata: + license-files: [Copyright] + license: MIT + depends: + - libxml2 + static-libs@unix: + - libxslt.a + - libexslt.a diff --git a/src/Package/Library/libxslt.php b/src/Package/Library/libxslt.php new file mode 100644 index 000000000..11ba2bf84 --- /dev/null +++ b/src/Package/Library/libxslt.php @@ -0,0 +1,52 @@ + true, 'no_php'])->getPackageDepsConfig($lib->getName(), array_keys($installer->getResolvedPackages())); + $cpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; + $ac = UnixAutoconfExecutor::create($lib) + ->appendEnv([ + 'CFLAGS' => "-I{$lib->getIncludeDir()}", + 'LDFLAGS' => "-L{$lib->getLibDir()}", + 'LIBS' => "{$static_libs['libs']} {$cpp}", + ]) + ->addConfigureArgs( + '--without-python', + '--without-crypto', + '--without-debug', + '--without-debugger', + "--with-libxml-prefix={$lib->getBuildRootPath()}", + ); + if (getenv('SPC_LD_LIBRARY_PATH') && getenv('SPC_LIBRARY_PATH')) { + $ac->appendEnv([ + 'LD_LIBRARY_PATH' => getenv('SPC_LD_LIBRARY_PATH'), + 'LIBRARY_PATH' => getenv('SPC_LIBRARY_PATH'), + ]); + } + $ac->configure()->make(); + + $lib->patchPkgconfPrefix(['libexslt.pc', 'libxslt.pc']); + $lib->patchLaDependencyPrefix(); + $AR = getenv('AR') ?: 'ar'; + shell()->cd($lib->getLibDir()) + ->exec("{$AR} -t libxslt.a | grep '\\.a$' | xargs -n1 {$AR} d libxslt.a") + ->exec("{$AR} -t libexslt.a | grep '\\.a$' | xargs -n1 {$AR} d libexslt.a"); + } +} From b42601d288dfc507d95399e78ade9f4dd8b66680 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 13:02:24 +0800 Subject: [PATCH 227/682] Add libyaml --- config/pkg/lib/libyaml.yml | 15 +++++++++++++++ src/Package/Library/libyaml.php | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 config/pkg/lib/libyaml.yml create mode 100644 src/Package/Library/libyaml.php diff --git a/config/pkg/lib/libyaml.yml b/config/pkg/lib/libyaml.yml new file mode 100644 index 000000000..0d39e0b1d --- /dev/null +++ b/config/pkg/lib/libyaml.yml @@ -0,0 +1,15 @@ +libyaml: + type: library + artifact: + source: + type: ghrel + repo: yaml/libyaml + match: yaml-.+\.tar\.gz + prefer-stable: true + metadata: + license-files: [License] + license: MIT + headers: + - yaml.h + static-libs@unix: + - libyaml.a diff --git a/src/Package/Library/libyaml.php b/src/Package/Library/libyaml.php new file mode 100644 index 000000000..602c875c8 --- /dev/null +++ b/src/Package/Library/libyaml.php @@ -0,0 +1,21 @@ +configure()->make(); + } +} From 2874336f0edff67e397a78095e68b8ecb0cadc46 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 13:05:37 +0800 Subject: [PATCH 228/682] Add mimalloc --- config/pkg/lib/mimalloc.yml | 12 ++++++++++++ src/Package/Library/mimalloc.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 config/pkg/lib/mimalloc.yml create mode 100644 src/Package/Library/mimalloc.php diff --git a/config/pkg/lib/mimalloc.yml b/config/pkg/lib/mimalloc.yml new file mode 100644 index 000000000..4ab343ab8 --- /dev/null +++ b/config/pkg/lib/mimalloc.yml @@ -0,0 +1,12 @@ +mimalloc: + type: library + artifact: + source: + type: ghtagtar + repo: microsoft/mimalloc + match: 'v2\.\d\.[^3].*' + metadata: + license-files: [LICENSE] + license: MIT + static-libs@unix: + - libmimalloc.a diff --git a/src/Package/Library/mimalloc.php b/src/Package/Library/mimalloc.php new file mode 100644 index 000000000..fd7a73513 --- /dev/null +++ b/src/Package/Library/mimalloc.php @@ -0,0 +1,31 @@ +addConfigureArgs( + '-DMI_BUILD_SHARED=OFF', + '-DMI_BUILD_OBJECT=OFF', + '-DMI_INSTALL_TOPLEVEL=ON', + ); + if (SystemTarget::getLibc() === 'musl') { + $cmake->addConfigureArgs('-DMI_LIBC_MUSL=ON'); + } + $cmake->build(); + } +} From 9912b213f0e093c937adab8aa207db2fa9c494f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 13:29:25 +0800 Subject: [PATCH 229/682] Add net-snmp --- config/pkg/lib/net-snmp.yml | 15 +++++++++ src/Package/Library/net_snmp.php | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 config/pkg/lib/net-snmp.yml create mode 100644 src/Package/Library/net_snmp.php diff --git a/config/pkg/lib/net-snmp.yml b/config/pkg/lib/net-snmp.yml new file mode 100644 index 000000000..b1e9912ec --- /dev/null +++ b/config/pkg/lib/net-snmp.yml @@ -0,0 +1,15 @@ +net-snmp: + type: library + artifact: + source: + type: ghtagtar + repo: net-snmp/net-snmp + metadata: + license-files: [COPYING] + license: 'BSD-3-Clause AND MIT' + depends: + - openssl + - zlib + pkg-configs: + - netsnmp + - netsnmp-agent diff --git a/src/Package/Library/net_snmp.php b/src/Package/Library/net_snmp.php new file mode 100644 index 000000000..cb7b2aaea --- /dev/null +++ b/src/Package/Library/net_snmp.php @@ -0,0 +1,57 @@ +getSourceDir()}/configure", 'LIBS="-lssl ${OPENSSL_LIBS}"', 'LIBS="-lssl ${OPENSSL_LIBS} -lpthread -ldl"'); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(LibraryPackage $lib): void + { + // use --static for PKG_CONFIG + UnixAutoconfExecutor::create($lib) + ->setEnv(['PKG_CONFIG' => getenv('PKG_CONFIG') . ' --static']) + ->configure( + '--disable-mibs', + '--without-nl', + '--disable-agent', + '--disable-applications', + '--disable-manuals', + '--disable-scripts', + '--disable-embedded-perl', + '--without-perl-modules', + '--with-out-mib-modules="if-mib host disman/event-mib ucd-snmp/diskio mibII"', + '--with-out-transports="Unix"', + '--with-mib-modules=""', + '--enable-mini-agent', + '--with-default-snmp-version="3"', + '--with-sys-contact="@@no.where"', + '--with-sys-location="Unknown"', + '--with-logfile="/var/log/snmpd.log"', + '--with-persistent-directory="/var/lib/net-snmp"', + "--with-openssl={$lib->getBuildRootPath()}", + "--with-zlib={$lib->getBuildRootPath()}", + )->make(with_install: 'installheaders installlibs install_pkgconfig'); + $lib->patchPkgconfPrefix(); + } +} From aad710ed3e2adeb4c3f0ed072128488aa9a41181 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 13:40:18 +0800 Subject: [PATCH 230/682] Add postgresql --- config/pkg/lib/postgresql.yml | 23 ++++++ src/Package/Library/postgresql.php | 119 ++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 config/pkg/lib/postgresql.yml diff --git a/config/pkg/lib/postgresql.yml b/config/pkg/lib/postgresql.yml new file mode 100644 index 000000000..1237b1846 --- /dev/null +++ b/config/pkg/lib/postgresql.yml @@ -0,0 +1,23 @@ +postgresql: + type: library + artifact: + source: + type: ghtagtar + repo: postgres/postgres + match: REL_18_\d+ + metadata: + license-files: [COPYRIGHT] + license: PostgreSQL + depends: + - libiconv + - libxml2 + - openssl + - zlib + - libedit + suggests: + - icu + - libxslt + - ldap + - zstd + pkg-configs: + - libpq diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index bd96da2c9..84b4657e0 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -6,12 +6,22 @@ use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; +use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Exception\FileSystemException; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageBuilder; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\TargetPackage; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; +use StaticPHP\Util\PkgConfigUtil; +use StaticPHP\Util\SPCConfigUtil; #[Library('postgresql')] -class postgresql +class postgresql extends LibraryPackage { #[BeforeStage('php', [php::class, 'configureForUnix'], 'postgresql')] #[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')] @@ -20,4 +30,111 @@ public function patchBeforePHPConfigure(TargetPackage $package): void shell()->cd($package->getSourceDir()) ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); } + + #[PatchBeforeBuild] + #[PatchDescription('Various patches before building PostgreSQL')] + public function patchBeforeBuild(): bool + { + // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) + if (SystemTarget::getLibcVersion() === '2.17' && SystemTarget::getTargetArch() === 'aarch64') { + try { + FileSystem::replaceFileStr("{$this->getSourceDir()}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/src/port/pg_crc32c_armv8_choose.c", + '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', + '#if defined(__linux__) && !defined(HWCAP_CRC32)' + ); + } catch (FileSystemException) { + // allow file not-existence to make it compatible with old and new version + } + } + + // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries + FileSystem::replaceFileStr("{$this->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); + // disable shared libs build + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/src/Makefile.shlib", + [ + '$(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(LDFLAGS_SL) $(SHLIB_LINK)', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(pkglibdir)/$(shlib)\'', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(libdir)/$(shlib)\'', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(bindir)/$(shlib)\'', + ], + '' + ); + return true; + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void + { + $spc_config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); + $config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), include_suggests: $builder->getOption('with-suggests', false)); + + $env_vars = [ + 'CFLAGS' => $config['cflags'] . ' -std=c17', + 'CPPFLAGS' => '-DPIC', + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => $config['libs'], + ]; + + if ($ldLibraryPath = getenv('SPC_LD_LIBRARY_PATH')) { + $env_vars['LD_LIBRARY_PATH'] = $ldLibraryPath; + } + + FileSystem::resetDir("{$this->getSourceDir()}/build"); + + // PHP source relies on the non-private encoding functions in libpgcommon.a + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/src/common/Makefile", + '$(OBJS_FRONTEND): CPPFLAGS += -DUSE_PRIVATE_ENCODING_FUNCS', + '$(OBJS_FRONTEND): CPPFLAGS += -UUSE_PRIVATE_ENCODING_FUNCS -DFRONTEND', + ); + + // configure + $shell = shell()->cd("{$this->getSourceDir()}/build")->initializeEnv($this) + ->appendEnv($env_vars) + ->exec( + '../configure ' . + "--prefix={$this->getBuildRootPath()} " . + '--enable-coverage=no ' . + '--with-ssl=openssl ' . + '--with-readline ' . + '--with-libxml ' . + ($installer->isPackageResolved('icu') ? '--with-icu ' : '--without-icu ') . + ($installer->isPackageResolved('ldap') ? '--with-ldap ' : '--without-ldap ') . + ($installer->isPackageResolved('libxslt') ? '--with-libxslt ' : '--without-libxslt ') . + ($installer->isPackageResolved('zstd') ? '--with-zstd ' : '--without-zstd ') . + '--without-lz4 ' . + '--without-perl ' . + '--without-python ' . + '--without-pam ' . + '--without-bonjour ' . + '--without-tcl ' + ); + + // patch ldap lib + if ($installer->isPackageResolved('ldap')) { + $libs = PkgConfigUtil::getLibsArray('ldap'); + $libs = clean_spaces(implode(' ', $libs)); + FileSystem::replaceFileStr("{$this->getSourceDir()}/build/config.status", '-lldap', $libs); + FileSystem::replaceFileStr("{$this->getSourceDir()}/build/src/Makefile.global", '-lldap', $libs); + } + + $shell + ->exec('make -C src/bin/pg_config install') + ->exec('make -C src/include install') + ->exec('make -C src/common install') + ->exec('make -C src/port install') + ->exec('make -C src/interfaces/libpq install'); + + // remove dynamic libs + shell()->cd($this->getSourceDir() . '/build') + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); + + FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); + } } From 67bea25214b0b43183cfdfe114d9cb9adc202bd4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 13:55:03 +0800 Subject: [PATCH 231/682] Add qdbm --- config/pkg/lib/qdbm.yml | 12 ++++++++++++ src/Package/Library/qdbm.php | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 config/pkg/lib/qdbm.yml create mode 100644 src/Package/Library/qdbm.php diff --git a/config/pkg/lib/qdbm.yml b/config/pkg/lib/qdbm.yml new file mode 100644 index 000000000..1b46e3049 --- /dev/null +++ b/config/pkg/lib/qdbm.yml @@ -0,0 +1,12 @@ +qdbm: + type: library + artifact: + source: + type: git + url: 'https://github.com/static-php/qdbm.git' + rev: main + metadata: + license-files: [COPYING] + license: 'GPL-2.0-only OR LGPL-2.1-only' + static-libs@unix: + - libqdbm.a diff --git a/src/Package/Library/qdbm.php b/src/Package/Library/qdbm.php new file mode 100644 index 000000000..3b5c276c8 --- /dev/null +++ b/src/Package/Library/qdbm.php @@ -0,0 +1,26 @@ +configure(); + FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/MYLIBS = libqdbm.a.*/m', 'MYLIBS = libqdbm.a'); + $ac->make(SystemTarget::getTargetOS() === 'Darwin' ? 'mac' : ''); + $lib->patchPkgconfPrefix(['qdbm.pc']); + } +} From 425010fbb0f2ee86d5d484fb33d59badc9a39f29 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 13:59:58 +0800 Subject: [PATCH 232/682] Add re2c --- config/pkg/target/re2c.yml | 14 ++++++++++++++ src/Package/Target/re2c.php | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 config/pkg/target/re2c.yml create mode 100644 src/Package/Target/re2c.php diff --git a/config/pkg/target/re2c.yml b/config/pkg/target/re2c.yml new file mode 100644 index 000000000..eb4f85f31 --- /dev/null +++ b/config/pkg/target/re2c.yml @@ -0,0 +1,14 @@ +re2c: + type: target + artifact: + source: + type: ghrel + repo: skvadrik/re2c + match: re2c.+\.tar\.xz + prefer-stable: true + source-mirror: 'https://dl.static-php.dev/static-php-cli/deps/re2c/re2c-4.3.tar.xz' + metadata: + license-files: [LICENSE] + license: 'MIT OR Apache-2.0' + static-bins@unix: + - re2c diff --git a/src/Package/Target/re2c.php b/src/Package/Target/re2c.php new file mode 100644 index 000000000..c718c1d83 --- /dev/null +++ b/src/Package/Target/re2c.php @@ -0,0 +1,38 @@ +addConfigureArgs( + '-DRE2C_BUILD_TESTS=OFF', + '-DRE2C_BUILD_EXAMPLES=OFF', + '-DRE2C_BUILD_DOCS=OFF', + '-DRE2C_BUILD_RE2D=OFF', + '-DRE2C_BUILD_RE2GO=OFF', + '-DRE2C_BUILD_RE2HS=OFF', + '-DRE2C_BUILD_RE2JAVA=OFF', + '-DRE2C_BUILD_RE2JS=OFF', + '-DRE2C_BUILD_RE2OCAML=OFF', + '-DRE2C_BUILD_RE2PY=OFF', + '-DRE2C_BUILD_RE2RUST=OFF', + '-DRE2C_BUILD_RE2SWIFT=OFF', + '-DRE2C_BUILD_RE2V=OFF', + '-DRE2C_BUILD_RE2ZIG=OFF', + ) + ->build(); + } +} From 6be4da26aade2fa5d1770e3e5ac778763bc3eb97 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:03:42 +0800 Subject: [PATCH 233/682] Add readline --- config/pkg/lib/readline.yml | 14 ++++++++++++++ src/Package/Library/readline.php | 27 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 config/pkg/lib/readline.yml create mode 100644 src/Package/Library/readline.php diff --git a/config/pkg/lib/readline.yml b/config/pkg/lib/readline.yml new file mode 100644 index 000000000..0cb80b00f --- /dev/null +++ b/config/pkg/lib/readline.yml @@ -0,0 +1,14 @@ +readline: + type: library + artifact: + source: + type: filelist + url: 'https://ftp.gnu.org/pub/gnu/readline/' + regex: '/href="(?readline-(?[^"]+)\.tar\.gz)"/' + metadata: + license-files: [COPYING] + license: GPL-3.0-or-later + depends: + - ncurses + static-libs@unix: + - libreadline.a diff --git a/src/Package/Library/readline.php b/src/Package/Library/readline.php new file mode 100644 index 000000000..a32334723 --- /dev/null +++ b/src/Package/Library/readline.php @@ -0,0 +1,27 @@ +configure( + '--with-curses', + '--enable-multibyte=yes', + ) + ->make(); + $lib->patchPkgconfPrefix(['readline.pc']); + } +} From fd40b92041c8ffe633cfb584481dffed10ca75ca Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:07:04 +0800 Subject: [PATCH 234/682] Add snappy --- config/pkg/lib/snappy.yml | 20 ++++++++++++++++++++ src/Package/Library/snappy.php | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 config/pkg/lib/snappy.yml create mode 100644 src/Package/Library/snappy.php diff --git a/config/pkg/lib/snappy.yml b/config/pkg/lib/snappy.yml new file mode 100644 index 000000000..a369fa339 --- /dev/null +++ b/config/pkg/lib/snappy.yml @@ -0,0 +1,20 @@ +snappy: + type: library + artifact: + source: + type: git + rev: main + url: 'https://github.com/google/snappy' + metadata: + license-files: [COPYING] + license: BSD-3-Clause + depends: + - zlib + headers@unix: + - snappy.h + - snappy-c.h + - snappy-sinksource.h + - snappy-stubs-public.h + lang: cpp + static-libs@unix: + - libsnappy.a diff --git a/src/Package/Library/snappy.php b/src/Package/Library/snappy.php new file mode 100644 index 000000000..d822c3cfd --- /dev/null +++ b/src/Package/Library/snappy.php @@ -0,0 +1,27 @@ +setBuildDir("{$lib->getSourceDir()}/cmake/build") + ->addConfigureArgs( + '-DSNAPPY_BUILD_TESTS=OFF', + '-DSNAPPY_BUILD_BENCHMARKS=OFF', + ) + ->build('../..'); + } +} From d163c3dff68510ac9a78a9f13d388a1976ad350e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:12:06 +0800 Subject: [PATCH 235/682] Add sqlite --- config/pkg/lib/sqlite.yml | 12 ++++++++++++ src/Package/Library/sqlite.php | 22 ++++++++++++++++++++++ src/globals/licenses/sqlite.txt | 6 ++++++ 3 files changed, 40 insertions(+) create mode 100644 config/pkg/lib/sqlite.yml create mode 100644 src/Package/Library/sqlite.php create mode 100644 src/globals/licenses/sqlite.txt diff --git a/config/pkg/lib/sqlite.yml b/config/pkg/lib/sqlite.yml new file mode 100644 index 000000000..fb6eecf35 --- /dev/null +++ b/config/pkg/lib/sqlite.yml @@ -0,0 +1,12 @@ +sqlite: + type: library + artifact: + source: 'https://www.sqlite.org/2024/sqlite-autoconf-3450200.tar.gz' + metadata: + license-files: ['@/sqlite.txt'] + license: Unlicense + headers: + - sqlite3.h + - sqlite3ext.h + static-libs@unix: + - libsqlite3.a diff --git a/src/Package/Library/sqlite.php b/src/Package/Library/sqlite.php new file mode 100644 index 000000000..ae802bfa9 --- /dev/null +++ b/src/Package/Library/sqlite.php @@ -0,0 +1,22 @@ +configure()->make(); + $lib->patchPkgconfPrefix(['sqlite3.pc']); + } +} diff --git a/src/globals/licenses/sqlite.txt b/src/globals/licenses/sqlite.txt new file mode 100644 index 000000000..f68a6c175 --- /dev/null +++ b/src/globals/licenses/sqlite.txt @@ -0,0 +1,6 @@ +The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + * May you do good and not evil. + * May you find forgiveness for yourself and forgive others. + * May you share freely, never taking more than you give. From a5f8402703624f639bfefe457771d6ae5c178bef Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:16:20 +0800 Subject: [PATCH 236/682] Add tidy --- config/pkg/lib/tidy.yml | 12 ++++++++++++ src/Package/Library/tidy.php | 31 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 config/pkg/lib/tidy.yml create mode 100644 src/Package/Library/tidy.php diff --git a/config/pkg/lib/tidy.yml b/config/pkg/lib/tidy.yml new file mode 100644 index 000000000..41487c1d3 --- /dev/null +++ b/config/pkg/lib/tidy.yml @@ -0,0 +1,12 @@ +tidy: + type: library + artifact: + source: + type: ghtar + repo: htacg/tidy-html5 + prefer-stable: true + metadata: + license-files: [README/LICENSE.md] + license: W3C + static-libs@unix: + - libtidy.a diff --git a/src/Package/Library/tidy.php b/src/Package/Library/tidy.php new file mode 100644 index 000000000..b59160262 --- /dev/null +++ b/src/Package/Library/tidy.php @@ -0,0 +1,31 @@ +setBuildDir("{$lib->getSourceDir()}/build-dir") + ->addConfigureArgs( + '-DSUPPORT_CONSOLE_APP=OFF', + '-DBUILD_SHARED_LIB=OFF' + ); + if (version_compare(get_cmake_version(), '4.0.0', '>=')) { + $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.5'); + } + $cmake->build(); + $lib->patchPkgconfPrefix(['tidy.pc']); + } +} From b6d8bf563981aed577ced3b36eadaa235b95dc41 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:20:41 +0800 Subject: [PATCH 237/682] Add unixodbc --- config/pkg/lib/unixodbc.yml | 13 ++++++++++ src/Package/Library/unixodbc.php | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 config/pkg/lib/unixodbc.yml create mode 100644 src/Package/Library/unixodbc.php diff --git a/config/pkg/lib/unixodbc.yml b/config/pkg/lib/unixodbc.yml new file mode 100644 index 000000000..af98916a0 --- /dev/null +++ b/config/pkg/lib/unixodbc.yml @@ -0,0 +1,13 @@ +unixodbc: + type: library + artifact: + source: 'https://www.unixodbc.org/unixODBC-2.3.12.tar.gz' + metadata: + license-files: [COPYING] + license: LGPL-2.1-only + depends: + - libiconv + static-libs@unix: + - libodbc.a + - libodbccr.a + - libodbcinst.a diff --git a/src/Package/Library/unixodbc.php b/src/Package/Library/unixodbc.php new file mode 100644 index 000000000..e482e68be --- /dev/null +++ b/src/Package/Library/unixodbc.php @@ -0,0 +1,43 @@ + match (SystemTarget::getTargetArch()) { + 'x86_64' => '/usr/local/etc', + 'aarch64' => '/opt/homebrew/etc', + default => throw new WrongUsageException('Unsupported architecture: ' . GNU_ARCH), + }, + 'Linux' => '/etc', + default => throw new WrongUsageException("Unsupported OS: {$os}"), + }; + UnixAutoconfExecutor::create($this) + ->configure( + '--disable-debug', + '--disable-dependency-tracking', + "--with-libiconv-prefix={$this->getBuildRootPath()}", + '--with-included-ltdl', + "--sysconfdir={$sysconf_selector}", + '--enable-gui=no', + ) + ->make(); + $this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']); + $this->patchLaDependencyPrefix(); + } +} From ca9dc25f9aa7f521199df86c98221c47ecfdcebb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:26:24 +0800 Subject: [PATCH 238/682] Add watcher --- config/pkg/lib/watcher.yml | 15 +++++++++++++++ src/Package/Library/watcher.php | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 config/pkg/lib/watcher.yml create mode 100644 src/Package/Library/watcher.php diff --git a/config/pkg/lib/watcher.yml b/config/pkg/lib/watcher.yml new file mode 100644 index 000000000..6cf376f69 --- /dev/null +++ b/config/pkg/lib/watcher.yml @@ -0,0 +1,15 @@ +watcher: + type: library + artifact: + source: + type: ghtar + repo: e-dant/watcher + prefer-stable: true + metadata: + license-files: [license] + license: MIT + headers: + - wtr/watcher-c.h + lang: cpp + static-libs@unix: + - libwatcher-c.a diff --git a/src/Package/Library/watcher.php b/src/Package/Library/watcher.php new file mode 100644 index 000000000..56f93d931 --- /dev/null +++ b/src/Package/Library/watcher.php @@ -0,0 +1,32 @@ +getLibExtraCXXFlags(); + if (stripos($cflags, '-fpic') === false) { + $cflags .= ' -fPIC'; + } + $ldflags = $this->getLibExtraLdFlags() ? ' ' . $this->getLibExtraLdFlags() : ''; + shell()->cd("{$this->getSourceDir()}/watcher-c") + ->exec(getenv('CXX') . " -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra {$cflags}{$ldflags}") + ->exec(getenv('AR') . ' rcs libwatcher-c.a libwatcher-c.o'); + + copy("{$this->getSourceDir()}/watcher-c/libwatcher-c.a", "{$this->getLibDir()}/libwatcher-c.a"); + FileSystem::createDir("{$this->getIncludeDir()}/wtr"); + copy("{$this->getSourceDir()}/watcher-c/include/wtr/watcher-c.h", "{$this->getIncludeDir()}/wtr/watcher-c.h"); + } +} From 368461d1ad0baad57b3095f62785ef35d6ecb208 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:27:56 +0800 Subject: [PATCH 239/682] phpstan fix --- src/StaticPHP/Runtime/SystemTarget.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/StaticPHP/Runtime/SystemTarget.php b/src/StaticPHP/Runtime/SystemTarget.php index 584fa49a6..489f84384 100644 --- a/src/StaticPHP/Runtime/SystemTarget.php +++ b/src/StaticPHP/Runtime/SystemTarget.php @@ -88,8 +88,6 @@ public static function getTargetOS(): string /** * Returns the target architecture, e.g. x86_64, aarch64. * Currently, we only support 'x86_64' and 'aarch64' and both can only be built natively. - * - * @return 'aarch64'|'x86_64' */ public static function getTargetArch(): string { From c72a2b622845c8238c7ca4f157d4a1d564dc39d2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 14:46:57 +0800 Subject: [PATCH 240/682] Refactor nasm,php-sdk-binary-tools,strawberry-perl,vswhere --- config/pkg.target.json | 21 --------------------- config/pkg/target/nasm.yml | 5 +++++ config/pkg/target/php-sdk-binary-tools.yml | 5 +++++ config/pkg/target/strawberry-perl.yml | 5 +++++ config/pkg/target/vswhere.yml | 5 +++++ spc.registry.json | 3 +-- src/StaticPHP/Package/PackageInstaller.php | 2 +- 7 files changed, 22 insertions(+), 24 deletions(-) delete mode 100644 config/pkg.target.json create mode 100644 config/pkg/target/nasm.yml create mode 100644 config/pkg/target/php-sdk-binary-tools.yml create mode 100644 config/pkg/target/strawberry-perl.yml create mode 100644 config/pkg/target/vswhere.yml diff --git a/config/pkg.target.json b/config/pkg.target.json deleted file mode 100644 index 59c6c65ac..000000000 --- a/config/pkg.target.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "nasm": { - "type": "target", - "artifact": "nasm" - }, - "php-sdk-binary-tools": { - "type": "target", - "artifact": "php-sdk-binary-tools" - }, - "strawberry-perl": { - "type": "target", - "artifact": "strawberry-perl" - }, - "vswhere": { - "type": "target", - "artifact": "vswhere", - "static-bins@windows": [ - "vswhere.exe" - ] - } -} diff --git a/config/pkg/target/nasm.yml b/config/pkg/target/nasm.yml new file mode 100644 index 000000000..3f483e8bb --- /dev/null +++ b/config/pkg/target/nasm.yml @@ -0,0 +1,5 @@ +nasm: + type: target + artifact: + binary: + windows-x86_64: { type: url, url: 'https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip', extract: { nasm.exe: '{php_sdk_path}/bin/nasm.exe', ndisasm.exe: '{php_sdk_path}/bin/ndisasm.exe' } } diff --git a/config/pkg/target/php-sdk-binary-tools.yml b/config/pkg/target/php-sdk-binary-tools.yml new file mode 100644 index 000000000..81180007e --- /dev/null +++ b/config/pkg/target/php-sdk-binary-tools.yml @@ -0,0 +1,5 @@ +php-sdk-binary-tools: + type: target + artifact: + binary: + windows-x86_64: { type: git, rev: master, url: 'https://github.com/php/php-sdk-binary-tools.git', extract: '{php_sdk_path}' } diff --git a/config/pkg/target/strawberry-perl.yml b/config/pkg/target/strawberry-perl.yml new file mode 100644 index 000000000..9e2e3187c --- /dev/null +++ b/config/pkg/target/strawberry-perl.yml @@ -0,0 +1,5 @@ +strawberry-perl: + type: target + artifact: + binary: + 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' } diff --git a/config/pkg/target/vswhere.yml b/config/pkg/target/vswhere.yml new file mode 100644 index 000000000..d7f8670cc --- /dev/null +++ b/config/pkg/target/vswhere.yml @@ -0,0 +1,5 @@ +vswhere: + type: target + artifact: + binary: + windows-x86_64: { type: url, url: 'https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe', extract: '{pkg_root_path}/bin/vswhere.exe' } diff --git a/spc.registry.json b/spc.registry.json index cf49c6c83..b55f63a68 100644 --- a/spc.registry.json +++ b/spc.registry.json @@ -16,8 +16,7 @@ "config": [ "config/pkg/lib/", "config/pkg/target/", - "config/pkg/ext/", - "config/pkg.target.json" + "config/pkg/ext/" ] }, "artifact": { diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 80ea1aef7..169630f99 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -534,7 +534,7 @@ private function validatePackageArtifact(Package $package): void { // target and library must have at least source or platform binary if (in_array($package->getType(), ['library', 'target']) && !$package->getArtifact()?->hasSource() && !$package->getArtifact()?->hasPlatformBinary()) { - throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or platform binary defined."); + throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or current platform (" . SystemTarget::getCurrentPlatformString() . ') binary defined.'); } } From d8d9f389ba1a75df812b7a1b31d140df0aa043ea Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 16:33:13 +0800 Subject: [PATCH 241/682] Refactor patching logic for Alpine Linux and macOS in attr.php --- src/Package/Artifact/attr.php | 6 +++--- src/globals/functions.php | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Package/Artifact/attr.php b/src/Package/Artifact/attr.php index e80c8c831..9974a4a23 100644 --- a/src/Package/Artifact/attr.php +++ b/src/Package/Artifact/attr.php @@ -7,6 +7,7 @@ use StaticPHP\Artifact\Artifact; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\System\LinuxUtil; @@ -16,8 +17,7 @@ class attr #[PatchDescription('Patch attr for Alpine Linux (musl) and macOS - gethostname declaration')] public function patchAttrForAlpine(Artifact $artifact): void { - if (PHP_OS_FAMILY === 'Darwin' || PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist()) { - SourcePatcher::patchFile('attr_alpine_gethostname.patch', $artifact->getSourceDir()); - } + spc_skip_unless(SystemTarget::getTargetOS() === 'Darwin' || SystemTarget::getTargetOS() === 'Linux' && !LinuxUtil::isMuslDist(), 'Only for Alpine Linux (musl) and macOS'); + SourcePatcher::patchFile('attr_alpine_gethostname.patch', $artifact->getSourceDir()); } } diff --git a/src/globals/functions.php b/src/globals/functions.php index e04bad5e0..712cf621e 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -321,28 +321,29 @@ function get_display_path(string $path): string } /** - * Get the global DI container instance. + * Skip the current operation if the condition is true. + * You should ALWAYS use this function inside an attribute callback. * - * @deprecated Use ApplicationContext::getContainer() or dependency injection instead. - * This function is kept for backward compatibility during the migration period. + * @param bool $condition Condition to evaluate + * @param string $message Optional message for the skip exception */ -function spc_container(): DI\Container +function spc_skip_if(bool $condition, string $message = ''): void { - return \StaticPHP\DI\ApplicationContext::getContainer(); + if ($condition) { + throw new StaticPHP\Exception\SkipException($message); + } } /** - * Skip the current operation if the condition is true. + * Skip the current operation unless the condition is true. * You should ALWAYS use this function inside an attribute callback. * * @param bool $condition Condition to evaluate * @param string $message Optional message for the skip exception */ -function spc_skip_if(bool $condition, string $message = ''): void +function spc_skip_unless(bool $condition, string $message = ''): void { - if ($condition) { - throw new StaticPHP\Exception\SkipException($message); - } + spc_skip_if(!$condition, $message); } /** From 95f34fbbc35f27f29dbd28d711655414cb26d01b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 16:33:34 +0800 Subject: [PATCH 242/682] Add extension amqp --- config/pkg/ext/ext-amqp.yml | 18 ++++++++++++++++++ src/Package/Extension/amqp.php | 26 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 config/pkg/ext/ext-amqp.yml create mode 100644 src/Package/Extension/amqp.php diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml new file mode 100644 index 000000000..937569144 --- /dev/null +++ b/config/pkg/ext/ext-amqp.yml @@ -0,0 +1,18 @@ +ext-amqp: + type: php-extension + artifact: + source: + type: url + url: 'https://pecl.php.net/get/amqp' + extract: php-src/ext/amqp + filename: amqp.tgz + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - librabbitmq + depends@windows: + - ext-openssl + php-extension: + arg-type: '--with-amqp@shared_suffix@ --with-librabbitmq-dir=@build_root_path@' + arg-type@windows: '--with-amqp' diff --git a/src/Package/Extension/amqp.php b/src/Package/Extension/amqp.php new file mode 100644 index 000000000..82e3c961b --- /dev/null +++ b/src/Package/Extension/amqp.php @@ -0,0 +1,26 @@ + Date: Fri, 6 Feb 2026 16:33:50 +0800 Subject: [PATCH 243/682] Add extension bcmath,openssl,zlib --- config/pkg/ext/builtin-extensions.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 config/pkg/ext/builtin-extensions.yml diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml new file mode 100644 index 000000000..d12cd2191 --- /dev/null +++ b/config/pkg/ext/builtin-extensions.yml @@ -0,0 +1,21 @@ +ext-bcmath: + type: php-extension +ext-openssl: + type: php-extension + depends: + - openssl + - zlib + - ext-zlib + php-extension: + arg-type: custom + arg-type@windows: with + build-with-php: true +ext-zlib: + type: php-extension + depends: + - zlib + php-extension: + arg-type: custom + arg-type@windows: with + build-with-php: true + build-shared: false From 478b85879fde188ec5264c93a425a4b945ec5784 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Feb 2026 16:34:51 +0800 Subject: [PATCH 244/682] Chore --- config/pkg/lib/librabbitmq.yml | 2 + src/Package/Artifact/bzip2.php | 6 +-- src/Package/Artifact/php_src.php | 16 +++---- .../Command/SwitchPhpVersionCommand.php | 11 ++--- src/Package/Target/php.php | 24 +++++++++-- src/Package/Target/php/unix.php | 2 +- src/Package/Target/pkgconfig.php | 2 +- src/SPC/store/SourcePatcher.php | 20 ++++----- src/StaticPHP/Package/LibraryPackage.php | 38 ++-------------- src/StaticPHP/Package/PackageBuilder.php | 3 ++ src/StaticPHP/Util/GlobalPathTrait.php | 43 +++++++++++++++++++ 11 files changed, 97 insertions(+), 70 deletions(-) create mode 100644 src/StaticPHP/Util/GlobalPathTrait.php diff --git a/config/pkg/lib/librabbitmq.yml b/config/pkg/lib/librabbitmq.yml index c58b13a03..da4e98562 100644 --- a/config/pkg/lib/librabbitmq.yml +++ b/config/pkg/lib/librabbitmq.yml @@ -10,3 +10,5 @@ librabbitmq: license: MIT depends: - openssl + static-libs@unix: + - librabbitmq.a diff --git a/src/Package/Artifact/bzip2.php b/src/Package/Artifact/bzip2.php index a6a1e58e5..b3cef4155 100644 --- a/src/Package/Artifact/bzip2.php +++ b/src/Package/Artifact/bzip2.php @@ -15,10 +15,6 @@ class bzip2 #[PatchDescription('Patch bzip2 Makefile to add -fPIC flag for position-independent code')] public function patchBzip2Makefile(Artifact $artifact): void { - FileSystem::replaceFileStr( - $artifact->getSourceDir() . '/Makefile', - 'CFLAGS=-Wall', - 'CFLAGS=-fPIC -Wall' - ); + FileSystem::replaceFileStr("{$artifact->getSourceDir()}/Makefile", 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); } } diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php index 498abc66e..ae9488d69 100644 --- a/src/Package/Artifact/php_src.php +++ b/src/Package/Artifact/php_src.php @@ -4,6 +4,7 @@ namespace Package\Artifact; +use Package\Target\php; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Runtime\SystemTarget; @@ -16,9 +17,8 @@ class php_src #[PatchDescription('Patch PHP source for libxml2 2.12 compatibility on Alpine Linux')] public function patchPhpLibxml212(): void { - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { - $ver_id = intval($match[1]); + $ver_id = php::getPHPVersionID(return_null_if_failed: true); + if ($ver_id) { if ($ver_id < 80000) { SourcePatcher::patchFile('spc_fix_alpine_build_php80.patch', SOURCE_PATH . '/php-src'); return; @@ -39,9 +39,8 @@ public function patchPhpLibxml212(): void #[PatchDescription('Patch GD extension for Windows builds')] public function patchGDWin32(): void { - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { - $ver_id = intval($match[1]); + $ver_id = php::getPHPVersionID(return_null_if_failed: true); + if ($ver_id) { if ($ver_id < 80200) { // see: https://github.com/php/php-src/commit/243966177e39eb71822935042c3f13fa6c5b9eed FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gd/libgd/gdft.c', '#ifndef MSWIN32', '#ifndef _WIN32'); @@ -58,9 +57,8 @@ public function patchGDWin32(): void public function patchFfiCentos7FixO3strncmp(): void { spc_skip_if(!($ver = SystemTarget::getLibcVersion()) || version_compare($ver, '2.17', '>')); - spc_skip_if(!file_exists(SOURCE_PATH . '/php-src/main/php_version.h')); - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - spc_skip_if(preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0 && intval($match[1]) < 80316); + $ver_id = php::getPHPVersionID(return_null_if_failed: true); + spc_skip_if($ver_id === null || $ver_id < 80316); SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); } diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php index 3782a6452..a6594713d 100644 --- a/src/Package/Command/SwitchPhpVersionCommand.php +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -4,6 +4,7 @@ namespace Package\Command; +use Package\Target\php; use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\DownloaderOptions; @@ -25,7 +26,7 @@ public function configure(): void $this->addArgument( 'php-version', InputArgument::REQUIRED, - 'PHP version (e.g., 8.4, 8.3, 8.2, 8.1, 8.0, 7.4, or specific like 8.4.5)', + 'PHP version (e.g., ' . implode(', ', php::SUPPORTED_MAJOR_VERSIONS) . ' or specific like 8.4.5)', ); // Downloader options @@ -42,7 +43,7 @@ public function handle(): int // Validate version format if (!$this->isValidPhpVersion($php_ver)) { $this->output->writeln("Invalid PHP version '{$php_ver}'!"); - $this->output->writeln('Supported formats: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, or specific version like 8.4.5'); + $this->output->writeln('Supported formats: ' . implode(', ', php::SUPPORTED_MAJOR_VERSIONS) . ', or specific version like 8.4.5'); return static::FAILURE; } @@ -101,13 +102,13 @@ public function handle(): int * Validate PHP version format. * * Accepts: - * - Major.Minor format: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 - * - Full version format: 8.4.5, 8.3.12, etc. + * - Major.Minor format, e.g. 7.4 + * - Full version format, e.g. 8.4.5, 8.3.12, etc. */ private function isValidPhpVersion(string $version): bool { // Check major.minor format (e.g., 8.4) - if (in_array($version, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'], true)) { + if (in_array($version, php::SUPPORTED_MAJOR_VERSIONS, true)) { return true; } diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 32e8a13b1..239fcf43b 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -43,18 +43,34 @@ class php extends TargetPackage use unix; use windows; - public static function getPHPVersionID(): int + /** @var string[] Supported major PHP versions */ + public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; + + /** + * Get PHP version ID from php_version.h + * + * @param null|string $from_custom_source Where to read php_version.h from custom source + * @param bool $return_null_if_failed Whether to return null if failed to get version ID + * @return null|int PHP version ID (e.g., 80400 for PHP 8.4.0) or null if failed + */ + public static function getPHPVersionID(?string $from_custom_source = null, bool $return_null_if_failed = false): ?int { - $artifact = ArtifactLoader::getArtifactInstance('php-src'); - if (!file_exists("{$artifact->getSourceDir()}/main/php_version.h")) { + $source_dir = $from_custom_source ?? ArtifactLoader::getArtifactInstance('php-src')->getSourceDir(); + if (!file_exists("{$source_dir}/main/php_version.h")) { + if ($return_null_if_failed) { + return null; + } throw new WrongUsageException('PHP source files are not available, you need to download them first'); } - $file = file_get_contents("{$artifact->getSourceDir()}/main/php_version.h"); + $file = file_get_contents("{$source_dir}/main/php_version.h"); if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { return intval($match[1]); } + if ($return_null_if_failed) { + return null; + } throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); } diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index edbc6dd23..18946ad4f 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -262,7 +262,7 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta UnixUtil::exportDynamicSymbols($libphp_a); // deploy embed php scripts - $package->runStage([$this, 'patchEmbedScripts']); + $package->runStage([$this, 'patchUnixEmbedScripts']); } #[Stage] diff --git a/src/Package/Target/pkgconfig.php b/src/Package/Target/pkgconfig.php index e99e2d7c0..aa75fc881 100644 --- a/src/Package/Target/pkgconfig.php +++ b/src/Package/Target/pkgconfig.php @@ -40,6 +40,6 @@ public function build(TargetPackage $package, ToolchainInterface $toolchain): vo ) ->make(with_install: 'install-exec'); - shell()->exec('strip ' . BUILD_ROOT_PATH . '/bin/pkg-config'); + shell()->exec("strip {$package->getBinDir()}/pkg-config"); } } diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index 0068f53e3..0a1e02998 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -18,22 +18,22 @@ class SourcePatcher public static function init(): void { // FileSystem::addSourceExtractHook('swow', [__CLASS__, 'patchSwow']); - FileSystem::addSourceExtractHook('openssl', [__CLASS__, 'patchOpenssl11Darwin']); + FileSystem::addSourceExtractHook('openssl', [__CLASS__, 'patchOpenssl11Darwin']); // migrated FileSystem::addSourceExtractHook('swoole', [__CLASS__, 'patchSwoole']); - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchPhpLibxml212']); - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchGDWin32']); - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); + FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchPhpLibxml212']); // migrated + FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchGDWin32']); // migrated + FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); // migrated FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']); FileSystem::addSourceExtractHook('yaml', [__CLASS__, 'patchYamlWin32']); - FileSystem::addSourceExtractHook('libyaml', [__CLASS__, 'patchLibYaml']); - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchImapLicense']); + FileSystem::addSourceExtractHook('libyaml', [__CLASS__, 'patchLibYaml']); // removed + FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchImapLicense']); // migrated FileSystem::addSourceExtractHook('ext-imagick', [__CLASS__, 'patchImagickWith84']); - FileSystem::addSourceExtractHook('libaom', [__CLASS__, 'patchLibaomForAlpine']); - FileSystem::addSourceExtractHook('pkg-config', [__CLASS__, 'patchPkgConfigForGcc15']); - FileSystem::addSourceExtractHook('attr', [__CLASS__, 'patchAttrForAlpine']); - FileSystem::addSourceExtractHook('gmssl', [__CLASS__, 'patchGMSSL']); + FileSystem::addSourceExtractHook('libaom', [__CLASS__, 'patchLibaomForAlpine']); // migrated + FileSystem::addSourceExtractHook('pkg-config', [__CLASS__, 'patchPkgConfigForGcc15']); // migrated + FileSystem::addSourceExtractHook('attr', [__CLASS__, 'patchAttrForAlpine']); // migrated + FileSystem::addSourceExtractHook('gmssl', [__CLASS__, 'patchGMSSL']); // migrated } public static function patchBeforeBuildconf(BuilderBase $builder): void diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 8b392bab3..aa24f057c 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -14,6 +14,7 @@ use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\GlobalPathTrait; use StaticPHP\Util\SPCConfigUtil; /** @@ -21,6 +22,8 @@ */ class LibraryPackage extends Package { + use GlobalPathTrait; + /** * Custom postinstall actions for this package. * @var array @@ -369,41 +372,6 @@ public function getLibExtraLibs(): string return getenv($this->getSnakeCaseName() . '_LIBS') ?: ''; } - /** - * Get the build root path for the package. - * - * TODO: Can be changed to support per-package build root path in the future. - */ - public function getBuildRootPath(): string - { - return BUILD_ROOT_PATH; - } - - /** - * Get the include directory for the package. - * - * TODO: Can be changed to support per-package include directory in the future. - */ - public function getIncludeDir(): string - { - return BUILD_INCLUDE_PATH; - } - - /** - * Get the library directory for the package. - * - * TODO: Can be changed to support per-package library directory in the future. - */ - public function getLibDir(): string - { - return BUILD_LIB_PATH; - } - - public function getBinDir(): string - { - return BUILD_BIN_PATH; - } - /** * Get tar compress options from suffix * diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 5d603625c..c9b1051f9 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -11,11 +11,14 @@ use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\GlobalPathTrait; use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\System\LinuxUtil; class PackageBuilder { + use GlobalPathTrait; + /** @var int make jobs count */ public readonly int $concurrency; diff --git a/src/StaticPHP/Util/GlobalPathTrait.php b/src/StaticPHP/Util/GlobalPathTrait.php new file mode 100644 index 000000000..f94c501bc --- /dev/null +++ b/src/StaticPHP/Util/GlobalPathTrait.php @@ -0,0 +1,43 @@ + Date: Fri, 6 Feb 2026 16:38:03 +0800 Subject: [PATCH 245/682] Support define php extension arg-type in config --- src/Package/Artifact/gmssl.php | 20 ++++++++ src/Package/Artifact/libaom.php | 22 ++++++++ src/Package/Artifact/pkg_config.php | 19 +++++++ src/Package/Extension/openssl.php | 50 +++++++++++++++++++ src/StaticPHP/Package/PhpExtensionPackage.php | 17 +++++-- 5 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 src/Package/Artifact/gmssl.php create mode 100644 src/Package/Artifact/libaom.php create mode 100644 src/Package/Artifact/pkg_config.php create mode 100644 src/Package/Extension/openssl.php diff --git a/src/Package/Artifact/gmssl.php b/src/Package/Artifact/gmssl.php new file mode 100644 index 000000000..4caa4d1e2 --- /dev/null +++ b/src/Package/Artifact/gmssl.php @@ -0,0 +1,20 @@ += 80400 ? '' : ' --with-openssl-dir=' . BUILD_ROOT_PATH; + $args = '--with-openssl=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH . $openssl_dir; + if (php::getPHPVersionID() >= 80500 || (php::getPHPVersionID() >= 80400 && !$builder->getOption('enable-zts'))) { + $args .= ' --with-openssl-argon2 OPENSSL_LIBS="-lz"'; + } + return $args; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(PackageBuilder $builder): string + { + $args = '--with-openssl'; + if (php::getPHPVersionID() >= 80500 || (php::getPHPVersionID() >= 80400 && !$builder->getOption('enable-zts'))) { + $args .= ' --with-openssl-argon2'; + } + return $args; + } +} diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 84aa3020d..3f2f18cf3 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -79,7 +79,7 @@ public function getPhpConfigureArg(string $os, bool $shared): string return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', $this->getExtensionName()); + $name = str_replace('_', '-', $this->getName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -89,13 +89,22 @@ public function getPhpConfigureArg(string $os, bool $shared): string default => $ext_config['arg-type'] ?? 'enable', }; - return match ($arg_type) { + $arg = match ($arg_type) { 'enable' => $shared ? "--enable-{$name}=shared" : "--enable-{$name}", 'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}", 'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}", 'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}", - default => throw new WrongUsageException("Unknown argument type '{$arg_type}' for PHP extension '{$name}'"), + 'custom' => '', + default => $arg_type, }; + // customize argument from config string + $replace = get_pack_replace(); + $arg = str_replace(array_values($replace), array_keys($replace), $arg); + $replace = [ + '@shared_suffix@' => $shared ? '=shared' : '', + '@shared_path_suffix@' => $shared ? "=shared,{$escapedPath}" : "={$escapedPath}", + ]; + return str_replace(array_keys($replace), array_values($replace), $arg); } public function setBuildShared(bool $build_shared = true): void @@ -233,7 +242,7 @@ public function registerDefaultStages(): void { // Add build stages for shared build on Unix-like systems // TODO: Windows shared build support - if ($this->build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { + if ((PackageConfig::get($this->getName(), 'php-extension')['build-shared'] ?? true) && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { if (!$this->hasStage('build')) { $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, 'buildSharedForUnix']); } From 041b08f10f39f34b31a12a1ba6e87a6176f38969 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 6 Feb 2026 17:02:36 +0800 Subject: [PATCH 246/682] V3 fix/phpunit (#1024) --- src/StaticPHP/Registry/Registry.php | 178 +++++++++--------- tests/StaticPHP/Config/ArtifactConfigTest.php | 30 +-- .../StaticPHP/Config/ConfigValidatorTest.php | 2 +- tests/StaticPHP/Config/PackageConfigTest.php | 47 ++--- tests/StaticPHP/DI/CallbackInvokerTest.php | 9 +- 5 files changed, 137 insertions(+), 129 deletions(-) diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index a753c122c..4075b70b4 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -7,6 +7,7 @@ use StaticPHP\Config\ArtifactConfig; use StaticPHP\Config\PackageConfig; use StaticPHP\ConsoleApplication; +use StaticPHP\Exception\FileSystemException; use StaticPHP\Exception\RegistryException; use StaticPHP\Util\FileSystem; use Symfony\Component\Yaml\Yaml; @@ -87,116 +88,121 @@ public static function loadRegistry(string $registry_file, bool $auto_require = self::$current_registry_name = $registry_name; - // Load composer autoload if specified (for external registries with their own dependencies) - if (isset($data['autoload']) && is_string($data['autoload'])) { - $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file)); - if (file_exists($autoload_path)) { - logger()->debug("Loading external autoload from: {$autoload_path}"); - require_once $autoload_path; - } else { - logger()->warning("Autoload file not found: {$autoload_path}"); + try { + // Load composer autoload if specified (for external registries with their own dependencies) + if (isset($data['autoload']) && is_string($data['autoload'])) { + $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file)); + if (file_exists($autoload_path)) { + logger()->debug("Loading external autoload from: {$autoload_path}"); + require_once $autoload_path; + } else { + logger()->warning("Autoload file not found: {$autoload_path}"); + } } - } - // load package configs - if (isset($data['package']['config']) && is_array($data['package']['config'])) { - foreach ($data['package']['config'] as $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - if (is_file($path)) { - self::$loaded_package_configs[] = PackageConfig::loadFromFile($path, $registry_name); - } elseif (is_dir($path)) { - self::$loaded_package_configs = array_merge(self::$loaded_package_configs, PackageConfig::loadFromDir($path, $registry_name)); + // load package configs + if (isset($data['package']['config']) && is_array($data['package']['config'])) { + foreach ($data['package']['config'] as $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + if (is_file($path)) { + self::$loaded_package_configs[] = PackageConfig::loadFromFile($path, $registry_name); + } elseif (is_dir($path)) { + self::$loaded_package_configs = array_merge(self::$loaded_package_configs, PackageConfig::loadFromDir($path, $registry_name)); + } } } - } - // load artifact configs - if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) { - foreach ($data['artifact']['config'] as $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - if (is_file($path)) { - self::$loaded_artifact_configs[] = ArtifactConfig::loadFromFile($path, $registry_name); - } elseif (is_dir($path)) { - self::$loaded_package_configs = array_merge(self::$loaded_package_configs, ArtifactConfig::loadFromDir($path, $registry_name)); + // load artifact configs + if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) { + foreach ($data['artifact']['config'] as $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + if (is_file($path)) { + self::$loaded_artifact_configs[] = ArtifactConfig::loadFromFile($path, $registry_name); + } elseif (is_dir($path)) { + self::$loaded_package_configs = array_merge(self::$loaded_package_configs, ArtifactConfig::loadFromDir($path, $registry_name)); + } } } - } - // load doctor items from PSR-4 directories - if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { - foreach ($data['doctor']['psr-4'] as $namespace => $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + // load doctor items from PSR-4 directories + if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { + foreach ($data['doctor']['psr-4'] as $namespace => $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } } - } - // load doctor items from specific classes - // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} - if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) { - foreach ($data['doctor']['classes'] as $key => $value) { - [$class, $file] = self::parseClassEntry($key, $value); - self::requireClassFile($class, $file, dirname($registry_file), $auto_require); - DoctorLoader::loadFromClass($class); + // load doctor items from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) { + foreach ($data['doctor']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + DoctorLoader::loadFromClass($class); + } } - } - // load packages from PSR-4 directories - if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) { - foreach ($data['package']['psr-4'] as $namespace => $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + // load packages from PSR-4 directories + if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) { + foreach ($data['package']['psr-4'] as $namespace => $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load packages from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['package']['classes']) && is_array($data['package']['classes'])) { + foreach ($data['package']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + PackageLoader::loadFromClass($class); + } } - } - // load packages from specific classes - // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} - if (isset($data['package']['classes']) && is_array($data['package']['classes'])) { - foreach ($data['package']['classes'] as $key => $value) { - [$class, $file] = self::parseClassEntry($key, $value); - self::requireClassFile($class, $file, dirname($registry_file), $auto_require); - PackageLoader::loadFromClass($class); + // load artifacts from PSR-4 directories + if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) { + foreach ($data['artifact']['psr-4'] as $namespace => $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } } - } - // load artifacts from PSR-4 directories - if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) { - foreach ($data['artifact']['psr-4'] as $namespace => $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + // load artifacts from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['artifact']['classes']) && is_array($data['artifact']['classes'])) { + foreach ($data['artifact']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + ArtifactLoader::loadFromClass($class); + } } - } - // load artifacts from specific classes - // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} - if (isset($data['artifact']['classes']) && is_array($data['artifact']['classes'])) { - foreach ($data['artifact']['classes'] as $key => $value) { - [$class, $file] = self::parseClassEntry($key, $value); - self::requireClassFile($class, $file, dirname($registry_file), $auto_require); - ArtifactLoader::loadFromClass($class); + // load additional commands from PSR-4 directories + if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) { + foreach ($data['command']['psr-4'] as $namespace => $path) { + $path = FileSystem::fullpath($path, dirname($registry_file)); + $classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require); + $instances = array_map(fn ($x) => new $x(), $classes); + ConsoleApplication::_addAdditionalCommands($instances); + } } - } - // load additional commands from PSR-4 directories - if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) { - foreach ($data['command']['psr-4'] as $namespace => $path) { - $path = FileSystem::fullpath($path, dirname($registry_file)); - $classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require); - $instances = array_map(fn ($x) => new $x(), $classes); + // load additional commands from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['command']['classes']) && is_array($data['command']['classes'])) { + $instances = []; + foreach ($data['command']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + $instances[] = new $class(); + } ConsoleApplication::_addAdditionalCommands($instances); } + } catch (FileSystemException $e) { + throw new RegistryException($e->getMessage(), 0, $e); } - // load additional commands from specific classes - // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} - if (isset($data['command']['classes']) && is_array($data['command']['classes'])) { - $instances = []; - foreach ($data['command']['classes'] as $key => $value) { - [$class, $file] = self::parseClassEntry($key, $value); - self::requireClassFile($class, $file, dirname($registry_file), $auto_require); - $instances[] = new $class(); - } - ConsoleApplication::_addAdditionalCommands($instances); - } self::$current_registry_name = null; } diff --git a/tests/StaticPHP/Config/ArtifactConfigTest.php b/tests/StaticPHP/Config/ArtifactConfigTest.php index dc3964881..f99903652 100644 --- a/tests/StaticPHP/Config/ArtifactConfigTest.php +++ b/tests/StaticPHP/Config/ArtifactConfigTest.php @@ -50,7 +50,7 @@ public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void $this->expectException(WrongUsageException::class); $this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load artifact config.'); - ArtifactConfig::loadFromDir('/nonexistent/path'); + ArtifactConfig::loadFromDir('/nonexistent/path', 'test'); } public function testLoadFromDirWithValidArtifactJson(): void @@ -63,7 +63,7 @@ public function testLoadFromDirWithValidArtifactJson(): void file_put_contents($this->tempDir . '/artifact.json', $artifactContent); - ArtifactConfig::loadFromDir($this->tempDir); + ArtifactConfig::loadFromDir($this->tempDir, 'test'); $config = ArtifactConfig::get('test-artifact'); $this->assertIsArray($config); @@ -88,7 +88,7 @@ public function testLoadFromDirWithMultipleArtifactFiles(): void file_put_contents($this->tempDir . '/artifact.lib.json', $artifact2Content); file_put_contents($this->tempDir . '/artifact.json', json_encode(['artifact-3' => ['source' => 'custom']])); - ArtifactConfig::loadFromDir($this->tempDir); + ArtifactConfig::loadFromDir($this->tempDir, 'test'); $this->assertNotNull(ArtifactConfig::get('artifact-1')); $this->assertNotNull(ArtifactConfig::get('artifact-2')); @@ -100,7 +100,7 @@ public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void $this->expectException(WrongUsageException::class); $this->expectExceptionMessage('Failed to read artifact config file:'); - ArtifactConfig::loadFromFile('/nonexistent/file.json'); + ArtifactConfig::loadFromFile('/nonexistent/file.json', 'test'); } public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void @@ -111,7 +111,7 @@ public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void $this->expectException(WrongUsageException::class); $this->expectExceptionMessage('Invalid JSON format in artifact config file:'); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); } public function testLoadFromFileWithValidJson(): void @@ -127,7 +127,7 @@ public function testLoadFromFileWithValidJson(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $config = ArtifactConfig::get('my-artifact'); $this->assertIsArray($config); @@ -144,7 +144,7 @@ public function testGetAllReturnsAllLoadedArtifacts(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $all = ArtifactConfig::getAll(); $this->assertIsArray($all); @@ -170,7 +170,7 @@ public function testGetReturnsConfigWhenArtifactExists(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $config = ArtifactConfig::get('test-artifact'); $this->assertIsArray($config); @@ -188,7 +188,7 @@ public function testLoadFromFileWithExpandedUrlInSource(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $config = ArtifactConfig::get('test-artifact'); $this->assertIsArray($config); @@ -208,7 +208,7 @@ public function testLoadFromFileWithBinaryCustom(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $config = ArtifactConfig::get('test-artifact'); $this->assertIsArray($config['binary']); @@ -228,7 +228,7 @@ public function testLoadFromFileWithBinaryHosted(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $config = ArtifactConfig::get('test-artifact'); $this->assertIsArray($config['binary']); @@ -253,7 +253,7 @@ public function testLoadFromFileWithBinaryPlatformSpecific(): void ]); file_put_contents($file, $content); - ArtifactConfig::loadFromFile($file); + ArtifactConfig::loadFromFile($file, 'test'); $config = ArtifactConfig::get('test-artifact'); $this->assertIsArray($config['binary']); @@ -266,7 +266,7 @@ public function testLoadFromFileWithBinaryPlatformSpecific(): void public function testLoadFromDirWithEmptyDirectory(): void { // Empty directory should not throw exception - ArtifactConfig::loadFromDir($this->tempDir); + ArtifactConfig::loadFromDir($this->tempDir, 'test'); $this->assertEquals([], ArtifactConfig::getAll()); } @@ -279,8 +279,8 @@ public function testMultipleLoadsAppendConfigs(): void file_put_contents($file1, json_encode(['art1' => ['source' => 'custom']])); file_put_contents($file2, json_encode(['art2' => ['source' => 'custom']])); - ArtifactConfig::loadFromFile($file1); - ArtifactConfig::loadFromFile($file2); + ArtifactConfig::loadFromFile($file1, 'test'); + ArtifactConfig::loadFromFile($file2, 'test'); $all = ArtifactConfig::getAll(); $this->assertCount(2, $all); diff --git a/tests/StaticPHP/Config/ConfigValidatorTest.php b/tests/StaticPHP/Config/ConfigValidatorTest.php index ae5544ae1..56f61ea63 100644 --- a/tests/StaticPHP/Config/ConfigValidatorTest.php +++ b/tests/StaticPHP/Config/ConfigValidatorTest.php @@ -582,7 +582,7 @@ public function testValidateAndLintPackagesWithAllFieldTypes(): void public function testValidateAndLintPackagesThrowsExceptionForWrongTypeString(): void { $this->expectException(ValidationException::class); - $this->expectExceptionMessage('Package test-pkg [artifact] must be string'); + $this->expectExceptionMessage('Package test-pkg [artifact] has invalid type specification'); $data = [ 'test-pkg' => [ diff --git a/tests/StaticPHP/Config/PackageConfigTest.php b/tests/StaticPHP/Config/PackageConfigTest.php index 4072e39e9..ce28aebc0 100644 --- a/tests/StaticPHP/Config/PackageConfigTest.php +++ b/tests/StaticPHP/Config/PackageConfigTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use StaticPHP\Config\PackageConfig; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\SystemTarget; @@ -49,7 +50,7 @@ public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void $this->expectException(WrongUsageException::class); $this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load pkg.json config.'); - PackageConfig::loadFromDir('/nonexistent/path'); + PackageConfig::loadFromDir('/nonexistent/path', 'test'); } public function testLoadFromDirWithValidPkgJson(): void @@ -63,7 +64,7 @@ public function testLoadFromDirWithValidPkgJson(): void file_put_contents($this->tempDir . '/pkg.json', $packageContent); - PackageConfig::loadFromDir($this->tempDir); + PackageConfig::loadFromDir($this->tempDir, 'test'); $this->assertTrue(PackageConfig::isPackageExists('test-pkg')); } @@ -87,7 +88,7 @@ public function testLoadFromDirWithMultiplePackageFiles(): void file_put_contents($this->tempDir . '/pkg.lib.json', $pkg2Content); file_put_contents($this->tempDir . '/pkg.json', json_encode(['pkg-3' => ['type' => 'virtual-target']])); - PackageConfig::loadFromDir($this->tempDir); + PackageConfig::loadFromDir($this->tempDir, 'test'); $this->assertTrue(PackageConfig::isPackageExists('pkg-1')); $this->assertTrue(PackageConfig::isPackageExists('pkg-2')); @@ -99,7 +100,7 @@ public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void $this->expectException(WrongUsageException::class); $this->expectExceptionMessage('Failed to read package config file:'); - PackageConfig::loadFromFile('/nonexistent/file.json'); + PackageConfig::loadFromFile('/nonexistent/file.json', 'test'); } public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void @@ -107,10 +108,10 @@ public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void $file = $this->tempDir . '/invalid.json'; file_put_contents($file, 'not valid json{'); - $this->expectException(WrongUsageException::class); - $this->expectExceptionMessage('Invalid JSON format in package config file:'); + $this->expectException(ValidationException::class); + $this->expectExceptionMessage('invalid.json is broken'); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); } public function testLoadFromFileWithValidJson(): void @@ -124,7 +125,7 @@ public function testLoadFromFileWithValidJson(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $this->assertTrue(PackageConfig::isPackageExists('my-pkg')); } @@ -145,7 +146,7 @@ public function testIsPackageExistsReturnsTrueWhenPackageLoaded(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $this->assertTrue(PackageConfig::isPackageExists('test-pkg')); } @@ -160,7 +161,7 @@ public function testGetAllReturnsAllLoadedPackages(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $all = PackageConfig::getAll(); $this->assertIsArray($all); @@ -189,7 +190,7 @@ public function testGetReturnsWholePackageWhenFieldNameIsNull(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $result = PackageConfig::get('test-pkg'); $this->assertIsArray($result); @@ -208,7 +209,7 @@ public function testGetReturnsFieldValueWhenFieldExists(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $result = PackageConfig::get('test-pkg', 'artifact'); $this->assertEquals('test-artifact', $result); @@ -225,7 +226,7 @@ public function testGetReturnsDefaultWhenFieldNotExists(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $result = PackageConfig::get('test-pkg', 'non-existent-field', 'default'); $this->assertEquals('default', $result); @@ -251,7 +252,7 @@ public function testGetWithSuffixFieldsOnLinux(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); // The get method will check SystemTarget::getTargetOS() // On real Linux systems, it should return 'depends@linux' first @@ -273,7 +274,7 @@ public function testGetWithSuffixFieldsReturnsBasicFieldWhenNoSuffixMatch(): voi ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $result = PackageConfig::get('test-pkg', 'depends'); $this->assertEquals(['base-dep'], $result); @@ -291,7 +292,7 @@ public function testGetWithNonSuffixedFieldIgnoresSuffixes(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); // 'artifact' is not in SUFFIX_ALLOWED_FIELDS, so it won't check suffixes $result = PackageConfig::get('test-pkg', 'artifact'); @@ -314,7 +315,7 @@ public function testGetAllSuffixAllowedFields(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); // These are all suffix-allowed fields $pkg = PackageConfig::get('test-pkg'); @@ -328,7 +329,7 @@ public function testGetAllSuffixAllowedFields(): void public function testLoadFromDirWithEmptyDirectory(): void { // Empty directory should not throw exception - PackageConfig::loadFromDir($this->tempDir); + PackageConfig::loadFromDir($this->tempDir, 'test'); $this->assertEquals([], PackageConfig::getAll()); } @@ -341,8 +342,8 @@ public function testMultipleLoadsAppendConfigs(): void file_put_contents($file1, json_encode(['pkg1' => ['type' => 'virtual-target']])); file_put_contents($file2, json_encode(['pkg2' => ['type' => 'virtual-target']])); - PackageConfig::loadFromFile($file1); - PackageConfig::loadFromFile($file2); + PackageConfig::loadFromFile($file1, 'test'); + PackageConfig::loadFromFile($file2, 'test'); $all = PackageConfig::getAll(); $this->assertCount(2, $all); @@ -366,7 +367,7 @@ public function testGetWithComplexPhpExtensionPackage(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $phpExt = PackageConfig::get('test-ext', 'php-extension'); $this->assertIsArray($phpExt); @@ -384,7 +385,7 @@ public function testGetReturnsNullAsDefaultWhenNotSpecified(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $result = PackageConfig::get('test-pkg', 'non-existent'); $this->assertNull($result); @@ -411,7 +412,7 @@ public function testLoadFromFileWithAllPackageTypes(): void ]); file_put_contents($file, $content); - PackageConfig::loadFromFile($file); + PackageConfig::loadFromFile($file, 'test'); $this->assertTrue(PackageConfig::isPackageExists('library-pkg')); $this->assertTrue(PackageConfig::isPackageExists('extension-pkg')); diff --git a/tests/StaticPHP/DI/CallbackInvokerTest.php b/tests/StaticPHP/DI/CallbackInvokerTest.php index 751a70eb9..01cacd392 100644 --- a/tests/StaticPHP/DI/CallbackInvokerTest.php +++ b/tests/StaticPHP/DI/CallbackInvokerTest.php @@ -7,6 +7,7 @@ use DI\Container; use PHPUnit\Framework\TestCase; use StaticPHP\DI\CallbackInvoker; +use StaticPHP\Exception\SPCInternalException; /** * Helper class that requires constructor parameters for testing @@ -92,7 +93,7 @@ public function testInvokeCallbackWithContainerResolution(): void // Should not resolve from container as 'test.service' is not a type // Will try default value or null - $this->expectException(\RuntimeException::class); + $this->expectException(SPCInternalException::class); $this->invoker->invoke($callback); } @@ -139,7 +140,7 @@ public function testInvokeCallbackThrowsExceptionForUnresolvableParameter(): voi return $required; }; - $this->expectException(\RuntimeException::class); + $this->expectException(SPCInternalException::class); $this->expectExceptionMessage("Cannot resolve parameter 'required' of type 'string'"); $this->invoker->invoke($callback); } @@ -527,7 +528,7 @@ public function testInvokeWithUnionTypeThrowsException(): void $callback = eval('return function (string|int $param) { return $param; };'); // Union types are not ReflectionNamedType, should not be resolved from container - $this->expectException(\RuntimeException::class); + $this->expectException(SPCInternalException::class); $this->invoker->invoke($callback); } @@ -594,7 +595,7 @@ public function testInvokeWithContainerExceptionAndNoFallback(): void return $obj; }; - $this->expectException(\RuntimeException::class); + $this->expectException(SPCInternalException::class); $this->expectExceptionMessage("Cannot resolve parameter 'obj'"); $this->invoker->invoke($callback); From 82bf317911e3b894d567f08b18ede250ecf8a807 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 6 Feb 2026 21:17:19 +0800 Subject: [PATCH 247/682] Refactor Linux and macOS tool checks for improved error handling and command execution (#1025) --- config/artifact/musl-wrapper.yml | 2 ++ src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 12 ++++++++---- src/StaticPHP/Doctor/Item/LinuxToolCheck.php | 6 +++--- src/StaticPHP/Doctor/Item/MacOSToolCheck.php | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 config/artifact/musl-wrapper.yml diff --git a/config/artifact/musl-wrapper.yml b/config/artifact/musl-wrapper.yml new file mode 100644 index 000000000..9ae3ec492 --- /dev/null +++ b/config/artifact/musl-wrapper.yml @@ -0,0 +1,2 @@ +musl-wrapper: + source: 'https://musl.libc.org/releases/musl-1.2.5.tar.gz' diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index b64df85b2..b01b7b7b6 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -73,10 +73,14 @@ public function fixMusl(): bool $prefix = 'sudo '; logger()->warning('Current user is not root, using sudo for running command'); } - shell()->cd(SOURCE_PATH . '/musl-wrapper') + $shell = shell()->cd(SOURCE_PATH . '/musl-wrapper') ->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper') - ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j') - ->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j'); + if ($prefix !== '') { + f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + } else { + $shell->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + } return true; } @@ -97,7 +101,7 @@ public function fixMuslCrossMake(): bool $downloader->add('musl-toolchain')->download(false); $extractor->extract('musl-toolchain'); $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; - shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); + f_passthru("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); FileSystem::removeDir($pkg_root); return true; } diff --git a/src/StaticPHP/Doctor/Item/LinuxToolCheck.php b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php index f4cdc3111..b44bcb9d2 100644 --- a/src/StaticPHP/Doctor/Item/LinuxToolCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php @@ -71,7 +71,7 @@ public function checkCliTools(): ?CheckResult } } if (!empty($missing)) { - return CheckResult::fail(implode(', ', $missing) . ' not installed on your system', 'install-linux-tools', [$distro, $missing]); + return CheckResult::fail(implode(', ', $missing) . ' not installed on your system', 'install-linux-tools', ['distro' => $distro, 'missing' => $missing]); } return CheckResult::ok(); } @@ -96,7 +96,7 @@ public function checkSystemOSPackages(): ?CheckResult if (LinuxUtil::isMuslDist()) { // check linux-headers installation if (!file_exists('/usr/include/linux/mman.h')) { - return CheckResult::fail('linux-headers not installed on your system', 'install-linux-tools', [LinuxUtil::getOSRelease(), ['linux-headers']]); + return CheckResult::fail('linux-headers not installed on your system', 'install-linux-tools', ['distro' => LinuxUtil::getOSRelease(), 'missing' => ['linux-headers']]); } } return CheckResult::ok(); @@ -137,7 +137,7 @@ public function fixBuildTools(array $distro, array $missing): bool $to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing; // debian, alpine libtool -> libtoolize $to_install = str_replace('libtoolize', 'libtool', $to_install); - shell()->exec($prefix . $install_cmd . ' ' . implode(' ', $to_install)); + f_passthru($prefix . $install_cmd . ' ' . implode(' ', $to_install)); return true; } diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php index d9c869ea7..b69528ad1 100644 --- a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -53,7 +53,7 @@ public function checkCliTools(): ?CheckResult } } if (!empty($missing)) { - return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]); + return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', ['missing' => $missing]); } return CheckResult::ok(); } @@ -63,7 +63,7 @@ public function checkBisonVersion(array $command_path = []): ?CheckResult { // if the bison command is /usr/bin/bison, it is the system bison that may be too old if (($bison = MacOSUtil::findCommand('bison', $command_path)) === null) { - return CheckResult::fail('bison is not installed or too old', 'build-tools', [['bison']]); + return CheckResult::fail('bison is not installed or too old', 'build-tools', ['missing' => ['bison']]); } // check version: bison (GNU Bison) x.y(.z) $version = shell()->execWithResult("{$bison} --version", false); From 6b67cb90fc1fdc9c3788f8bfbe528b6bba41082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 11 Feb 2026 16:24:13 +0100 Subject: [PATCH 248/682] fix: Postgres build with ancient libc --- src/SPC/builder/unix/library/postgresql.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index a72f3a1a6..2ad4f51be 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -4,29 +4,14 @@ namespace SPC\builder\unix\library; -use SPC\exception\FileSystemException; use SPC\store\FileSystem; use SPC\util\PkgConfigUtil; use SPC\util\SPCConfigUtil; -use SPC\util\SPCTarget; trait postgresql { public function patchBeforeBuild(): bool { - // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) - if (SPCTarget::getLibcVersion() === '2.17' && GNU_ARCH === 'aarch64') { - try { - FileSystem::replaceFileStr("{$this->source_dir}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); - FileSystem::replaceFileStr( - "{$this->source_dir}/src/port/pg_crc32c_armv8_choose.c", - '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', - '#if defined(__linux__) && !defined(HWCAP_CRC32)' - ); - } catch (FileSystemException) { - // allow file not-existence to make it compatible with old and new version - } - } // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries FileSystem::replaceFileStr("{$this->source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); // disable shared libs build From 1e4780397b5871d344b36a2c35d10f312bf53047 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 11 Feb 2026 23:32:19 +0800 Subject: [PATCH 249/682] Update test-extensions.php for PHP versions and extensions Commented out older PHP versions and Windows 2025 in the test configuration. Updated the extensions to test for Linux and Darwin. --- src/globals/test-extensions.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 8b22658ca..ba02e672d 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,10 +13,10 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', - '8.2', - '8.3', - '8.4', + // '8.1', + // '8.2', + // '8.3', + // '8.4', '8.5', // 'git', ]; @@ -26,12 +26,12 @@ // 'macos-15-intel', // bin/spc for x86_64 // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - // 'ubuntu-24.04-arm', // bin/spc for arm64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', + // 'windows-2025', ]; // whether enable thread safe @@ -50,13 +50,13 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'mysqli,gmp', + 'Linux', 'Darwin' => 'pgsql', 'Windows' => 'com_dotnet', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'grpc,mysqlnd_parsec,mysqlnd_ed25519', + 'Linux' => '', 'Darwin' => '', 'Windows' => '', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'libwebp', + 'Linux', 'Darwin' => '', 'Windows' => '', }; From 0fe1442f7e6e64c1113fc0cc4d1ef64e43124add Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Thu, 12 Feb 2026 00:02:38 +0800 Subject: [PATCH 250/682] Bump version from 2.8.0 to 2.8.2 --- src/SPC/ConsoleApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 19fdd41dc..415af40d9 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.0'; + public const string VERSION = '2.8.2'; public function __construct() { From 9a53ef34983e9c81de83ff866056f8b15849d90e Mon Sep 17 00:00:00 2001 From: Yoram Date: Fri, 13 Feb 2026 14:25:14 +0100 Subject: [PATCH 251/682] add input with-suggested-libs for build command --- .github/workflows/build-unix.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 0166bfa00..6549a94ee 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -46,6 +46,10 @@ on: description: Prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Build with suggested libs + type: boolean + default: false debug: description: Show full build logs type: boolean @@ -86,6 +90,10 @@ on: description: Prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Include suggested libs + type: boolean + default: false debug: description: Show full build logs type: boolean @@ -157,6 +165,9 @@ jobs: if [ ${{ inputs.prefer-pre-built }} == true ]; then DOWN_CMD="$DOWN_CMD --prefer-pre-built" fi + if [ ${{ inputs.with-suggested-libs }} == true ]; then + BUILD_CMD="$BUILD_CMD --with-suggested-libs" + fi if [ ${{ inputs.build-cli }} == true ]; then BUILD_CMD="$BUILD_CMD --build-cli" fi From d9834d05c6149d9e3850153690dc8b31fdde16c4 Mon Sep 17 00:00:00 2001 From: Yoram Date: Mon, 16 Feb 2026 11:22:25 +0100 Subject: [PATCH 252/682] upload debug logs on 'build php' failures --- .github/workflows/build-unix.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 6549a94ee..0f2fa172f 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -213,6 +213,14 @@ jobs: # if: ${{ failure() }} # uses: mxschmitt/action-tmate@v3 + # Upload debug logs + - if: ${{ inputs.debug && failure() }} + name: "Upload build logs on failure" + uses: actions/upload-artifact@v4 + with: + name: php-cli-logs-${{ inputs.php-version }}-${{ inputs.os }} + path: log/*.log + # Upload cli executable - if: ${{ inputs.build-cli == true }} name: "Upload PHP cli SAPI" From 661723c99a2f0fa4c71c6c9231397fd7132800d5 Mon Sep 17 00:00:00 2001 From: tricker Date: Mon, 16 Feb 2026 12:26:49 +0100 Subject: [PATCH 253/682] change logs name Co-authored-by: Marc --- .github/workflows/build-unix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 0f2fa172f..bf6df9ac4 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -218,7 +218,7 @@ jobs: name: "Upload build logs on failure" uses: actions/upload-artifact@v4 with: - name: php-cli-logs-${{ inputs.php-version }}-${{ inputs.os }} + name: spc-logs-${{ inputs.php-version }}-${{ inputs.os }} path: log/*.log # Upload cli executable From c6802996547f4ed938b2eab2c08fcc79b8df71de Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 19:12:19 +0700 Subject: [PATCH 254/682] libavif needs at least one encoder to work --- config/lib.json | 7 +++++++ src/SPC/builder/unix/library/libavif.php | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/config/lib.json b/config/lib.json index 3be972484..087c38931 100644 --- a/config/lib.json +++ b/config/lib.json @@ -373,6 +373,13 @@ ], "static-libs-windows": [ "avif.lib" + ], + "lib-suggests": [ + "libaom", + "libwebp", + "libjpeg", + "libxml2", + "libpng" ] }, "libcares": { diff --git a/src/SPC/builder/unix/library/libavif.php b/src/SPC/builder/unix/library/libavif.php index fbd4fa18c..a5b57aef2 100644 --- a/src/SPC/builder/unix/library/libavif.php +++ b/src/SPC/builder/unix/library/libavif.php @@ -11,6 +11,11 @@ trait libavif protected function build(): void { UnixCMakeExecutor::create($this) + ->optionalLib('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF') + ->optionalLib('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF') + ->optionalLib('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF') + ->optionalLib('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF') + ->optionalLib('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF') ->addConfigureArgs('-DAVIF_LIBYUV=OFF') ->build(); // patch pkgconfig From 608c915e14bc74a6adaac36400cb60f8c99157f0 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 19:14:29 +0700 Subject: [PATCH 255/682] should depend on it instead --- config/lib.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/lib.json b/config/lib.json index 087c38931..ebbf4b87b 100644 --- a/config/lib.json +++ b/config/lib.json @@ -374,8 +374,10 @@ "static-libs-windows": [ "avif.lib" ], + "lib-depends": [ + "libaom" + ], "lib-suggests": [ - "libaom", "libwebp", "libjpeg", "libxml2", From 98117c3a04b0749368b3c2f24f09c86ddf026fa3 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 19:56:59 +0700 Subject: [PATCH 256/682] remove pre built --- config/source.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/source.json b/config/source.json index 036260155..114118bb4 100644 --- a/config/source.json +++ b/config/source.json @@ -526,7 +526,7 @@ "libavif": { "type": "ghtar", "repo": "AOMediaCodec/libavif", - "provide-pre-built": true, + "provide-pre-built": false, "license": { "type": "file", "path": "LICENSE" From 5623fed37fa03aaed7e8da8e686dc6d03156a174 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 21:05:18 +0700 Subject: [PATCH 257/682] fix redownloading go-xcaddy every time --- src/SPC/ConsoleApplication.php | 2 +- src/SPC/store/pkg/GoXcaddy.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 415af40d9..750c49e4b 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.2'; + public const string VERSION = '2.8.3'; public function __construct() { diff --git a/src/SPC/store/pkg/GoXcaddy.php b/src/SPC/store/pkg/GoXcaddy.php index 93821aaa9..462342dba 100644 --- a/src/SPC/store/pkg/GoXcaddy.php +++ b/src/SPC/store/pkg/GoXcaddy.php @@ -30,8 +30,8 @@ public function getSupportName(): array public function fetch(string $name, bool $force = false, ?array $config = null): void { $pkgroot = PKG_ROOT_PATH; - $go_exec = "{$pkgroot}/{$name}/bin/go"; - $xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy"; + $go_exec = "{$pkgroot}/go-xcaddy/bin/go"; + $xcaddy_exec = "{$pkgroot}/go-xcaddy/bin/xcaddy"; if ($force) { FileSystem::removeDir("{$pkgroot}/{$name}"); } From d83a597689b79f435b3fa902defdd5825f79af70 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 21:49:30 +0700 Subject: [PATCH 258/682] unquote the string in case a shell script passes it stupidly --- src/SPC/builder/unix/UnixBuilderBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 464c9b2fc..2f192a12f 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -365,6 +365,7 @@ protected function processFrankenphpApp(): void $frankenphpAppPath = $this->getOption('with-frankenphp-app'); if ($frankenphpAppPath) { + $frankenphpAppPath = trim($frankenphpAppPath, "\"'"); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } From 18434b68f639151af3e44d550f33bc30cd31846e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Feb 2026 22:35:25 +0800 Subject: [PATCH 259/682] Add frankenphp SAPI build support --- composer.lock | 84 +++++------ config/pkg/ext/ext-phar.yml | 4 + config/pkg/target/frankenphp.yml | 18 +++ config/pkg/target/php.yml | 15 +- src/Package/Extension/phar.php | 29 ++++ src/Package/Target/go_xcaddy.php | 2 + src/Package/Target/micro.php | 7 + src/Package/Target/php.php | 32 ++++- src/Package/Target/php/frankenphp.php | 155 +++++++++++++++++++++ src/Package/Target/php/unix.php | 117 ++++++++++++---- src/StaticPHP/Package/PackageBuilder.php | 11 +- src/StaticPHP/Package/PackageInstaller.php | 9 +- 12 files changed, 393 insertions(+), 90 deletions(-) create mode 100644 config/pkg/ext/ext-phar.yml create mode 100644 config/pkg/target/frankenphp.yml create mode 100644 src/Package/Extension/phar.php create mode 100644 src/Package/Target/php/frankenphp.php diff --git a/composer.lock b/composer.lock index 40489a068..eded86efb 100644 --- a/composer.lock +++ b/composer.lock @@ -8,30 +8,30 @@ "packages": [ { "name": "laravel/prompts", - "version": "v0.3.11", + "version": "v0.3.12", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217" + "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", - "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", + "url": "https://api.github.com/repos/laravel/prompts/zipball/4861ded9003b7f8a158176a0b7666f74ee761be8", + "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.5", "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", @@ -61,33 +61,33 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.11" + "source": "https://github.com/laravel/prompts/tree/v0.3.12" }, - "time": "2026-01-27T02:55:06+00:00" + "time": "2026-02-03T06:57:26+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.8", + "version": "v2.0.9", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", - "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -124,20 +124,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-01-08T16:22:46+00:00" + "time": "2026-02-03T06:55:34+00:00" }, { "name": "nette/php-generator", - "version": "v4.2.0", + "version": "v4.2.1", "source": { "type": "git", "url": "https://github.com/nette/php-generator.git", - "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac" + "reference": "52aff4d9b12f20ca9f3e31a559b646d2fd21dd61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/4707546a1f11badd72f5d82af4f8a6bc64bd56ac", - "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac", + "url": "https://api.github.com/repos/nette/php-generator/zipball/52aff4d9b12f20ca9f3e31a559b646d2fd21dd61", + "reference": "52aff4d9b12f20ca9f3e31a559b646d2fd21dd61", "shasum": "" }, "require": { @@ -146,9 +146,9 @@ }, "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", - "nette/tester": "^2.4", + "nette/tester": "^2.6", "nikic/php-parser": "^5.0", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.0@stable", "tracy/tracy": "^2.8" }, "suggest": { @@ -194,22 +194,22 @@ ], "support": { "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v4.2.0" + "source": "https://github.com/nette/php-generator/tree/v4.2.1" }, - "time": "2025-08-06T18:24:31+00:00" + "time": "2026-02-09T05:43:31+00:00" }, { "name": "nette/utils", - "version": "v4.1.1", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" + "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", - "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", + "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", + "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", "shasum": "" }, "require": { @@ -222,7 +222,7 @@ "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", "nette/tester": "^2.5", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.0@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -283,9 +283,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.1" + "source": "https://github.com/nette/utils/tree/v4.1.2" }, - "time": "2025-12-22T12:14:32+00:00" + "time": "2026-02-03T17:21:09+00:00" }, { "name": "php-di/invoker", @@ -2217,7 +2217,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.27.5", + "version": "5.28.0", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2271,7 +2271,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.27.5" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.28.0" }, "funding": [ { @@ -2660,29 +2660,29 @@ }, { "name": "doctrine/deprecations", - "version": "1.1.5", + "version": "1.1.6", "source": { "type": "git", "url": "https://github.com/doctrine/deprecations.git", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", - "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { - "phpunit/phpunit": "<=7.5 || >=13" + "phpunit/phpunit": "<=7.5 || >=14" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^13", - "phpstan/phpstan": "1.4.10 || 2.1.11", + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", "psr/log": "^1 || ^2 || ^3" }, "suggest": { @@ -2702,9 +2702,9 @@ "homepage": "https://www.doctrine-project.org/", "support": { "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" }, - "time": "2025-04-07T20:06:18+00:00" + "time": "2026-02-07T07:09:04+00:00" }, { "name": "evenement/evenement", diff --git a/config/pkg/ext/ext-phar.yml b/config/pkg/ext/ext-phar.yml new file mode 100644 index 000000000..3625d2c00 --- /dev/null +++ b/config/pkg/ext/ext-phar.yml @@ -0,0 +1,4 @@ +ext-phar: + type: php-extension + depends: + - zlib diff --git a/config/pkg/target/frankenphp.yml b/config/pkg/target/frankenphp.yml new file mode 100644 index 000000000..96f9c082a --- /dev/null +++ b/config/pkg/target/frankenphp.yml @@ -0,0 +1,18 @@ +frankenphp: + type: target + artifact: + source: + type: ghtar + repo: php/frankenphp + prefer-stable: true + metadata: + license-files: [LICENSE] + license: MIT + depends: + - php-embed + - go-xcaddy + suggests@unix: + - brotli + - watcher + static-bins@unix: + - frankenphp diff --git a/config/pkg/target/php.yml b/config/pkg/target/php.yml index 8f88cb581..d45e509e4 100644 --- a/config/pkg/target/php.yml +++ b/config/pkg/target/php.yml @@ -1,16 +1,3 @@ -frankenphp: - type: virtual-target - artifact: - source: - type: ghtar - repo: php/frankenphp - prefer-stable: true - metadata: - license-files: [LICENSE] - license: MIT - depends: - - php-embed - - go-xcaddy php: type: target artifact: php-src @@ -32,6 +19,8 @@ php-fpm: type: virtual-target depends: - php + suggests@linux: + - libacl php-micro: type: virtual-target artifact: diff --git a/src/Package/Extension/phar.php b/src/Package/Extension/phar.php new file mode 100644 index 000000000..dd9b1dff9 --- /dev/null +++ b/src/Package/Extension/phar.php @@ -0,0 +1,29 @@ +getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'php-micro')] + #[PatchDescription('Patch Makefile to skip installing micro binary')] + public function patchMakefileBeforeUnixMake(TargetPackage $package): void + { + FileSystem::replaceFileStr("{$package->getSourceDir()}/Makefile", 'install-micro', ''); + } } diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 239fcf43b..c21ae5909 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -74,6 +74,34 @@ public static function getPHPVersionID(?string $from_custom_source = null, bool throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); } + /** + * Get PHP version from php_version.h + * + * @param null|string $from_custom_source Where to read php_version.h from custom source + * @param bool $return_null_if_failed Whether to return null if failed to get version + * @return null|string PHP version (e.g., "8.4.0") or null if failed + */ + public static function getPHPVersion(?string $from_custom_source = null, bool $return_null_if_failed = false): ?string + { + $source_dir = $from_custom_source ?? ArtifactLoader::getArtifactInstance('php-src')->getSourceDir(); + if (!file_exists("{$source_dir}/main/php_version.h")) { + if ($return_null_if_failed) { + return null; + } + throw new WrongUsageException('PHP source files are not available, you need to download them first'); + } + + $file = file_get_contents("{$source_dir}/main/php_version.h"); + if (preg_match('/PHP_VERSION "(.*)"/', $file, $match) !== 0) { + return $match[1]; + } + + if ($return_null_if_failed) { + return null; + } + throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); + } + #[InitPackage] public function init(TargetPackage $package): void { @@ -222,6 +250,8 @@ public function info(Package $package, PackageInstaller $installer): array 'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)), 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), 'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)), + 'Strip Binaries' => $package->getBuildOption('no-strip') ? 'No' : 'Yes', + 'Enable ZTS' => $package->getBuildOption('enable-zts') ? 'Yes' : 'No', ]; } @@ -236,7 +266,7 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void logger()->info("Adding hardcoded INI [{$source_name} = {$ini_value}]"); } if (!empty($custom_ini)) { - SourcePatcher::patchHardcodedINI($package->getSourceDir(), $custom_ini); + ApplicationContext::invoke([SourcePatcher::class, 'patchHardcodedINI'], [$package->getSourceDir(), $custom_ini]); } // Patch StaticPHP version diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php new file mode 100644 index 000000000..066870902 --- /dev/null +++ b/src/Package/Target/php/frankenphp.php @@ -0,0 +1,155 @@ +runStage([$this, 'processFrankenphpApp']); + + // modules + $no_brotli = $installer->isPackageResolved('brotli') ? '' : ',nobrotli'; + $no_watcher = $installer->isPackageResolved('watcher') ? '' : ',nowatcher'; + $xcaddy_modules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); // from env.ini + $source_dir = $package->getSourceDir(); + + $xcaddy_modules = preg_replace('#--with github.com/dunglas/frankenphp\S*#', '', $xcaddy_modules); + $xcaddy_modules = "--with github.com/dunglas/frankenphp={$source_dir} " . + "--with github.com/dunglas/frankenphp/caddy={$source_dir}/caddy {$xcaddy_modules}"; + + // disable caddy-cbrotli if brotli is not built + if (!$installer->isPackageResolved('brotli') && str_contains($xcaddy_modules, '--with github.com/dunglas/caddy-cbrotli')) { + logger()->warning('caddy-cbrotli module is enabled, but brotli library is not built. Disabling caddy-cbrotli.'); + $xcaddy_modules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddy_modules); + } + + $frankenphp_version = $this->getFrankenPHPVersion($package); + $libphp_version = php::getPHPVersion(); + $dynamic_exports = ''; + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $libphp_version = preg_replace('/\.\d+$/', '', $libphp_version); + } elseif ($dynamicSymbolsArgument = LinuxUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { + $dynamic_exports = ' ' . $dynamicSymbolsArgument; + } + + // full-static build flags + if ($toolchain->isStatic()) { + $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$package->getLibExtraLdFlags()}'"; + $muslTags = 'static_build,'; + $staticFlags = '-static-pie'; + } else { + $extLdFlags = "-extldflags '-pie{$dynamic_exports} {$package->getLibExtraLdFlags()}'"; + $muslTags = ''; + $staticFlags = ''; + } + + $resolved = array_keys($installer->getResolvedPackages()); + // remove self from deps + $resolved = array_filter($resolved, fn ($pkg_name) => $pkg_name !== $package->getName()); + $config = new SPCConfigUtil()->config($resolved); + $cflags = "{$package->getLibExtraCFlags()} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . " -DFRANKENPHP_VERSION={$frankenphp_version}"; + $libs = $config['libs']; + + // Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix. + if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') || + str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) && + $toolchain instanceof GccNativeToolchain) { + $cflags .= ' -Wno-error=missing-profile'; + $libs .= ' -lgcov'; + } + + $env = [ + 'CGO_ENABLED' => '1', + 'CGO_CFLAGS' => clean_spaces($cflags), + 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}", + 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . + '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . + '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . + "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . + "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with xcaddy')); + shell()->cd(BUILD_LIB_PATH) + ->setEnv($env) + ->exec("xcaddy build --output frankenphp {$xcaddy_modules}"); + + $builder->deployBinary(BUILD_LIB_PATH . '/frankenphp', BUILD_BIN_PATH . '/frankenphp'); + $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); + } + + /** + * Process the --with-frankenphp-app option + * Creates app.tar and app.checksum in source/frankenphp directory + */ + #[Stage] + public function processFrankenphpApp(TargetPackage $package): void + { + $frankenphpSourceDir = $package->getSourceDir(); + + $frankenphpAppPath = $package->getBuildOption('with-frankenphp-app'); + + if ($frankenphpAppPath) { + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); + if (!is_dir($frankenphpAppPath)) { + throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); + } + $appTarPath = "{$frankenphpSourceDir}/app.tar"; + logger()->info("Creating app.tar from {$frankenphpAppPath}"); + + shell()->exec('tar -cf ' . escapeshellarg($appTarPath) . ' -C ' . escapeshellarg($frankenphpAppPath) . ' .'); + + $checksum = hash_file('md5', $appTarPath); + file_put_contents($frankenphpSourceDir . '/app_checksum.txt', $checksum); + } else { + FileSystem::removeFileIfExists("{$frankenphpSourceDir}/app.tar"); + FileSystem::removeFileIfExists("{$frankenphpSourceDir}/app_checksum.txt"); + file_put_contents("{$frankenphpSourceDir}/app.tar", ''); + file_put_contents("{$frankenphpSourceDir}/app_checksum.txt", ''); + } + } + + protected function getFrankenPHPVersion(TargetPackage $package): string + { + if ($version = getenv('FRANKENPHP_VERSION')) { + return $version; + } + $frankenphpSourceDir = $package->getSourceDir(); + $goModPath = $frankenphpSourceDir . '/caddy/go.mod'; + + if (!file_exists($goModPath)) { + throw new SPCInternalException("FrankenPHP caddy/go.mod file not found at {$goModPath}, why did we not download FrankenPHP?"); + } + + $content = file_get_contents($goModPath); + if (preg_match('/github\.com\/dunglas\/frankenphp\s+v?(\d+\.\d+\.\d+)/', $content, $matches)) { + return $matches[1]; + } + + throw new SPCInternalException('Could not find FrankenPHP version in caddy/go.mod'); + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 18946ad4f..13c897807 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -4,11 +4,13 @@ namespace Package\Target\php; +use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\PatchDescription; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\PatchException; use StaticPHP\Exception\SPCException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; @@ -20,7 +22,6 @@ 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; @@ -28,6 +29,8 @@ trait unix { + use frankenphp; + #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] @@ -46,11 +49,47 @@ public function patchBeforeBuildconf(TargetPackage $package): void FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } + #[BeforeStage('php', [php::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch TSRM for musl TLS symbol visibility issue')] + #[PatchDescription('Patch ext/standard/info.c for configure command info')] + public function patchTSRMBeforeUnixMake(ToolchainInterface $toolchain): void + { + if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { + // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } else { + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } + + if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { + FileSystem::replaceFileLineContainsString( + SOURCE_PATH . '/php-src/ext/standard/info.c', + '#ifdef CONFIGURE_COMMAND', + '#ifdef NO_CONFIGURE_COMMAND', + ); + } else { + FileSystem::replaceFileLineContainsString( + SOURCE_PATH . '/php-src/ext/standard/info.c', + '#ifdef NO_CONFIGURE_COMMAND', + '#ifdef CONFIGURE_COMMAND', + ); + } + } + #[Stage] public function buildconfForUnix(TargetPackage $package): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); V2CompatLayer::emitPatchPoint('before-php-buildconf'); + // run ./buildconf shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } @@ -112,18 +151,23 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); + // cli if ($installer->isPackageResolved('php-cli')) { $package->runStage([self::class, 'makeCliForUnix']); } + // cgi if ($installer->isPackageResolved('php-cgi')) { $package->runStage([self::class, 'makeCgiForUnix']); } + // fpm if ($installer->isPackageResolved('php-fpm')) { $package->runStage([self::class, 'makeFpmForUnix']); } + // micro if ($installer->isPackageResolved('php-micro')) { $package->runStage([self::class, 'makeMicroForUnix']); } + // embed if ($installer->isPackageResolved('php-embed')) { $package->runStage([self::class, 'makeEmbedForUnix']); } @@ -175,32 +219,44 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install } #[Stage] - #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] + #[PatchDescription('Patch micro.sfx after UPX compression')] 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' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} 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(); + 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' : ''; + $makeArgs = $this->makeVarsToArgs($vars); + // build + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); + + $dst = BUILD_BIN_PATH . '/micro.sfx'; + $builder->deployBinary("{$package->getSourceDir()}/sapi/micro/micro.sfx", $dst); + + /* + * Patch micro.sfx after UPX compression. + * micro needs special section handling in LinuxBuilder. + * The micro.sfx does not support UPX directly, but we can remove UPX + * info segment to adapt. + * This will also make micro.sfx with upx-packed more like a malware fore antivirus + */ + if ($package->getBuildOption('with-upx-pack') && SystemTarget::getTargetOS() === 'Linux') { + // strip first + // cut binary with readelf + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); } + + $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); } #[Stage] @@ -229,13 +285,18 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta // process libphp.so for shared embed $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; + $libphp_so_dst = $libphp_so; if (file_exists($libphp_so)) { // rename libphp.so if -release is set if (SystemTarget::getTargetOS() === 'Linux') { - $this->processLibphpSoFile($libphp_so, $installer); + // deploy libphp.so + preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); + if (!empty($matches[1])) { + $libphp_so_dst = str_replace('.so', '-' . $matches[1] . '.so', $libphp_so); + } } // deploy - $builder->deployBinary($libphp_so, $libphp_so, false); + $builder->deployBinary($libphp_so, $libphp_so_dst, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -312,7 +373,11 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa public function build(TargetPackage $package): void { // virtual target, do nothing - if ($package->getName() !== 'php') { + if (in_array($package->getName(), ['php-cli', 'php-fpm', 'php-cgi', 'php-micro', 'php-embed'], true)) { + return; + } + if ($package->getName() === 'frankenphp') { + $package->runStage([$this, 'buildFrankenphpUnix']); return; } diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index c9b1051f9..d782b3e20 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -113,11 +113,10 @@ public function deployBinary(string $src, string $dst, bool $executable = true): throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); } - // extract debug info - $this->extractDebugInfo($dst); - - // strip if (!$this->getOption('no-strip') && SystemTarget::isUnix()) { + // extract debug info + $this->extractDebugInfo($dst); + // strip binary $this->stripBinary($dst); } @@ -145,13 +144,12 @@ 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}") @@ -176,6 +174,7 @@ public function stripBinary(string $binary_path): void shell()->exec(match (SystemTarget::getTargetOS()) { 'Darwin' => "strip -S {$binary_path}", 'Linux' => "strip --strip-unneeded {$binary_path}", + 'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'), }); } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 169630f99..d156eb615 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -614,8 +614,13 @@ private function handlePhpTargetPackage(TargetPackage $package): void } } else { // process specific php sapi targets - $this->build_packages['php'] = PackageLoader::getPackage('php'); - $this->install_packages[$package->getName()] = $package; + if ($package->getName() === 'frankenphp') { + $this->build_packages['php'] = PackageLoader::getPackage('php'); + $this->build_packages['frankenphp'] = PackageLoader::getPackage('frankenphp'); + } else { + $this->install_packages[$package->getName()] = $package; + $this->build_packages['php'] = PackageLoader::getPackage('php'); + } } } From 508f635f014a9570199d68adec1bedc58ea2ec72 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Feb 2026 16:54:23 +0800 Subject: [PATCH 260/682] Add permission copying to ArtifactExtractor --- src/StaticPHP/Artifact/ArtifactExtractor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 217a6f54f..4d38a84bd 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -428,6 +428,9 @@ protected function copyFileOrDir(string $src, string $dst): void FileSystem::copyDir($src, $dst); } else { copy($src, $dst); + // copy permissions + $perms = fileperms($src) & 0x1FF; // Get rwxrwxrwx bits + chmod($dst, $perms); } logger()->debug("Copied {$src} -> {$dst}"); From 7a3f10bd77f0fa61b8aa61e30994c878d3802305 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Feb 2026 16:54:46 +0800 Subject: [PATCH 261/682] Make downloader configurable --- config/downloader.php | 30 +++++++++++++++++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 26 ++++------------ 2 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 config/downloader.php diff --git a/config/downloader.php b/config/downloader.php new file mode 100644 index 000000000..48710a888 --- /dev/null +++ b/config/downloader.php @@ -0,0 +1,30 @@ + */ +return [ + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar' => GitHubTarball::class, + 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, +]; diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b0cbfeb83..a5eb9b148 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,16 +6,9 @@ use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; -use StaticPHP\Artifact\Downloader\Type\BitBucketTag; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; -use StaticPHP\Artifact\Downloader\Type\FileList; use StaticPHP\Artifact\Downloader\Type\Git; -use StaticPHP\Artifact\Downloader\Type\GitHubRelease; -use StaticPHP\Artifact\Downloader\Type\GitHubTarball; -use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; -use StaticPHP\Artifact\Downloader\Type\PhpRelease; -use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; use StaticPHP\Artifact\Downloader\Type\ValidatorInterface; use StaticPHP\DI\ApplicationContext; @@ -38,19 +31,7 @@ class ArtifactDownloader { /** @var array> */ - public const array DOWNLOADERS = [ - 'bitbuckettag' => BitBucketTag::class, - 'filelist' => FileList::class, - 'git' => Git::class, - 'ghrel' => GitHubRelease::class, - 'ghtar' => GitHubTarball::class, - 'ghtagtar' => GitHubTarball::class, - 'local' => LocalDir::class, - 'pie' => PIE::class, - 'url' => Url::class, - 'php-release' => PhpRelease::class, - 'hosted' => HostedPackageBin::class, - ]; + protected array $downloaders = []; /** @var array Artifact objects */ protected array $artifacts = []; @@ -214,6 +195,9 @@ public function __construct(protected array $options = []) // read downloads dir $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + + // load downloaders + $this->downloaders = require ROOT_DIR . '/config/downloader.php'; } /** @@ -371,7 +355,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, foreach ($queue as $item) { try { $instance = null; - $call = self::DOWNLOADERS[$item['config']['type']] ?? null; + $call = $this->downloaders[$item['config']['type']] ?? null; $type_display_name = match (true) { $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader', $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader', From 0be4e859f381d25a8a45d322bbb035d10154d00d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Feb 2026 17:24:49 +0800 Subject: [PATCH 262/682] Avoid using glob in phar mode --- src/StaticPHP/Config/ArtifactConfig.php | 3 ++- src/StaticPHP/Config/PackageConfig.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index 83f079202..e3e59fb55 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -6,6 +6,7 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\Registry; +use StaticPHP\Util\FileSystem; use Symfony\Component\Yaml\Yaml; class ArtifactConfig @@ -18,7 +19,7 @@ public static function loadFromDir(string $dir, string $registry_name): array throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config."); } $loaded = []; - $files = glob("{$dir}/*"); + $files = FileSystem::scanDirFiles($dir, false); if (is_array($files)) { foreach ($files as $file) { self::loadFromFile($file, $registry_name); diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index bd9786c3d..c4f22a528 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -7,6 +7,7 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\Registry; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; use Symfony\Component\Yaml\Yaml; class PackageConfig @@ -23,7 +24,7 @@ public static function loadFromDir(string $dir, string $registry_name): array throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); } $loaded = []; - $files = glob("{$dir}/*"); + $files = FileSystem::scanDirFiles($dir, false); if (is_array($files)) { foreach ($files as $file) { self::loadFromFile($file, $registry_name); From 753fdd725eb71d552923f142d16a2f6c8749be78 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:56:00 +0800 Subject: [PATCH 263/682] Add registry availability check --- src/StaticPHP/Doctor/Item/RegistryCheck.php | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/StaticPHP/Doctor/Item/RegistryCheck.php diff --git a/src/StaticPHP/Doctor/Item/RegistryCheck.php b/src/StaticPHP/Doctor/Item/RegistryCheck.php new file mode 100644 index 000000000..d6cd99295 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/RegistryCheck.php @@ -0,0 +1,23 @@ + 0) { + return CheckResult::ok(implode(',', array_map(fn ($x) => ConsoleColor::green($x), $regs))); + } + return CheckResult::fail('No registry configured'); + } +} From 1095807e5bf9d1637226e6b877c9dd56e93baee1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:56:18 +0800 Subject: [PATCH 264/682] Use yml instead --- spc.registry.json | 35 ----------------------------------- spc.registry.yml | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 spc.registry.json create mode 100644 spc.registry.yml diff --git a/spc.registry.json b/spc.registry.json deleted file mode 100644 index b55f63a68..000000000 --- a/spc.registry.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "core", - "autoload": "vendor/autoload.php", - "scripts": [ - "ext/" - ], - "doctor": { - "psr-4": { - "StaticPHP\\Doctor\\Item": "src/StaticPHP/Doctor/Item" - } - }, - "package": { - "psr-4": { - "Package": "src/Package" - }, - "config": [ - "config/pkg/lib/", - "config/pkg/target/", - "config/pkg/ext/" - ] - }, - "artifact": { - "config": [ - "config/artifact/" - ], - "psr-4": { - "Package\\Artifact": "src/Package/Artifact" - } - }, - "command": { - "psr-4": { - "Package\\Command": "src/Package/Command" - } - } -} diff --git a/spc.registry.yml b/spc.registry.yml new file mode 100644 index 000000000..98e4bb42c --- /dev/null +++ b/spc.registry.yml @@ -0,0 +1,20 @@ +name: core +autoload: vendor/autoload.php +doctor: + psr-4: + StaticPHP\Doctor\Item: src/StaticPHP/Doctor/Item +package: + psr-4: + Package: src/Package + config: + - config/pkg/lib/ + - config/pkg/target/ + - config/pkg/ext/ +artifact: + config: + - config/artifact/ + psr-4: + Package\Artifact: src/Package/Artifact +command: + psr-4: + Package\Command: src/Package/Command From f1e9dd8de89501aca57d0bd9b696e76ebbe1b045 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:56:44 +0800 Subject: [PATCH 265/682] Fix box compile config for v3 --- box.json | 38 +++++++++++++++++++++----------------- composer.json | 1 - 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/box.json b/box.json index f6eecf1ca..13b785447 100644 --- a/box.json +++ b/box.json @@ -1,19 +1,23 @@ { - "alias": "spc-php.phar", - "banner": false, - "blacklist": [ - ".github" - ], - "compression": "GZ", - "directories": [ - "config", - "src", - "vendor/psr", - "vendor/laravel/prompts", - "vendor/illuminate", - "vendor/symfony", - "vendor/zhamao" - ], - "git-commit-short": "git_commit_short", - "output": "spc.phar" + "alias": "spc-php.phar", + "banner": false, + "blacklist": [ + ".github" + ], + "compression": "GZ", + "check-requirements": false, + "directories": [ + "config", + "src", + "vendor/psr", + "vendor/laravel/prompts", + "vendor/symfony", + "vendor/php-di", + "vendor/zhamao" + ], + "files": [ + "spc.registry.yml" + ], + "git-commit-short": "git_commit_short", + "output": "spc.phar" } diff --git a/composer.json b/composer.json index c5470b54b..0d4fde4e8 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ "ext-mbstring": "*", "ext-zlib": "*", "laravel/prompts": "~0.1", - "nette/php-generator": "^4.2", "php-di/php-di": "^7.1", "symfony/console": "^5.4 || ^6 || ^7", "symfony/process": "^7.2", From 2fdb0b406fbe40596c159742b259d9b47e5f979c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:56:56 +0800 Subject: [PATCH 266/682] Correct bootstrap --- src/bootstrap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bootstrap.php b/src/bootstrap.php index 15e30b593..95384b719 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -59,6 +59,6 @@ } // load core registry -Registry::loadRegistry(ROOT_DIR . '/spc.registry.json'); +Registry::loadRegistry(ROOT_DIR . '/spc.registry.yml'); // load registries from environment variable SPC_REGISTRIES Registry::loadFromEnvOrOption(); From 059f785e0d5d2ab565bd6af5c8336ad3b0473aa2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:57:21 +0800 Subject: [PATCH 267/682] Remove redundant catch for downloader --- src/StaticPHP/Artifact/ArtifactDownloader.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index a5eb9b148..fd3caeaf1 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -14,7 +14,6 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\DownloaderException; use StaticPHP\Exception\ExecutionException; -use StaticPHP\Exception\SPCException; use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\ArtifactLoader; @@ -316,9 +315,6 @@ public function download(bool $interactive = true): void InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true); } } - } catch (SPCException $e) { - array_map(fn ($x) => InteractiveTerm::error($x), explode("\n", $e->getMessage())); - throw new WrongUsageException(); } finally { if ($interactive) { Shell::passthruCallback(null); From ee5aabbe34b822130f8146ea29a6419ff0ca0a66 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:57:55 +0800 Subject: [PATCH 268/682] Add CMakeConfigureLog.yaml lookup --- .../Runtime/Executor/UnixCMakeExecutor.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 3d628b894..9442d30c2 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -311,6 +311,17 @@ private function seekLogFileOnException(mixed $callable): static $callable(); return $this; } catch (SPCException $e) { + // CMake configure log (CMake 3.26+) + $cmake_configure_log = "{$this->build_dir}/CMakeFiles/CMakeConfigureLog.yaml"; + if (file_exists($cmake_configure_log)) { + logger()->debug("CMake configure log file found: {$cmake_configure_log}"); + $log_file = "lib.{$this->package->getName()}.cmake-configure.log"; + logger()->debug('Saved CMake configure log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library CMakeConfigureLog.yaml", $log_file); + copy($cmake_configure_log, SPC_LOGS_DIR . "/{$log_file}"); + } + + // CMake error log $cmake_log = "{$this->build_dir}/CMakeFiles/CMakeError.log"; if (file_exists($cmake_log)) { logger()->debug("CMake error log file found: {$cmake_log}"); @@ -319,6 +330,8 @@ private function seekLogFileOnException(mixed $callable): static $e->addExtraLogFile("{$this->package->getName()} library CMakeError.log", $log_file); copy($cmake_log, SPC_LOGS_DIR . "/{$log_file}"); } + + // CMake output log $cmake_output = "{$this->build_dir}/CMakeFiles/CMakeOutput.log"; if (file_exists($cmake_output)) { logger()->debug("CMake output log file found: {$cmake_output}"); From bbab685247c9592fb5786650fcb218e8641a16d7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:58:42 +0800 Subject: [PATCH 269/682] Refactor exception handler to v3, use standard shell exitcode --- bin/spc | 6 +- src/StaticPHP/Command/BaseCommand.php | 11 +- src/StaticPHP/Command/Dev/EnvCommand.php | 2 +- .../Command/Dev/IsInstalledCommand.php | 2 +- .../Command/Dev/LintConfigCommand.php | 4 +- src/StaticPHP/Command/Dev/ShellCommand.php | 2 +- src/StaticPHP/Command/DoctorCommand.php | 2 +- src/StaticPHP/Command/DumpLicenseCommand.php | 6 +- src/StaticPHP/Command/ExtractCommand.php | 2 +- src/StaticPHP/Command/ReturnCode.php | 42 +++ src/StaticPHP/Command/SPCConfigCommand.php | 2 +- src/StaticPHP/Exception/ExceptionHandler.php | 260 +++++++++++------- .../Exception/ExecutionException.php | 4 +- src/StaticPHP/Exception/SPCException.php | 188 +++++++++---- .../Exception/ValidationException.php | 8 - src/StaticPHP/Package/Package.php | 31 ++- src/StaticPHP/Package/PackageBuilder.php | 35 ++- 17 files changed, 398 insertions(+), 209 deletions(-) create mode 100644 src/StaticPHP/Command/ReturnCode.php diff --git a/bin/spc b/bin/spc index 9bac188ae..622dda863 100755 --- a/bin/spc +++ b/bin/spc @@ -21,10 +21,8 @@ if (PHP_VERSION_ID < 80400) { try { (new ConsoleApplication())->run(); } catch (SPCException $e) { - ExceptionHandler::handleSPCException($e); - exit(1); + exit(ExceptionHandler::handleSPCException($e)); } catch (\Throwable $e) { - ExceptionHandler::handleDefaultException($e); - exit(1); + exit(ExceptionHandler::handleDefaultException($e)); } diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index 02f84ffb5..ddcb3671d 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -14,6 +14,8 @@ abstract class BaseCommand extends Command { + use ReturnCode; + /** * The message of the day (MOTD) displayed when the command is run. * You can customize this to show your application's name and version if you are using SPC in vendor mode. @@ -101,12 +103,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->handle(); } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { // Handle SPCException and log it - ExceptionHandler::handleSPCException($e); - return static::FAILURE; + return ExceptionHandler::handleSPCException($e); } catch (\Throwable $e) { // Handle any other exceptions - ExceptionHandler::handleDefaultException($e); - return static::FAILURE; + return ExceptionHandler::handleDefaultException($e); } } @@ -129,7 +129,8 @@ private function getVersionWithCommit(): string // Don't show commit ID when running in phar if (\Phar::running()) { - return $version; + $stable = file_exists(ROOT_DIR . '/src/.release') ? 'stable' : 'unstable'; + return "{$version} ({$stable})"; } $commitId = $this->getGitCommitShortId(); diff --git a/src/StaticPHP/Command/Dev/EnvCommand.php b/src/StaticPHP/Command/Dev/EnvCommand.php index 160504ed9..96d9409b1 100644 --- a/src/StaticPHP/Command/Dev/EnvCommand.php +++ b/src/StaticPHP/Command/Dev/EnvCommand.php @@ -29,7 +29,7 @@ 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; + return static::USER_ERROR; } if (is_array($val)) { foreach ($val as $k => $v) { diff --git a/src/StaticPHP/Command/Dev/IsInstalledCommand.php b/src/StaticPHP/Command/Dev/IsInstalledCommand.php index a3f693217..c5dfc498d 100644 --- a/src/StaticPHP/Command/Dev/IsInstalledCommand.php +++ b/src/StaticPHP/Command/Dev/IsInstalledCommand.php @@ -29,6 +29,6 @@ public function handle(): int return static::SUCCESS; } $this->output->writeln("Package [{$package}] is not installed."); - return static::FAILURE; + return static::USER_ERROR; } } diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index d0e4cfa1a..1efba4d5a 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -34,7 +34,7 @@ public function handle(): int if ($checkOnly && $hasChanges) { $this->output->writeln('Some config files need sorting. Run "bin/spc dev:lint-config" to fix them.'); - return static::FAILURE; + return static::VALIDATION_ERROR; } return static::SUCCESS; @@ -125,7 +125,7 @@ private function sortConfigFile(mixed $file, string $config_type, bool $checkOnl return false; } ksort($data); - foreach ($data as $artifact_name => &$config) { + foreach ($data as &$config) { uksort($config, $config_type === 'artifact' ? [$this, 'artifactSortKey'] : [$this, 'packageSortKey']); } unset($config); diff --git a/src/StaticPHP/Command/Dev/ShellCommand.php b/src/StaticPHP/Command/Dev/ShellCommand.php index 560cc7fed..103300b58 100644 --- a/src/StaticPHP/Command/Dev/ShellCommand.php +++ b/src/StaticPHP/Command/Dev/ShellCommand.php @@ -28,6 +28,6 @@ public function handle(): int return $code; } $this->output->writeln('Unsupported OS for shell command.'); - return static::FAILURE; + return static::ENVIRONMENT_ERROR; } } diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index cd90cd94c..6ae6d68a1 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -30,6 +30,6 @@ public function handle(): int return static::SUCCESS; } - return static::FAILURE; + return static::ENVIRONMENT_ERROR; } } diff --git a/src/StaticPHP/Command/DumpLicenseCommand.php b/src/StaticPHP/Command/DumpLicenseCommand.php index d90ecbf95..2ba4d002d 100644 --- a/src/StaticPHP/Command/DumpLicenseCommand.php +++ b/src/StaticPHP/Command/DumpLicenseCommand.php @@ -71,7 +71,7 @@ public function handle(): int $this->output->writeln(' - --for-extensions: dump-license --for-extensions=openssl,mbstring'); $this->output->writeln(' - --for-libs: dump-license --for-libs=openssl,zlib'); $this->output->writeln(' - --for-packages: dump-license --for-packages=php,libssl'); - return self::FAILURE; + return static::USER_ERROR; } // Deduplicate artifacts @@ -90,11 +90,11 @@ public function handle(): int InteractiveTerm::success('Licenses dumped successfully: ' . $dump_dir); // $this->output->writeln("✓ Successfully dumped licenses to: {$dump_dir}"); // $this->output->writeln(" Total artifacts: " . count($artifacts_to_dump) . ''); - return self::SUCCESS; + return static::SUCCESS; } $this->output->writeln('Failed to dump licenses'); - return self::FAILURE; + return static::INTERNAL_ERROR; } /** diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index 14951a341..e2e79bdf0 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -47,7 +47,7 @@ public function handle(): int $artifact = ArtifactLoader::getArtifactInstance($name); if ($artifact === null) { $this->output->writeln("Artifact '{$name}' not found."); - return static::FAILURE; + return static::USER_ERROR; } $artifacts[$name] = $artifact; } diff --git a/src/StaticPHP/Command/ReturnCode.php b/src/StaticPHP/Command/ReturnCode.php new file mode 100644 index 000000000..d152101ef --- /dev/null +++ b/src/StaticPHP/Command/ReturnCode.php @@ -0,0 +1,42 @@ + "{$config['cflags']} {$config['ldflags']} {$config['libs']}", }); - return 0; + return static::SUCCESS; } } diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 53dc15a85..20cf9395e 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -4,15 +4,15 @@ namespace StaticPHP\Exception; -use SPC\builder\BuilderBase; -use SPC\builder\freebsd\BSDBuilder; -use SPC\builder\linux\LinuxBuilder; -use SPC\builder\macos\MacOSBuilder; -use SPC\builder\windows\WindowsBuilder; +use StaticPHP\Command\BaseCommand; use StaticPHP\DI\ApplicationContext; use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; +/** + * Exception handler for StaticPHP. + * Provides centralized exception handling for the Package-based architecture. + */ class ExceptionHandler { public const array KNOWN_EXCEPTIONS = [ @@ -35,28 +35,25 @@ class ExceptionHandler RegistryException::class, ]; - /** @var null|BuilderBase Builder binding */ - private static ?BuilderBase $builder = null; - /** @var array Build PHP extra info binding */ private static array $build_php_extra_info = []; - public static function handleSPCException(SPCException $e): void + public static function handleSPCException(SPCException $e): int { // XXX error: yyy $head_msg = match ($class = get_class($e)) { - BuildFailureException::class => "✗ Build failed: {$e->getMessage()}", - DownloaderException::class => "✗ Download failed: {$e->getMessage()}", + BuildFailureException::class => "✘ Build failed: {$e->getMessage()}", + DownloaderException::class => "✘ Download failed: {$e->getMessage()}", EnvironmentException::class => "⚠ Environment check failed: {$e->getMessage()}", - ExecutionException::class => "✗ Command execution failed: {$e->getMessage()}", - FileSystemException::class => "✗ File system error: {$e->getMessage()}", + ExecutionException::class => "✘ Command execution failed: {$e->getMessage()}", + FileSystemException::class => "✘ File system error: {$e->getMessage()}", InterruptException::class => "⚠ Build interrupted by user: {$e->getMessage()}", - PatchException::class => "✗ Patch apply failed: {$e->getMessage()}", - SPCInternalException::class => "✗ SPC internal error: {$e->getMessage()}", + PatchException::class => "✘ Patch apply failed: {$e->getMessage()}", + SPCInternalException::class => "✘ SPC internal error: {$e->getMessage()}", ValidationException::class => "⚠ Validation failed: {$e->getMessage()}", WrongUsageException::class => $e->getMessage(), - RegistryException::class => "✗ Registry parsing error: {$e->getMessage()}", - default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}", + RegistryException::class => "✘ Registry error: {$e->getMessage()}", + default => "✘ Unknown SPC exception {$class}: {$e->getMessage()}", }; self::logError($head_msg); @@ -64,83 +61,10 @@ public static function handleSPCException(SPCException $e): void $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); if ($minor_logs) { - return; - } - - self::logError("----------------------------------------\n"); - - // get the SPCException module - if ($lib_info = $e->getLibraryInfo()) { - self::logError('Failed module: ' . ConsoleColor::yellow("library {$lib_info['library_name']} builder for {$lib_info['os']}")); - } elseif ($ext_info = $e->getExtensionInfo()) { - self::logError('Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder")); - } elseif (self::$builder) { - $os = match (get_class(self::$builder)) { - WindowsBuilder::class => 'Windows', - MacOSBuilder::class => 'macOS', - LinuxBuilder::class => 'Linux', - BSDBuilder::class => 'FreeBSD', - default => 'Unknown OS', - }; - self::logError('Failed module: ' . ConsoleColor::yellow("Builder for {$os}")); - } elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) { - self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); - } - - // get command execution info - if ($e instanceof ExecutionException) { - self::logError(''); - self::logError('Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand())); - if ($cd = $e->getCd()) { - self::logError('Command executed in: ' . ConsoleColor::yellow($cd)); - } - if ($env = $e->getEnv()) { - self::logError('Command inline env variables:'); - foreach ($env as $k => $v) { - self::logError(ConsoleColor::yellow("{$k}={$v}"), 4); - } - } - } - - // validation error - if ($e instanceof ValidationException) { - self::logError('Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString())); + return self::getReturnCode($e); } - // environment error - if ($e instanceof EnvironmentException) { - self::logError('Failed environment check: ' . ConsoleColor::yellow($e->getMessage())); - if (($solution = $e->getSolution()) !== null) { - self::logError('Solution: ' . ConsoleColor::yellow($solution)); - } - } - - // get patch info - if ($e instanceof PatchException) { - self::logError("Failed patch module: {$e->getPatchModule()}"); - } - - // get internal trace - if ($e instanceof SPCInternalException) { - self::logError('Internal trace:'); - self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4); - } - - // get the full build info if possible - if ($info = ExceptionHandler::$build_php_extra_info) { - self::logError('', output_log: ApplicationContext::isDebug()); - self::logError('Build PHP extra info:', output_log: ApplicationContext::isDebug()); - self::printArrayInfo($info); - } - - // get the full builder options if possible - if ($e->getBuildPHPInfo()) { - $info = $e->getBuildPHPInfo(); - self::logError('', output_log: ApplicationContext::isDebug()); - self::logError('Builder function: ' . ConsoleColor::yellow($info['builder_function']), output_log: ApplicationContext::isDebug()); - } - - self::logError("\n----------------------------------------\n"); + self::printModuleErrorInfo($e); // convert log file path if in docker $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); @@ -153,36 +77,48 @@ public static function handleSPCException(SPCException $e): void } if ($e->getExtraLogFiles() !== []) { foreach ($e->getExtraLogFiles() as $key => $file) { - self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::cyan("{$spc_logs_dir_convert}/{$file}")); + self::logError('⚠ Log file ' . ConsoleColor::cyan($key) . ' is saved in: ' . ConsoleColor::cyan("{$spc_logs_dir_convert}/{$file}")); } } if (!ApplicationContext::isDebug()) { - self::logError('⚠ If you want to see more details in console, use `--debug` option.'); + self::logError('⚠ If you want to see more details in console, use `-vvv` option.'); } + return self::getReturnCode($e); } - public static function handleDefaultException(\Throwable $e): void + public static function handleDefaultException(\Throwable $e): int { $class = get_class($e); $file = $e->getFile(); $line = $e->getLine(); - self::logError("✗ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); + self::logError("✘ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); self::logError('Stack trace:'); self::logError(ConsoleColor::gray($e->getTraceAsString()) . PHP_EOL, 4); self::logError('⚠ Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues'); + return self::getReturnCode($e); } - public static function bindBuilder(?BuilderBase $bind_builder): void + public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void { - self::$builder = $bind_builder; + self::$build_php_extra_info = $build_php_extra_info; } - public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void + private static function getReturnCode(\Throwable $e): int { - self::$build_php_extra_info = $build_php_extra_info; + return match (get_class($e)) { + BuildFailureException::class, ExecutionException::class => BaseCommand::BUILD_ERROR, + DownloaderException::class => BaseCommand::DOWNLOAD_ERROR, + EnvironmentException::class => BaseCommand::ENVIRONMENT_ERROR, + FileSystemException::class => BaseCommand::FILE_SYSTEM_ERROR, + InterruptException::class => BaseCommand::INTERRUPT_SIGNAL, + PatchException::class => BaseCommand::PATCH_ERROR, + ValidationException::class => BaseCommand::VALIDATION_ERROR, + WrongUsageException::class => BaseCommand::USER_ERROR, + default => BaseCommand::INTERNAL_ERROR, + }; } - private static function logError($message, int $indent_space = 0, bool $output_log = true): void + private static function logError($message, int $indent_space = 0, bool $output_log = true, string $color = 'red'): void { $spc_log = fopen(SPC_OUTPUT_LOG, 'a'); $msg = explode("\n", (string) $message); @@ -190,7 +126,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) . '', 'error'); + InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error'); } } } @@ -229,4 +165,124 @@ private static function printArrayInfo(array $info): void } } } + + private static function printModuleErrorInfo(SPCException $e): void + { + $class = get_class($e); + self::logError("\n-------------------- " . ConsoleColor::red('Module error info') . ' --------------------', color: 'default'); + + $has_info = false; + + // Get Package information + if ($package_info = $e->getPackageInfo()) { + $type_label = match ($package_info['package_type']) { + 'library' => 'Library Package', + 'php-extension' => 'PHP Extension Package', + 'target' => 'Target Package', + default => 'Package', + }; + self::logError('Failed module: ' . ConsoleColor::gray("{$type_label} '{$package_info['package_name']}'")); + if ($package_info['file'] && $package_info['line']) { + self::logError('Package location: ' . ConsoleColor::gray("{$package_info['file']}:{$package_info['line']}")); + } + $has_info = true; + } + + // Get Stage information (can be displayed together with Package info) + $stage_stack = $e->getStageStack(); + if (!empty($stage_stack)) { + // Build stage call chain: innermost -> ... -> outermost + $stage_names = array_reverse(array_column($stage_stack, 'stage_name')); + $stage_chain = implode(' -> ', $stage_names); + + if (count($stage_names) > 1) { + self::logError('Failed stage: ' . ConsoleColor::gray($stage_chain)); + } else { + self::logError('Failed stage: ' . ConsoleColor::gray($stage_names[0])); + } + + // Show context keys of the innermost (actual failing) stage + $innermost_stage = $stage_stack[0]; + if (!empty($innermost_stage['context_keys'])) { + self::logError('Stage context keys: ' . ConsoleColor::gray(implode(', ', $innermost_stage['context_keys']))); + } + $has_info = true; + } + + // Get PackageBuilder information + if (!$has_info && ($builder_info = $e->getPackageBuilderInfo())) { + self::logError('Failed module: ' . ConsoleColor::gray('PackageBuilder')); + if ($builder_info['method']) { + self::logError('Builder method: ' . ConsoleColor::gray($builder_info['method'])); + } + if ($builder_info['file'] && $builder_info['line']) { + self::logError('Builder location: ' . ConsoleColor::gray("{$builder_info['file']}:{$builder_info['line']}")); + } + $has_info = true; + } + + // Get PackageInstaller information + if (!$has_info && ($installer_info = $e->getPackageInstallerInfo())) { + self::logError('Failed module: ' . ConsoleColor::gray('PackageInstaller')); + if ($installer_info['method']) { + self::logError('Installer method: ' . ConsoleColor::gray($installer_info['method'])); + } + if ($installer_info['file'] && $installer_info['line']) { + self::logError('Installer location: ' . ConsoleColor::gray("{$installer_info['file']}:{$installer_info['line']}")); + } + $has_info = true; + } + + if (!$has_info && !in_array($class, self::KNOWN_EXCEPTIONS)) { + self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); + } + + // get command execution info + if ($e instanceof ExecutionException) { + self::logError(''); + self::logError('Failed command: ' . ConsoleColor::gray($e->getExecutionCommand())); + if ($cd = $e->getCd()) { + self::logError(' - Command executed in: ' . ConsoleColor::gray($cd)); + } + if ($env = $e->getEnv()) { + self::logError(' - Command inline env variables:'); + foreach ($env as $k => $v) { + self::logError(ConsoleColor::gray("{$k}={$v}"), 6); + } + } + } + + // validation error + if ($e instanceof ValidationException) { + self::logError('Failed validation module: ' . ConsoleColor::gray($e->getValidationModuleString())); + } + + // environment error + if ($e instanceof EnvironmentException) { + self::logError('Failed environment check: ' . ConsoleColor::gray($e->getMessage())); + if (($solution = $e->getSolution()) !== null) { + self::logError('Solution: ' . ConsoleColor::gray($solution)); + } + } + + // get patch info + if ($e instanceof PatchException) { + self::logError("Failed patch module: {$e->getPatchModule()}"); + } + + // get internal trace + if ($e instanceof SPCInternalException) { + self::logError('Internal trace:'); + self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4); + } + + // get the full build info if possible + if ($info = ExceptionHandler::$build_php_extra_info) { + self::logError('', output_log: ApplicationContext::isDebug()); + self::logError('Build PHP extra info:', output_log: ApplicationContext::isDebug()); + self::printArrayInfo($info); + } + + self::logError("---------------------------------------------------------\n", color: 'none'); + } } diff --git a/src/StaticPHP/Exception/ExecutionException.php b/src/StaticPHP/Exception/ExecutionException.php index 3fc4c67f4..2cb62a2e4 100644 --- a/src/StaticPHP/Exception/ExecutionException.php +++ b/src/StaticPHP/Exception/ExecutionException.php @@ -4,8 +4,8 @@ namespace StaticPHP\Exception; -use SPC\util\shell\UnixShell; -use SPC\util\shell\WindowsCmd; +use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\Shell\WindowsCmd; /** * Exception thrown when an error occurs during execution of shell command. diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index 1859dd1ff..b989f7d93 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -4,12 +4,12 @@ namespace StaticPHP\Exception; -use SPC\builder\BuilderBase; -use SPC\builder\freebsd\library\BSDLibraryBase; -use SPC\builder\LibraryBase; -use SPC\builder\linux\library\LinuxLibraryBase; -use SPC\builder\macos\library\MacOSLibraryBase; -use SPC\builder\windows\library\WindowsLibraryBase; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\Package; +use StaticPHP\Package\PackageBuilder; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Package\TargetPackage; /** * Base class for SPC exceptions. @@ -20,11 +20,17 @@ */ abstract class SPCException extends \Exception { - private ?array $library_info = null; + /** @var null|array Package information */ + private ?array $package_info = null; - private ?array $extension_info = null; + /** @var null|array Package builder information */ + private ?array $package_builder_info = null; - private ?array $build_php_info = null; + /** @var null|array Package installer information */ + private ?array $package_installer_info = null; + + /** @var array Stage execution call stack */ + private array $stage_stack = []; private array $extra_log_files = []; @@ -34,9 +40,38 @@ public function __construct(string $message = '', int $code = 0, ?\Throwable $pr $this->loadStackTraceInfo(); } - public function bindExtensionInfo(array $extension_info): void + /** + * Bind package information manually. + * + * @param array $package_info Package information array + */ + public function bindPackageInfo(array $package_info): void + { + $this->package_info = $package_info; + } + + /** + * Add stage to the call stack. + * This builds a call chain like: build -> configure -> compile + * + * @param string $stage_name Stage name being executed + * @param array $context Stage context (optional) + */ + public function addStageToStack(string $stage_name, array $context = []): void { - $this->extension_info = $extension_info; + $this->stage_stack[] = [ + 'stage_name' => $stage_name, + 'context_keys' => array_keys($context), + ]; + } + + /** + * Legacy method for backward compatibility. + * @deprecated Use addStageToStack() instead + */ + public function bindStageInfo(string $stage_name, array $context = []): void + { + $this->addStageToStack($stage_name, $context); } public function addExtraLogFile(string $key, string $filename): void @@ -45,52 +80,74 @@ public function addExtraLogFile(string $key, string $filename): void } /** - * Returns an array containing information about the SPC module. - * - * This method can be overridden by subclasses to provide specific module information. + * Returns package information. * * @return null|array{ - * library_name: string, - * library_class: string, - * os: string, + * package_name: string, + * package_type: string, + * package_class: string, * file: null|string, * line: null|int, - * } an array containing module information + * } Package information or null */ - public function getLibraryInfo(): ?array + public function getPackageInfo(): ?array { - return $this->library_info; + return $this->package_info; } /** - * Returns an array containing information about the PHP build process. + * Returns package builder information. * * @return null|array{ - * builder_function: string, * file: null|string, * line: null|int, - * } an array containing PHP build information + * method: null|string, + * } Package builder information or null */ - public function getBuildPHPInfo(): ?array + public function getPackageBuilderInfo(): ?array { - return $this->build_php_info; + return $this->package_builder_info; } /** - * Returns an array containing information about the SPC extension. - * - * This method can be overridden by subclasses to provide specific extension information. + * Returns package installer information. * * @return null|array{ - * extension_name: string, - * extension_class: string, * file: null|string, * line: null|int, - * } an array containing extension information + * method: null|string, + * } Package installer information or null + */ + public function getPackageInstallerInfo(): ?array + { + return $this->package_installer_info; + } + + /** + * Returns the stage call stack. + * + * @return array, + * }> Stage call stack (empty array if no stages) */ - public function getExtensionInfo(): ?array + public function getStageStack(): array { - return $this->extension_info; + return $this->stage_stack; + } + + /** + * Returns the innermost (actual failing) stage information. + * Legacy method for backward compatibility. + * + * @return null|array{ + * stage_name: string, + * context_keys: array, + * } Stage information or null + */ + public function getStageInfo(): ?array + { + return empty($this->stage_stack) ? null : end($this->stage_stack); } public function getExtraLogFiles(): array @@ -98,6 +155,9 @@ public function getExtraLogFiles(): array return $this->extra_log_files; } + /** + * Load stack trace information to detect Package, Builder, and Installer context. + */ private function loadStackTraceInfo(): void { $trace = $this->getTrace(); @@ -106,40 +166,48 @@ private function loadStackTraceInfo(): void continue; } - // Check if the class is a subclass of LibraryBase - if (!$this->library_info && is_a($frame['class'], LibraryBase::class, true)) { + // Check if the class is a Package subclass + if (!$this->package_info && is_a($frame['class'], Package::class, true)) { try { - $reflection = new \ReflectionClass($frame['class']); - if ($reflection->hasConstant('NAME')) { - $name = $reflection->getConstant('NAME'); - if ($name !== 'unknown') { - $this->library_info = [ - 'library_name' => $name, - 'library_class' => $frame['class'], - 'os' => match (true) { - is_a($frame['class'], BSDLibraryBase::class, true) => 'BSD', - is_a($frame['class'], LinuxLibraryBase::class, true) => 'Linux', - is_a($frame['class'], MacOSLibraryBase::class, true) => 'macOS', - is_a($frame['class'], WindowsLibraryBase::class, true) => 'Windows', - default => 'Unknown', - }, - 'file' => $frame['file'] ?? null, - 'line' => $frame['line'] ?? null, - ]; - continue; - } + // Try to get package information from object if available + if (isset($frame['object']) && $frame['object'] instanceof Package) { + $package = $frame['object']; + $package_type = match (true) { + $package instanceof LibraryPackage => 'library', + $package instanceof PhpExtensionPackage => 'php-extension', + $package instanceof TargetPackage => 'target', + default => 'package', + }; + $this->package_info = [ + 'package_name' => $package->name, + 'package_type' => $package_type, + 'package_class' => $frame['class'], + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + ]; + continue; } - } catch (\ReflectionException) { - continue; + } catch (\Throwable) { + // Ignore reflection errors } } - // Check if the class is a subclass of BuilderBase and the method is buildPHP - if (!$this->build_php_info && is_a($frame['class'], BuilderBase::class, true)) { - $this->build_php_info = [ - 'builder_function' => $frame['function'], + // Check if the class is PackageBuilder + if (!$this->package_builder_info && is_a($frame['class'], PackageBuilder::class, true)) { + $this->package_builder_info = [ + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + 'method' => $frame['function'] ?? null, + ]; + continue; + } + + // Check if the class is PackageInstaller + if (!$this->package_installer_info && is_a($frame['class'], PackageInstaller::class, true)) { + $this->package_installer_info = [ 'file' => $frame['file'] ?? null, 'line' => $frame['line'] ?? null, + 'method' => $frame['function'] ?? null, ]; } } diff --git a/src/StaticPHP/Exception/ValidationException.php b/src/StaticPHP/Exception/ValidationException.php index fe0cb4287..7eae9d405 100644 --- a/src/StaticPHP/Exception/ValidationException.php +++ b/src/StaticPHP/Exception/ValidationException.php @@ -4,8 +4,6 @@ namespace StaticPHP\Exception; -use SPC\builder\Extension; - /** * Exception thrown for validation errors in SPC. * @@ -23,12 +21,6 @@ public function __construct(string $message = '', int $code = 0, ?\Throwable $pr // init validation module if ($validation_module === null) { foreach ($this->getTrace() as $trace) { - // Extension validate() => "Extension validator" - if (is_a($trace['class'] ?? null, Extension::class, true) && $trace['function'] === 'validate') { - $this->validation_module = 'Extension validator'; - break; - } - // Other => "ClassName::functionName" $this->validation_module = [ 'class' => $trace['class'] ?? null, diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index cd4f38409..64b9f2e44 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -7,6 +7,7 @@ use StaticPHP\Artifact\Artifact; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\SPCException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Registry\PackageLoader; @@ -63,13 +64,29 @@ public function runStage(mixed $name, array $context = []): mixed static::class => $this, ], $context); - // emit BeforeStage - $this->emitBeforeStage($name, $stageContext); - - $ret = ApplicationContext::invoke($this->stages[$name], $stageContext); - // emit AfterStage - $this->emitAfterStage($name, $stageContext, $ret); - return $ret; + try { + // emit BeforeStage + $this->emitBeforeStage($name, $stageContext); + + $ret = ApplicationContext::invoke($this->stages[$name], $stageContext); + // emit AfterStage + $this->emitAfterStage($name, $stageContext, $ret); + return $ret; + } catch (SPCException $e) { + // Bind package information only if not already bound + if ($e->getPackageInfo() === null) { + $e->bindPackageInfo([ + 'package_name' => $this->name, + 'package_type' => $this->type, + 'package_class' => static::class, + 'file' => null, + 'line' => null, + ]); + } + // Always add current stage to the stack to build call chain + $e->addStageToStack($name, $stageContext); + throw $e; + } } public function setOutput(string $key, string $value): static diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index d782b3e20..e50798adc 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -6,6 +6,7 @@ use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\SPCException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\Shell\Shell; @@ -59,19 +60,33 @@ public function buildPackage(Package $package, bool $force = false): int InteractiveTerm::advance(); }); - if ($package->getType() !== 'virtual-target') { - // patch before build - $package->emitPatchBeforeBuild(); - } + try { + if ($package->getType() !== 'virtual-target') { + // patch before build + $package->emitPatchBeforeBuild(); + } - // build - $package->runStage('build'); + // build + $package->runStage('build'); - if ($package->getType() !== 'virtual-target') { - // install license - if (($license = PackageConfig::get($package->getName(), 'license')) !== null) { - $this->installLicense($package, $license); + if ($package->getType() !== 'virtual-target') { + // install license + if (($license = PackageConfig::get($package->getName(), 'license')) !== null) { + $this->installLicense($package, $license); + } + } + } catch (SPCException $e) { + // Ensure package information is bound if not already + if ($e->getPackageInfo() === null) { + $e->bindPackageInfo([ + 'package_name' => $package->name, + 'package_type' => $package->type, + 'package_class' => get_class($package), + 'file' => null, + 'line' => null, + ]); } + throw $e; } return SPC_STATUS_BUILT; } From bc7ac812b138f43807a17f3c174147fcf9a35ad8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Feb 2026 18:33:21 +0800 Subject: [PATCH 270/682] phpstan, package display message enhance --- src/StaticPHP/Exception/SPCException.php | 3 +++ src/StaticPHP/Package/PackageInstaller.php | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index b989f7d93..307cf6cda 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -175,6 +175,7 @@ private function loadStackTraceInfo(): void $package_type = match (true) { $package instanceof LibraryPackage => 'library', $package instanceof PhpExtensionPackage => 'php-extension', + /* @phpstan-ignore-next-line */ $package instanceof TargetPackage => 'target', default => 'package', }; @@ -197,6 +198,7 @@ private function loadStackTraceInfo(): void $this->package_builder_info = [ 'file' => $frame['file'] ?? null, 'line' => $frame['line'] ?? null, + /* @phpstan-ignore-next-line */ 'method' => $frame['function'] ?? null, ]; continue; @@ -207,6 +209,7 @@ private function loadStackTraceInfo(): void $this->package_installer_info = [ 'file' => $frame['file'] ?? null, 'line' => $frame['line'] ?? null, + /* @phpstan-ignore-next-line */ 'method' => $frame['function'] ?? null, ]; } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index d156eb615..628900fa2 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -629,7 +629,11 @@ private function printInstallerInfo(): void InteractiveTerm::notice('Installation summary:'); $summary['Packages to be built'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->build_packages))); $summary['Packages to be installed'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->packages))); - $summary['Artifacts to be downloaded'] = implode(',', array_map(fn ($x) => $x->getName(), $this->getArtifacts())); + if (!($this->options['no-download'] ?? false)) { + $summary['Artifacts to be downloaded'] = implode(',', array_map(fn ($x) => $x->getName(), $this->getArtifacts())); + } else { + $summary['Artifacts to be downloaded'] = 'none (--no-download option enabled)'; + } $this->printArrayInfo(array_filter($summary)); echo PHP_EOL; From 471df00ea3950ec21c3bed44bcfa9f78562f014d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 19 Feb 2026 23:07:17 +0800 Subject: [PATCH 271/682] Use StaticPHP instead of static-php-cli --- README-zh.md | 8 ++++---- README.md | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README-zh.md b/README-zh.md index d8d1b3964..8dc8d0a3e 100755 --- a/README-zh.md +++ b/README-zh.md @@ -1,4 +1,4 @@ -# static-php-cli +# StaticPHP [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) @@ -6,7 +6,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) -**static-php-cli** 是一个用于构建静态、独立 PHP 运行时的强大工具,支持众多流行扩展。 +**StaticPHP** 是一个用于构建静态编译可执行文件(包括 PHP、扩展等)的强大工具。 ## 特性 @@ -80,7 +80,7 @@ download-options: ### 3. 静态 PHP 使用 -现在您可以将 static-php-cli 构建的二进制文件复制到另一台机器上,无需依赖即可运行: +现在您可以将 StaticPHP 构建的二进制文件复制到另一台机器上,无需依赖即可运行: ``` # php-cli @@ -97,7 +97,7 @@ buildroot/bin/php-fpm -v ## 文档 -当前 README 包含基本用法。有关 static-php-cli 的所有功能, +当前 README 包含基本用法。有关 StaticPHP 的所有功能, 请访问 。 ## 直接下载 diff --git a/README.md b/README.md index 3f3bfbf1e..1d355c469 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# static-php-cli +# StaticPHP [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) @@ -6,8 +6,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) -**static-php-cli** is a powerful tool designed for building static, standalone PHP runtime -with popular extensions. +**StaticPHP** is a powerful tool designed for building portable executables including PHP, extensions, and more. ## Features @@ -81,7 +80,7 @@ Run command: ### 3. Static PHP usage -Now you can copy binaries built by static-php-cli to another machine and run with no dependencies: +Now you can copy binaries built by StaticPHP to another machine and run with no dependencies: ``` # php-cli @@ -98,7 +97,7 @@ buildroot/bin/php-fpm -v ## Documentation -The current README contains basic usage. For all the features of static-php-cli, +The current README contains basic usage. For all the features of StaticPHP, see . ## Direct Download From d49545590221725cdfbc217ea4c8f70a9c661369 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Feb 2026 10:32:08 +0800 Subject: [PATCH 272/682] Remove motd for lint-config command --- src/StaticPHP/Command/Dev/LintConfigCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index 1efba4d5a..ad1efb517 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -13,6 +13,8 @@ #[AsCommand('dev:lint-config', 'Lint configuration file format', ['dev:sort-config'])] class LintConfigCommand extends BaseCommand { + protected bool $no_motd = true; + public function handle(): int { $checkOnly = $this->input->getOption('check'); @@ -37,6 +39,9 @@ public function handle(): int return static::VALIDATION_ERROR; } + if (!$hasChanges) { + $this->output->writeln('No changes.'); + } return static::SUCCESS; } From 2a8fa7d15547fab68b7b320353896b2348a31809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 23 Feb 2026 16:29:43 +0100 Subject: [PATCH 273/682] Update build flags for FrankenPHP in UnixBuilderBase --- src/SPC/builder/unix/UnixBuilderBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 2f192a12f..1b532bbcf 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -456,6 +456,7 @@ protected function buildFrankenphp(): void 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . + '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", From a35751010990ee3c9ef90582aac2f2083052c6ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 10:02:16 +0800 Subject: [PATCH 274/682] Add frankenphp building message for console output --- src/Package/Target/php/frankenphp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 066870902..889f90b39 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -29,6 +29,7 @@ public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $in } // process --with-frankenphp-app option + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); $package->runStage([$this, 'processFrankenphpApp']); // modules @@ -114,7 +115,6 @@ public function processFrankenphpApp(TargetPackage $package): void $frankenphpAppPath = $package->getBuildOption('with-frankenphp-app'); if ($frankenphpAppPath) { - InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } From 08595cca73792faf587ffcb4fa4cd3dd32844f63 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:45:06 +0800 Subject: [PATCH 275/682] Add PatchDescription attribute to libacl for Unix FPM_EXTRA_LIBS fix --- src/Package/Library/libacl.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Package/Library/libacl.php b/src/Package/Library/libacl.php index a74cb2d43..97c57d39f 100644 --- a/src/Package/Library/libacl.php +++ b/src/Package/Library/libacl.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Util\FileSystem; @@ -16,6 +17,7 @@ class libacl { #[BeforeStage('php', [php::class, 'makeForUnix'], 'libacl')] + #[PatchDescription('Fix FPM_EXTRA_LIBS to avoid linking with acl on Unix')] public function patchBeforeMakePhpUnix(LibraryPackage $lib): void { $file_path = SOURCE_PATH . '/php-src/Makefile'; From 0f012f267bd8d96642abc09df318dec7dfaf686f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:45:16 +0800 Subject: [PATCH 276/682] Rename tracker file from .spc-tracker.json to .build.json --- src/StaticPHP/Util/BuildRootTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Util/BuildRootTracker.php b/src/StaticPHP/Util/BuildRootTracker.php index 306bf90ca..eae9a21dc 100644 --- a/src/StaticPHP/Util/BuildRootTracker.php +++ b/src/StaticPHP/Util/BuildRootTracker.php @@ -15,7 +15,7 @@ class BuildRootTracker /** @var array}> Tracking data */ protected array $tracking_data = []; - protected static string $tracker_file = BUILD_ROOT_PATH . '/.spc-tracker.json'; + protected static string $tracker_file = BUILD_ROOT_PATH . '/.build.json'; protected ?DirDiff $current_diff = null; From a57b48fda6ab3ccb9b3a9b2ef2b67d4115f629ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:45:30 +0800 Subject: [PATCH 277/682] Add macOS check to patchBeforePHPConfigure for explicit_bzero detection --- src/Package/Library/postgresql.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 84b4657e0..98392ced7 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -27,8 +27,11 @@ class postgresql extends LibraryPackage #[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')] public function patchBeforePHPConfigure(TargetPackage $package): void { - shell()->cd($package->getSourceDir()) - ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + if (SystemTarget::getTargetOS() === 'Darwin') { + // on macOS, explicit_bzero is available but causes build failure due to detection issues, so we fake it as unavailable + shell()->cd($package->getSourceDir()) + ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + } } #[PatchBeforeBuild] From 3238c447451c661b23b659fa410af9fe8363b217 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:46:18 +0800 Subject: [PATCH 278/682] Refactor FrankenPHP build and smoke test processes for Unix --- src/Package/Target/php.php | 16 +- src/Package/Target/php/frankenphp.php | 27 +- src/Package/Target/php/unix.php | 354 +++++++++++++----- src/StaticPHP/Package/PhpExtensionPackage.php | 91 ++++- 4 files changed, 393 insertions(+), 95 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c21ae5909..ff92ed6a5 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,6 +4,7 @@ namespace Package\Target; +use Package\Target\php\frankenphp; use Package\Target\php\unix; use Package\Target\php\windows; use StaticPHP\Attribute\Package\BeforeStage; @@ -42,6 +43,7 @@ class php extends TargetPackage { use unix; use windows; + use frankenphp; /** @var string[] Supported major PHP versions */ public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; @@ -119,6 +121,7 @@ public function init(TargetPackage $package): void $package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d'); $package->addBuildOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); $package->addBuildOption('enable-zts', null, null, 'Enable thread safe support'); + $package->addBuildOption('no-smoke-test', null, InputOption::VALUE_OPTIONAL, 'Disable smoke test for specific SAPIs, or all if no value provided', false); // phpmicro build options if ($package->getName() === 'php' || $package->getName() === 'php-micro') { @@ -198,6 +201,11 @@ public function resolveBuild(TargetPackage $package, PackageInstaller $installer $installer->addBuildPackage('php-embed'); } + // frankenphp depends on embed SAPI (libphp.a) + if ($package->getName() === 'frankenphp') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -209,7 +217,7 @@ public function validate(Package $package): void if (!$package->getBuildOption('enable-zts')) { throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); } - // frankenphp doesn't support windows, BSD is currently not supported by static-php-cli + // frankenphp doesn't support windows, BSD is currently not supported by StaticPHP if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); } @@ -272,10 +280,10 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void // Patch StaticPHP version // detect patch (remove this when 8.3 deprecated) $file = FileSystem::readFile("{$package->getSourceDir()}/main/main.c"); - if (!str_contains($file, 'static-php-cli.version')) { + if (!str_contains($file, 'StaticPHP.version')) { $version = SPC_VERSION; - logger()->debug('Inserting static-php-cli.version to php-src'); - $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); + logger()->debug('Inserting StaticPHP.version to php-src'); + $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"StaticPHP.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file); } diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 889f90b39..d8324574c 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -7,6 +7,7 @@ use Package\Target\php; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,7 +23,7 @@ trait frankenphp { #[Stage] - public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void + public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void { if (getenv('GOROOT') === false) { throw new SPCInternalException('go-xcaddy is not initialized properly. GOROOT is not set.'); @@ -89,6 +90,7 @@ public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $in 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . + '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", @@ -103,6 +105,29 @@ public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $in $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); } + #[Stage] + public function smokeTestFrankenphpForUnix(): void + { + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); + $frankenphp = BUILD_BIN_PATH . '/frankenphp'; + if (!file_exists($frankenphp)) { + throw new ValidationException( + "FrankenPHP binary not found: {$frankenphp}", + validation_module: 'FrankenPHP smoke test' + ); + } + $prefix = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_' : 'LD_'; + [$ret, $output] = shell() + ->setEnv(["{$prefix}LIBRARY_PATH" => BUILD_LIB_PATH]) + ->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new ValidationException( + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', + validation_module: 'FrankenPHP smoke test' + ); + } + } + /** * Process the --with-frankenphp-app option * Creates app.tar and app.checksum in source/frankenphp directory diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 13c897807..bb64271e9 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -12,6 +12,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\PatchException; use StaticPHP\Exception\SPCException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,6 +23,7 @@ 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; @@ -29,8 +31,6 @@ trait unix { - use frankenphp; - #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] @@ -49,47 +49,11 @@ public function patchBeforeBuildconf(TargetPackage $package): void FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } - #[BeforeStage('php', [php::class, 'makeForUnix'], 'php')] - #[PatchDescription('Patch TSRM for musl TLS symbol visibility issue')] - #[PatchDescription('Patch ext/standard/info.c for configure command info')] - public function patchTSRMBeforeUnixMake(ToolchainInterface $toolchain): void - { - if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { - // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } else { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } - - if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef CONFIGURE_COMMAND', - '#ifdef NO_CONFIGURE_COMMAND', - ); - } else { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef NO_CONFIGURE_COMMAND', - '#ifdef CONFIGURE_COMMAND', - ); - } - } - #[Stage] public function buildconfForUnix(TargetPackage $package): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); V2CompatLayer::emitPatchPoint('before-php-buildconf'); - // run ./buildconf shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } @@ -102,6 +66,13 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args = []; $version_id = self::getPHPVersionID(); + + // disable undefined behavior sanitizer when opcache JIT is enabled (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && !$package->getBuildOption('disable-opcache-jit', false)) { + if ($version_id >= 80500 || $installer->isPackageResolved('ext-opcache')) { + f_putenv('SPC_COMPILER_EXTRA=-fno-sanitize=undefined'); + } + } // PHP JSON extension is built-in since PHP 8.0 if ($version_id < 80000) { $args[] = '--enable-json'; @@ -122,7 +93,9 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta } // 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-fpm') + ? '--enable-fpm' . ($installer->isPackageResolved('libacl') ? ' --with-fpm-acl' : '') + : '--disable-fpm'; $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { 'Linux' => '--enable-micro=all-static', default => '--enable-micro', @@ -151,23 +124,18 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - // cli if ($installer->isPackageResolved('php-cli')) { $package->runStage([self::class, 'makeCliForUnix']); } - // cgi if ($installer->isPackageResolved('php-cgi')) { $package->runStage([self::class, 'makeCgiForUnix']); } - // fpm if ($installer->isPackageResolved('php-fpm')) { $package->runStage([self::class, 'makeFpmForUnix']); } - // micro if ($installer->isPackageResolved('php-micro')) { $package->runStage([self::class, 'makeMicroForUnix']); } - // embed if ($installer->isPackageResolved('php-embed')) { $package->runStage([self::class, 'makeEmbedForUnix']); } @@ -180,6 +148,9 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cli"); @@ -195,6 +166,9 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cgi"); @@ -210,6 +184,9 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} fpm"); @@ -219,44 +196,49 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install } #[Stage] - #[PatchDescription('Patch micro.sfx after UPX compression')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { - 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' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - - $dst = BUILD_BIN_PATH . '/micro.sfx'; - $builder->deployBinary("{$package->getSourceDir()}/sapi/micro/micro.sfx", $dst); - - /* - * Patch micro.sfx after UPX compression. - * micro needs special section handling in LinuxBuilder. - * The micro.sfx does not support UPX directly, but we can remove UPX - * info segment to adapt. - * This will also make micro.sfx with upx-packed more like a malware fore antivirus - */ - if ($package->getBuildOption('with-upx-pack') && SystemTarget::getTargetOS() === 'Linux') { - // strip first - // cut binary with readelf - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + $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' : ''; + $makeArgs = $this->makeVarsToArgs($vars); + // build + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); + + $dst = BUILD_BIN_PATH . '/micro.sfx'; + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); + // patch after UPX-ed micro.sfx (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { + // cut binary with readelf to remove UPX extra segment + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); + } + $package->setOutput('Binary path for micro SAPI', $dst); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); } - - $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); } #[Stage] @@ -285,18 +267,13 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta // process libphp.so for shared embed $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; - $libphp_so_dst = $libphp_so; if (file_exists($libphp_so)) { // rename libphp.so if -release is set if (SystemTarget::getTargetOS() === 'Linux') { - // deploy libphp.so - preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); - if (!empty($matches[1])) { - $libphp_so_dst = str_replace('.so', '-' . $matches[1] . '.so', $libphp_so); - } + $this->processLibphpSoFile($libphp_so, $installer); } // deploy - $builder->deployBinary($libphp_so, $libphp_so_dst, false); + $builder->deployBinary($libphp_so, $libphp_so, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -368,16 +345,68 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa } } + #[Stage] + public function smokeTestForUnix(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + // validate option + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=cli,fpm, skip specified smoke tests + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed', 'frankenphp']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + /* + 1. if option is not in valid tests, throw WrongUsageException + 2. if all passed options are valid, remove them from $valid_tests, and run the remaining tests + */ + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + // run cli tests + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForUnix']); + } + // run cgi tests + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForUnix']); + } + // run micro tests + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForUnix'], ['skipExtTest' => $skipExtTest]); + } + // run embed tests + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForUnix']); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function build(TargetPackage $package): void { - // virtual target, do nothing - if (in_array($package->getName(), ['php-cli', 'php-fpm', 'php-cgi', 'php-micro', 'php-embed'], true)) { + // frankenphp is not a php sapi, it's a standalone Go binary that depends on libphp.a (embed) + if ($package->getName() === 'frankenphp') { + /* @var php $this */ + $package->runStage([$this, 'buildFrankenphpForUnix']); + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); return; } - if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'buildFrankenphpUnix']); + // virtual target, do nothing + if ($package->getName() !== 'php') { return; } @@ -386,6 +415,7 @@ public function build(TargetPackage $package): void $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); + $package->runStage([$this, 'smokeTestForUnix']); } /** @@ -415,6 +445,132 @@ public function patchUnixEmbedScripts(): void } } + #[Stage] + public function smokeTestCliForUnix(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliUnix(); + } + } + + #[Stage] + public function smokeTestCgiForUnix(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + [$ret, $output] = shell()->execWithResult("echo 'Hello, World!\";' | " . BUILD_BIN_PATH . '/php-cgi -n'); + $raw_output = implode('', $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!') || !str_contains($raw_output, 'text/html')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForUnix(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '/micro.sfx'; + + // micro_ext_test + InteractiveTerm::setMessage('Running php-micro ext smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '/micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + + // micro_zend_bug_test + InteractiveTerm::setMessage('Running php-micro zend bug smoke test'); + $content = file_get_contents(ROOT_DIR . '/src/globals/common-tests/micro_zend_mm_heap_corrupted.txt'); + $test_file = SOURCE_PATH . '/micro_zend_bug_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + if ($ret !== 0) { + $raw_out = trim(implode('', $out)); + throw new ValidationException( + "micro_zend_bug_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_zend_bug_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForUnix(PackageInstaller $installer, ToolchainInterface $toolchain): void + { + $sample_file_path = SOURCE_PATH . '/embed-test'; + FileSystem::createDir($sample_file_path); + // copy embed test files + copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); + copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); + + $config = new SPCConfigUtil()->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; + if ($toolchain->isStatic()) { + $lens .= ' -static'; + } + + $dynamic_exports = ''; + $envVars = []; + $embedType = 'static'; + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $embedType = 'shared'; + $libPathKey = SystemTarget::getTargetOS() === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; + $envVars[$libPathKey] = BUILD_LIB_PATH . (($existing = getenv($libPathKey)) ? ':' . $existing : ''); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a'); + } else { + $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; + foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) { + unlink($file); + } + // calling getDynamicExportedSymbols on non-Linux is okay + if ($dynamic_exports = UnixUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { + $dynamic_exports = ' ' . $dynamic_exports; + } + } + + $cc = getenv('CC'); + InteractiveTerm::setMessage('Running php-embed build smoke test'); + [$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens}{$dynamic_exports}"); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: $embedType . ' libphp embed build smoke test' + ); + } + + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = shell()->cd($sample_file_path)->setEnv($envVars)->execWithResult('./embed'); + if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . implode("\n", $output), + validation_module: $embedType . ' libphp embed run smoke test' + ); + } + } + /** * Seek php-src/config.log when building PHP, add it to exception. */ @@ -431,6 +587,26 @@ protected function seekPhpSrcLogFileOnException(callable $callback, string $sour } } + /** + * Generate micro extension test php code. + */ + private function generateMicroExtTests(PackageInstaller $installer): string + { + $php = "getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if (!$ext->isBuildStatic()) { + continue; + } + $ext_name = $ext->getDistName(); + if (!empty($ext_name)) { + $php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n"; + $php .= "assert(extension_loaded('{$ext_name}'));\n\n"; + } + } + $php .= "echo '[micro-test-end]';\n"; + return $php; + } + /** * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. */ diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 3f2f18cf3..29dd29428 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -79,7 +79,7 @@ public function getPhpConfigureArg(string $os, bool $shared): string return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', $this->getName()); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -146,6 +146,54 @@ public function buildShared(): void } } + /** + * Get the dist name used for `--ri` check in smoke test. + * Reads from config `dist-name` field, defaults to extension name. + */ + public function getDistName(): string + { + return $this->extension_config['dist-name'] ?? $this->getExtensionName(); + } + + /** + * Run smoke test for the extension on Unix CLI. + * Override this method in a subclass。 + */ + public function runSmokeTestCliUnix(): void + { + if (($this->extension_config['smoke-test'] ?? true) === false) { + return; + } + + $distName = $this->getDistName(); + // empty dist-name → no --ri check (e.g. password_argon2) + if ($distName === '') { + return; + } + + $sharedExtensions = $this->getSharedExtensionLoadString(); + [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $distName . '"', false); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed compile check: php-cli returned {$ret}", + validation_module: 'Extension ' . $this->getName() . ' sanity check' + ); + } + + $test_file = ROOT_DIR . '/src/globals/ext-tests/' . $this->getExtensionName() . '.php'; + if (file_exists($test_file)) { + // Trim additional content & escape special characters to allow inline usage + $test = self::escapeInlineTest(file_get_contents($test_file)); + [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' -r "' . trim($test) . '"'); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out), + validation_module: 'Extension ' . $this->getName() . ' function check' + ); + } + } + } + /** * Get shared extension build environment variables for Unix. * @@ -284,4 +332,45 @@ protected function splitLibsIntoStaticAndShared(string $allLibs): array } return [trim($staticLibString), trim($sharedLibString)]; } + + /** + * Builds the `-d extension_dir=... -d extension=...` string for all resolved shared extensions. + * Used in CLI smoke test to load shared extension dependencies at runtime. + */ + private function getSharedExtensionLoadString(): string + { + $sharedExts = array_filter( + $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), + fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() + ); + + if (empty($sharedExts)) { + return ''; + } + + $ret = ' -d "extension_dir=' . BUILD_MODULES_PATH . '"'; + foreach ($sharedExts as $ext) { + $extConfig = PackageConfig::get($ext->getName(), 'php-extension', []); + if ($extConfig['zend-extension'] ?? false) { + $ret .= ' -d "zend_extension=' . $ext->getExtensionName() . '"'; + } else { + $ret .= ' -d "extension=' . $ext->getExtensionName() . '"'; + } + } + + return $ret; + } + + /** + * Escape PHP test file content for inline `-r` usage. + * Strips Date: Thu, 26 Feb 2026 15:46:33 +0800 Subject: [PATCH 279/682] Add extension apcu --- config/pkg/ext/ext-apcu.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 config/pkg/ext/ext-apcu.yml diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml new file mode 100644 index 000000000..289de301c --- /dev/null +++ b/config/pkg/ext/ext-apcu.yml @@ -0,0 +1,11 @@ +ext-apcu: + type: php-extension + artifact: + source: + type: url + url: 'https://pecl.php.net/get/APCu' + extract: php-src/ext/apcu + filename: apcu.tgz + metadata: + license-files: [LICENSE] + license: PHP-3.01 From e9279940d7af55195420f1fcfba4a54badbf75f8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 16:09:18 +0800 Subject: [PATCH 280/682] Add DumpStagesCommand to dump package stages and their locations --- .../Command/Dev/DumpStagesCommand.php | 157 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Package/Package.php | 10 ++ src/StaticPHP/Registry/PackageLoader.php | 20 +++ 4 files changed, 189 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/DumpStagesCommand.php diff --git a/src/StaticPHP/Command/Dev/DumpStagesCommand.php b/src/StaticPHP/Command/Dev/DumpStagesCommand.php new file mode 100644 index 000000000..4b20fe21b --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -0,0 +1,157 @@ +addArgument('packages', InputArgument::OPTIONAL, 'Comma-separated list of packages to dump, e.g. "openssl,zlib,curl". Dumps all packages if omitted.'); + $this->addArgument('output', InputArgument::OPTIONAL, 'Output file path', ROOT_DIR . '/dump-stages.json'); + $this->addOption('relative', 'r', InputOption::VALUE_NONE, 'Output file paths relative to ROOT_DIR'); + } + + public function handle(): int + { + $outputFile = $this->getArgument('output'); + $useRelative = (bool) $this->getOption('relative'); + + $filterPackages = null; + if ($packagesArg = $this->getArgument('packages')) { + $filterPackages = array_flip(parse_comma_list($packagesArg)); + } + + $result = []; + + foreach (PackageLoader::getPackages() as $name => $pkg) { + if ($filterPackages !== null && !isset($filterPackages[$name])) { + continue; + } + $entry = [ + 'type' => $pkg->getType(), + 'stages' => [], + 'before_stages' => [], + 'after_stages' => [], + ]; + + // Resolve main stages + foreach ($pkg->getStages() as $stageName => $callable) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry['stages'][$stageName] = $location; + } + + $result[$name] = $entry; + } + + // Resolve before/after stage external callbacks + foreach (PackageLoader::getAllBeforeStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['before_stages'][$stageName][] = $entry_data; + } + } + } + + foreach (PackageLoader::getAllAfterStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['after_stages'][$stageName][] = $entry_data; + } + } + } + + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + + $this->output->writeln('Dumped stages for ' . count($result) . " package(s) to: {$outputFile}"); + return static::SUCCESS; + } + + /** + * Resolve the file, start line, class and method name of a callable using reflection. + * + * @return null|array{file: string, line: false|int, class: string, method: string} + */ + private function resolveCallableLocation(mixed $callable): ?array + { + try { + if (is_array($callable) && count($callable) === 2) { + $ref = new \ReflectionMethod($callable[0], $callable[1]); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if ($callable instanceof \Closure) { + $ref = new \ReflectionFunction($callable); + $scopeClass = $ref->getClosureScopeClass(); + return [ + 'class' => $scopeClass !== null ? $scopeClass->getName() : '{closure}', + 'method' => '{closure}', + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if (is_string($callable) && str_contains($callable, '::')) { + [$class, $method] = explode('::', $callable, 2); + $ref = new \ReflectionMethod($class, $method); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + } catch (\ReflectionException) { + // ignore + } + return null; + } + + private function toRelativePath(string $absolutePath): string + { + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($absolutePath, $root)) { + return substr($absolutePath, strlen($root)); + } + return $absolutePath; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 8608f7617..4afa221c1 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\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; @@ -67,6 +68,7 @@ public function __construct() new EnvCommand(), new LintConfigCommand(), new PackLibCommand(), + new DumpStagesCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 64b9f2e44..fd59e98bd 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -128,6 +128,16 @@ public function addStage(string $name, callable $stage): void $this->stages[$name] = $stage; } + /** + * Get all defined stages for this package. + * + * @return array + */ + public function getStages(): array + { + return $this->stages; + } + /** * Check if the package has a specific stage defined. * diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index ca195ff0e..573ad7da8 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -240,6 +240,26 @@ public static function loadFromClass(mixed $class): void } } + /** + * Get all registered before-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllBeforeStages(): array + { + return self::$before_stages; + } + + /** + * Get all registered after-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllAfterStages(): array + { + return self::$after_stages; + } + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { // match condition From da1f348daa1daad6bc6fd7e1f996f62fce9e19d1 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 27 Feb 2026 09:18:28 +0800 Subject: [PATCH 281/682] Update src/StaticPHP/Package/PhpExtensionPackage.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Package/PhpExtensionPackage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 29dd29428..582216d7e 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -157,7 +157,7 @@ public function getDistName(): string /** * Run smoke test for the extension on Unix CLI. - * Override this method in a subclass。 + * Override this method in a subclass. */ public function runSmokeTestCliUnix(): void { From 0e80f29e61ac6a8ebf55eeb839740a311eea64d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:27:19 +0800 Subject: [PATCH 282/682] Add DumpCapabilitiesCommand to output installable and buildable capabilities of packages --- .../Command/Dev/DumpCapabilitiesCommand.php | 111 ++++++++++++++++++ .../Command/Dev/DumpStagesCommand.php | 7 +- src/StaticPHP/ConsoleApplication.php | 4 + 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php diff --git a/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php new file mode 100644 index 000000000..e2f3dba99 --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php @@ -0,0 +1,111 @@ +addArgument('output', InputArgument::OPTIONAL, 'Output file path (JSON). Defaults to /dump-capabilities.json', ROOT_DIR . '/dump-capabilities.json'); + $this->addOption('print', null, InputOption::VALUE_NONE, 'Print capabilities as a table to the terminal instead of writing to a file'); + } + + public function handle(): int + { + $result = $this->buildCapabilities(); + + if ($this->getOption('print')) { + $this->printTable($result); + } else { + $outputFile = $this->getArgument('output'); + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + $this->output->writeln('Dumped capabilities for ' . count($result) . " package(s) to: {$outputFile}"); + } + + return static::SUCCESS; + } + + /** + * Build the capabilities map for all relevant packages. + * + * For library/target/virtual-target: + * buildable: string[] - OS families with a registered #[BuildFor] function + * installable: string[] - arch-os platforms with a declared binary + * + * For php-extension: + * buildable: array - {OS: 'yes'|'wip'|'partial'|'no'} (v2 support semantics) + * installable: (not applicable, omitted) + */ + private function buildCapabilities(): array + { + $result = []; + + // library / target / virtual-target + foreach (PackageLoader::getPackages(['library', 'target', 'virtual-target']) as $name => $pkg) { + $installable = []; + $artifact = $pkg->getArtifact(); + if ($artifact !== null) { + $installable = $artifact->getBinaryPlatforms(); + } + + $result[$name] = [ + 'type' => $pkg->getType(), + 'buildable' => $pkg->getBuildForOSList(), + 'installable' => $installable, + ]; + } + + // php-extension: buildable uses v2 support-field semantics + foreach (PackageLoader::getPackages('php-extension') as $name => $pkg) { + /* @var PhpExtensionPackage $pkg */ + $result[$name] = [ + 'type' => $pkg->getType(), + 'buildable' => $pkg->getBuildSupportStatus(), + ]; + } + + return $result; + } + + private function printTable(array $result): void + { + $table = new Table($this->output); + $table->setHeaders(['Package', 'Type', 'Buildable (OS)', 'Installable (arch-os)']); + + foreach ($result as $name => $info) { + // For php-extension, buildable is a map {OS => status} + if (is_array($info['buildable']) && array_is_list($info['buildable']) === false) { + $buildableStr = implode("\n", array_map( + static fn (string $os, string $status) => $status === 'yes' ? $os : "{$os} ({$status})", + array_keys($info['buildable']), + array_values($info['buildable']) + )); + } else { + $buildableStr = implode("\n", $info['buildable']) ?: ''; + } + + $table->addRow([ + $name, + $info['type'], + $buildableStr, + implode("\n", $info['installable'] ?? []) ?: '', + ]); + } + + $table->render(); + } +} diff --git a/src/StaticPHP/Command/Dev/DumpStagesCommand.php b/src/StaticPHP/Command/Dev/DumpStagesCommand.php index 4b20fe21b..c757ab868 100644 --- a/src/StaticPHP/Command/Dev/DumpStagesCommand.php +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -148,10 +148,11 @@ private function resolveCallableLocation(mixed $callable): ?array private function toRelativePath(string $absolutePath): string { + $normalized = realpath($absolutePath) ?: $absolutePath; $root = rtrim(ROOT_DIR, '/') . '/'; - if (str_starts_with($absolutePath, $root)) { - return substr($absolutePath, strlen($root)); + if (str_starts_with($normalized, $root)) { + return substr($normalized, strlen($root)); } - return $absolutePath; + return $normalized; } } diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 4afa221c1..023ddf840 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,10 +6,12 @@ use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\Dev\DumpCapabilitiesCommand; use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; +use StaticPHP\Command\Dev\PackageInfoCommand; use StaticPHP\Command\Dev\PackLibCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; @@ -69,6 +71,8 @@ public function __construct() new LintConfigCommand(), new PackLibCommand(), new DumpStagesCommand(), + new DumpCapabilitiesCommand(), + new PackageInfoCommand(), ]); // add additional commands from registries From d6ec0b78095993a18b62a67d505487513637a4e5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:31:37 +0800 Subject: [PATCH 283/682] Remove aarch64 build fix for glibc 2.17 from patchBeforeBuild method in postgresql.php --- src/Package/Library/postgresql.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 98392ced7..18893d0ec 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -10,7 +10,6 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Exception\FileSystemException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -38,20 +37,6 @@ public function patchBeforePHPConfigure(TargetPackage $package): void #[PatchDescription('Various patches before building PostgreSQL')] public function patchBeforeBuild(): bool { - // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) - if (SystemTarget::getLibcVersion() === '2.17' && SystemTarget::getTargetArch() === 'aarch64') { - try { - FileSystem::replaceFileStr("{$this->getSourceDir()}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); - FileSystem::replaceFileStr( - "{$this->getSourceDir()}/src/port/pg_crc32c_armv8_choose.c", - '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', - '#if defined(__linux__) && !defined(HWCAP_CRC32)' - ); - } catch (FileSystemException) { - // allow file not-existence to make it compatible with old and new version - } - } - // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries FileSystem::replaceFileStr("{$this->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); // disable shared libs build From cfce1770704e7b53be09ed7b1c1606330eccadc1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:42:28 +0800 Subject: [PATCH 284/682] Add beforeMakeUnix method to patch TSRM.h for musl TLS symbol visibility --- src/Package/Target/php/unix.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index bb64271e9..10884c317 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -116,6 +116,26 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch TSRM.h to fix musl TLS symbol visibility for non-static builds')] + public function beforeMakeUnix(ToolchainInterface $toolchain): void + { + if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { + // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } else { + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } + } + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { From 28c82b811b4ebfc4bc99e1fd7dda417d16f73005 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:50:21 +0800 Subject: [PATCH 285/682] Add PackageInfoCommand to display package configuration information and support status --- .gitignore | 3 + src/StaticPHP/Artifact/Artifact.php | 33 +++ .../Command/Dev/PackageInfoCommand.php | 193 ++++++++++++++++++ src/StaticPHP/Package/Package.php | 10 + src/StaticPHP/Package/PhpExtensionPackage.php | 21 ++ 5 files changed, 260 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/PackageInfoCommand.php diff --git a/.gitignore b/.gitignore index d33eae535..2a351fcd9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ log/ # spc.phar spc.phar spc.exe + +# dumped files from StaticPHP v3 +/dump-*.json diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index dc602538b..6dc35ad58 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -237,6 +237,39 @@ public function hasPlatformBinary(): bool return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]); } + /** + * Get all platform strings for which a binary is declared (config or custom callback). + * + * For platforms where the binary type is "custom", a registered custom_binary_callback + * is required to consider it truly installable. + * + * @return string[] e.g. ['linux-x86_64', 'linux-aarch64', 'macos-aarch64'] + */ + public function getBinaryPlatforms(): array + { + $platforms = []; + if (isset($this->config['binary']) && is_array($this->config['binary'])) { + foreach ($this->config['binary'] as $platform => $platformConfig) { + $type = is_array($platformConfig) ? ($platformConfig['type'] ?? '') : ''; + if ($type === 'custom') { + // Only installable if a custom callback has been registered + if (isset($this->custom_binary_callbacks[$platform])) { + $platforms[] = $platform; + } + } else { + $platforms[] = $platform; + } + } + } + // Include custom callbacks for platforms not listed in config at all + foreach (array_keys($this->custom_binary_callbacks) as $platform) { + if (!in_array($platform, $platforms, true)) { + $platforms[] = $platform; + } + } + return $platforms; + } + public function getDownloadConfig(string $type): mixed { return $this->config[$type] ?? null; diff --git a/src/StaticPHP/Command/Dev/PackageInfoCommand.php b/src/StaticPHP/Command/Dev/PackageInfoCommand.php new file mode 100644 index 000000000..7c8691993 --- /dev/null +++ b/src/StaticPHP/Command/Dev/PackageInfoCommand.php @@ -0,0 +1,193 @@ +addArgument('package', InputArgument::REQUIRED, 'Package name to inspect'); + $this->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON instead of colored terminal display'); + } + + public function handle(): int + { + $packageName = $this->getArgument('package'); + + if (!PackageConfig::isPackageExists($packageName)) { + $this->output->writeln("Package '{$packageName}' not found."); + return static::USER_ERROR; + } + + $pkgConfig = PackageConfig::get($packageName); + $artifactConfig = ArtifactConfig::get($packageName); + $pkgInfo = Registry::getPackageConfigInfo($packageName); + $artifactInfo = Registry::getArtifactConfigInfo($packageName); + + if ($this->getOption('json')) { + return $this->outputJson($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + } + + return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + } + + private function outputJson(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + { + $data = [ + 'name' => $name, + 'registry' => $pkgInfo['registry'] ?? null, + 'package_config_file' => $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : null, + 'package' => $pkgConfig, + ]; + + if ($artifactConfig !== null) { + $data['artifact_config_file'] = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : null; + $data['artifact'] = $this->splitArtifactConfig($artifactConfig); + } + + $this->output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::SUCCESS; + } + + private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + { + $type = $pkgConfig['type'] ?? 'unknown'; + $registry = $pkgInfo['registry'] ?? 'unknown'; + $pkgFile = $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : 'unknown'; + + // Header + $this->output->writeln(''); + $this->output->writeln("Package: {$name} Type: {$type} Registry: {$registry}"); + $this->output->writeln("Config file: {$pkgFile}"); + $this->output->writeln(''); + + // Package config fields (excluding type and artifact which are shown separately) + $pkgFields = array_diff_key($pkgConfig, array_flip(['type', 'artifact'])); + if (!empty($pkgFields)) { + $this->output->writeln('── Package Config ──'); + $this->printYamlBlock($pkgFields, 0); + $this->output->writeln(''); + } + + // Artifact config + if ($artifactConfig !== null) { + $artifactFile = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : 'unknown'; + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); + + // Check if artifact config is inline (embedded in pkg config) or separate + $inlineArtifact = $pkgConfig['artifact'] ?? null; + if (is_array($inlineArtifact)) { + $this->output->writeln(' (inline in package config)'); + } + + $split = $this->splitArtifactConfig($artifactConfig); + + foreach ($split as $section => $value) { + $this->output->writeln(''); + $this->output->writeln(" [{$section}]"); + $this->printYamlBlock($value, 4); + } + $this->output->writeln(''); + } else { + $this->output->writeln('── Artifact Config ── (none)'); + $this->output->writeln(''); + } + + return static::SUCCESS; + } + + /** + * Split artifact config into logical sections for cleaner display. + * + * @return array + */ + private function splitArtifactConfig(array $config): array + { + $sections = []; + $sectionOrder = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata']; + foreach ($sectionOrder as $key) { + if (array_key_exists($key, $config)) { + $sections[$key] = $config[$key]; + } + } + // Any remaining unknown keys + foreach ($config as $k => $v) { + if (!array_key_exists($k, $sections)) { + $sections[$k] = $v; + } + } + return $sections; + } + + /** + * Print a value as indented YAML-style output with Symfony Console color tags. + */ + private function printYamlBlock(mixed $value, int $indent): void + { + $pad = str_repeat(' ', $indent); + if (!is_array($value)) { + $this->output->writeln($pad . $this->colorScalar($value)); + return; + } + $isList = array_is_list($value); + foreach ($value as $k => $v) { + if ($isList) { + if (is_array($v)) { + $this->output->writeln($pad . '- '); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . '- ' . $this->colorScalar($v)); + } + } else { + if (is_array($v)) { + $this->output->writeln($pad . "{$k}:"); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . "{$k}: " . $this->colorScalar($v)); + } + } + } + } + + private function colorScalar(mixed $v): string + { + if (is_bool($v)) { + return '' . ($v ? 'true' : 'false') . ''; + } + if (is_int($v) || is_float($v)) { + return '' . $v . ''; + } + if ($v === null) { + return 'null'; + } + // Strings that look like URLs + if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) { + return '' . $v . ''; + } + return '' . $v . ''; + } + + private function toRelativePath(string $absolutePath): string + { + $normalized = realpath($absolutePath) ?: $absolutePath; + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($normalized, $root)) { + return substr($normalized, strlen($root)); + } + return $normalized; + } +} diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index fd59e98bd..1ec1a503e 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -138,6 +138,16 @@ public function getStages(): array return $this->stages; } + /** + * Get the list of OS families that have a registered build function (via #[BuildFor]). + * + * @return string[] e.g. ['Linux', 'Darwin'] + */ + public function getBuildForOSList(): array + { + return array_keys($this->build_functions); + } + /** * Check if the package has a specific stage defined. * diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 582216d7e..07bc6abd8 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -280,6 +280,27 @@ public function buildSharedForUnix(PackageBuilder $builder): void $builder->deployBinary($soFile, $soFile, false); } + /** + * Get per-OS build support status for this php-extension. + * + * Rules (same as v2): + * - OS not listed in 'support' config => 'yes' (fully supported) + * - OS listed with 'wip' => 'wip' + * - OS listed with 'partial' => 'partial' + * - OS listed with 'no' => 'no' + * + * @return array e.g. ['Linux' => 'yes', 'Darwin' => 'partial', 'Windows' => 'no'] + */ + public function getBuildSupportStatus(): array + { + $exceptions = $this->extension_config['support'] ?? []; + $result = []; + foreach (['Linux', 'Darwin', 'Windows'] as $os) { + $result[$os] = $exceptions[$os] ?? 'yes'; + } + return $result; + } + /** * Register default stages if not already defined by attributes. * This is called after all attributes have been loaded. From f9fe2adb1d61e9ab8d5d4391d6c3d894db476d48 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:52:02 +0800 Subject: [PATCH 286/682] Trim quotes from frankenphp app path to ensure valid directory check --- src/Package/Target/php/frankenphp.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index d8324574c..669d00c38 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -140,6 +140,7 @@ public function processFrankenphpApp(TargetPackage $package): void $frankenphpAppPath = $package->getBuildOption('with-frankenphp-app'); if ($frankenphpAppPath) { + $frankenphpAppPath = trim($frankenphpAppPath, "\"'"); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } From b3d67b928a950723a952ab4f724de487feff7d83 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:54:40 +0800 Subject: [PATCH 287/682] Add tryPatchMakefileUnix method to fix //lib path in Makefile for Linux builds --- src/Package/Target/php/unix.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 10884c317..888ae8bda 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -136,6 +136,18 @@ public function beforeMakeUnix(ToolchainInterface $toolchain): void } } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch Makefile to fix //lib path for Linux builds')] + public function tryPatchMakefileUnix(): void + { + if (SystemTarget::getTargetOS() !== 'Linux') { + return; + } + + // replace //lib with /lib in Makefile + shell()->cd(SOURCE_PATH . '/php-src')->exec('sed -i "s|//lib|/lib|g" Makefile'); + } + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { @@ -168,9 +180,6 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cli"); @@ -186,9 +195,6 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cgi"); @@ -204,9 +210,6 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} fpm"); @@ -231,9 +234,6 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; $makeArgs = $this->makeVarsToArgs($vars); // build - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); From 8c7d113c2f802edb4ca3e0e88edd45f1ca04d8de Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:59:55 +0800 Subject: [PATCH 288/682] Apply smoke test control option for frankenphp --- src/Package/Target/php/frankenphp.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 669d00c38..8b2fb81d4 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -106,8 +106,19 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller } #[Stage] - public function smokeTestFrankenphpForUnix(): void + public function smokeTestFrankenphpForUnix(PackageBuilder $builder): void { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test', false); + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,... + }; + if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) { + return; + } + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); $frankenphp = BUILD_BIN_PATH . '/frankenphp'; if (!file_exists($frankenphp)) { From fa175963f93fcf73da71d5295e023f3b70485534 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 10:03:25 +0800 Subject: [PATCH 289/682] Enable suggested libs by default in build configurations for Unix and Windows --- .github/workflows/build-unix.yml | 2 +- .github/workflows/build-windows-x86_64.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index bf6df9ac4..9a40960e5 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -49,7 +49,7 @@ on: with-suggested-libs: description: Build with suggested libs type: boolean - default: false + default: true debug: description: Show full build logs type: boolean diff --git a/.github/workflows/build-windows-x86_64.yml b/.github/workflows/build-windows-x86_64.yml index 57a681848..a53d9e437 100644 --- a/.github/workflows/build-windows-x86_64.yml +++ b/.github/workflows/build-windows-x86_64.yml @@ -29,6 +29,10 @@ on: description: prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Build with suggested libs + type: boolean + default: true debug: description: enable debug logs type: boolean From 7623b9e673c398fa103d0d82f6794b3d3d3f3627 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 09:47:51 +0800 Subject: [PATCH 290/682] Deprecate '--debug' option and update logging level handling --- src/StaticPHP/Command/BaseCommand.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index ddcb3671d..5673e6ba3 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -87,6 +87,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug', default => 'warning', }; + $isDebug = false; + // if '--debug' is set, override log level to debug + if ($this->input->getOption('debug')) { + $level = 'debug'; + logger()->warning('The --debug option is deprecated and will be removed in future versions. Please use -vv or -vvv to enable debug mode.'); + $this->output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + $isDebug = true; + } logger()->setLevel($level); // ansi @@ -95,7 +103,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Set debug mode in ApplicationContext - $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; + $isDebug = $isDebug ?: $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; ApplicationContext::setDebug($isDebug); // show raw argv list for logger()->debug From c218aef9478be4a055b7accb892a7920bbc899d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 10:32:50 +0800 Subject: [PATCH 291/682] Add doctor cache check and version management to ensure environment validation --- .gitignore | 3 ++ src/Package/Target/php.php | 20 ++++++++ src/StaticPHP/Command/BaseCommand.php | 16 +++++++ src/StaticPHP/Command/BuildLibsCommand.php | 2 + src/StaticPHP/Command/BuildTargetCommand.php | 2 + src/StaticPHP/Command/DoctorCommand.php | 1 + src/StaticPHP/Command/DownloadCommand.php | 2 + src/StaticPHP/Doctor/Doctor.php | 48 ++++++++++++++++++++ 8 files changed, 94 insertions(+) diff --git a/.gitignore b/.gitignore index 2a351fcd9..21bae186b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ packlib_files.txt .php-cs-fixer.cache .phpunit.result.cache +# doctor cache fallback (when ~/.cache/spc/ is not writable) +.spc-doctor.lock + # exclude self-runtime /bin/* !/bin/spc* diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index ff92ed6a5..54d41dc5c 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -7,6 +7,7 @@ use Package\Target\php\frankenphp; use Package\Target\php\unix; use Package\Target\php\windows; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; @@ -104,6 +105,24 @@ public static function getPHPVersion(?string $from_custom_source = null, bool $r throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); } + /** + * Get PHP version from source archive filename + * + * @return null|string PHP version (e.g., "8.4.0") + */ + public static function getPHPVersionFromArchive(bool $return_null_if_failed = false): ?string + { + $archives = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src'); + $filename = $archives['filename'] ?? ''; + if (!preg_match('/php-(\d+\.\d+\.\d+(?:RC\d+|alpha\d+|beta\d+)?)\.tar\.(?:gz|bz2|xz)/', $filename, $match)) { + if ($return_null_if_failed) { + return null; + } + throw new WrongUsageException('PHP source archive filename format is malformed (got: ' . $filename . ')'); + } + return $match[1]; + } + #[InitPackage] public function init(TargetPackage $package): void { @@ -255,6 +274,7 @@ public function info(Package $package, PackageInstaller $installer): array 'Build Target' => getenv('SPC_TARGET') ?: '', 'Build Toolchain' => ToolchainManager::getToolchainClass(), 'Build SAPI' => implode(', ', $sapis), + 'PHP Version' => self::getPHPVersion(return_null_if_failed: true) ?? self::getPHPVersionFromArchive(return_null_if_failed: true) ?? 'Unknown', 'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)), 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), 'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)), diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index 5673e6ba3..fcd39e5fc 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -5,6 +5,7 @@ namespace StaticPHP\Command; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Doctor\Doctor; use StaticPHP\Exception\ExceptionHandler; use StaticPHP\Exception\SPCException; use Symfony\Component\Console\Command\Command; @@ -118,6 +119,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + /** + * Warn the user if doctor has not been run (or is outdated). + * Set SPC_SKIP_DOCTOR_CHECK=1 to suppress. + */ + protected function checkDoctorCache(): void + { + if (getenv('SPC_SKIP_DOCTOR_CHECK') || Doctor::isHealthy()) { + return; + } + $this->output->writeln(''); + $this->output->writeln('[WARNING] Please run `spc doctor` first to verify your build environment.'); + $this->output->writeln(''); + sleep(2); + } + protected function getOption(string $name): mixed { return $this->input->getOption($name); diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index 63a3ad0f9..c18acb0fc 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -44,6 +44,8 @@ public function configure(): void public function handle(): int { + $this->checkDoctorCache(); + $libs = parse_comma_list($this->input->getArgument('libraries')); $installer = new PackageInstaller($this->input->getOptions()); diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index 2756070bf..8e1ed6329 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -37,6 +37,8 @@ public function __construct(private readonly string $target, ?string $descriptio public function handle(): int { + $this->checkDoctorCache(); + // resolve legacy options to new options V2CompatLayer::convertOptions($this->input); diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index 6ae6d68a1..40303d141 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -26,6 +26,7 @@ public function handle(): int }; $doctor = new Doctor($this->output, $fix_policy); if ($doctor->checkAll()) { + Doctor::markPassed(); $this->output->writeln('Doctor check complete !'); return static::SUCCESS; } diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 270f55385..e021e58b9 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -56,6 +56,8 @@ public function handle(): int return $this->handleClean(); } + $this->checkDoctorCache(); + $downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions())); // arguments diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 36db37ae8..fc69cc2a8 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -9,6 +9,7 @@ use StaticPHP\Exception\SPCException; use StaticPHP\Registry\DoctorLoader; use StaticPHP\Runtime\Shell\Shell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Output\OutputInterface; use ZM\Logger\ConsoleColor; @@ -25,6 +26,29 @@ public function __construct(private ?OutputInterface $output = null, private int logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); } + /** + * Returns true if doctor was previously passed with the current SPC version. + */ + public static function isHealthy(): bool + { + $lock = self::getLockPath(); + return file_exists($lock) && trim((string) @file_get_contents($lock)) === \StaticPHP\ConsoleApplication::VERSION; + } + + /** + * Write current SPC version to the lock file, marking doctor as passed. + */ + public static function markPassed(): void + { + $primary = self::getLockPath(); + if (!is_dir(dirname($primary))) { + @mkdir(dirname($primary), 0755, true); + } + if (@file_put_contents($primary, \StaticPHP\ConsoleApplication::VERSION) === false) { + @file_put_contents((getcwd() ?: '.') . DIRECTORY_SEPARATOR . '.spc-doctor.lock', \StaticPHP\ConsoleApplication::VERSION); + } + } + /** * Check all valid check items. * @return bool true if all checks passed, false otherwise @@ -119,6 +143,30 @@ public function checkItem(CheckItem|string $check, bool $interactive = true): bo return false; } + private static function getLockPath(): string + { + if (SystemTarget::getTargetOS() === 'Windows') { + $trial_ls = [ + getenv('LOCALAPPDATA') ?: ((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock', + sys_get_temp_dir() . '\.spc-doctor.lock', + WORKING_DIR . '\.spc-doctor.lock', + ]; + } else { + $trial_ls = [ + getenv('XDG_CACHE_HOME') ?: ((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock', + sys_get_temp_dir() . '/.spc-doctor.lock', + WORKING_DIR . '/.spc-doctor.lock', + ]; + } + foreach ($trial_ls as $path) { + if (is_writable(dirname($path))) { + return $path; + } + } + // fallback to current directory + return WORKING_DIR . DIRECTORY_SEPARATOR . '.spc-doctor.lock'; + } + private function emitFix(string $fix_item, array $fix_item_params = []): bool { keyboard_interrupt_register(function () { From d316684995d32dee5d16b2c2eca86fb21822df08 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 10:37:38 +0800 Subject: [PATCH 292/682] Add optional package support for libaom, libsharpyuv, libjpeg, libxml2, and libpng in Unix build --- config/pkg/lib/libavif.yml | 7 +++++++ src/Package/Library/libavif.php | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/config/pkg/lib/libavif.yml b/config/pkg/lib/libavif.yml index 0d7ae151d..c75b05c45 100644 --- a/config/pkg/lib/libavif.yml +++ b/config/pkg/lib/libavif.yml @@ -7,5 +7,12 @@ libavif: metadata: license-files: [LICENSE] license: BSD-2-Clause + depends: + - libaom + suggests: + - libwebp + - libjpeg + - libxml2 + - libpng static-libs@unix: - libavif.a diff --git a/src/Package/Library/libavif.php b/src/Package/Library/libavif.php index 87e6c650f..6db235e19 100644 --- a/src/Package/Library/libavif.php +++ b/src/Package/Library/libavif.php @@ -17,6 +17,11 @@ class libavif public function buildUnix(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) + ->optionalPackage('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF') + ->optionalPackage('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF') + ->optionalPackage('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF') + ->optionalPackage('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF') + ->optionalPackage('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF') ->addConfigureArgs('-DAVIF_LIBYUV=OFF') ->build(); // patch pkgconfig From 2d550a8db44f90bb063487f15111ba330727c2ad Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:43:28 +0800 Subject: [PATCH 293/682] Add simple output handling to exception classes --- src/StaticPHP/Exception/InterruptException.php | 5 ++++- src/StaticPHP/Exception/RegistryException.php | 5 ++++- src/StaticPHP/Exception/SPCException.php | 12 ++++++++++++ src/StaticPHP/Exception/WrongUsageException.php | 5 ++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Exception/InterruptException.php b/src/StaticPHP/Exception/InterruptException.php index 77b5240a3..3f55d7a85 100644 --- a/src/StaticPHP/Exception/InterruptException.php +++ b/src/StaticPHP/Exception/InterruptException.php @@ -7,4 +7,7 @@ /** * Exception caused by manual intervention. */ -class InterruptException extends SPCException {} +class InterruptException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php index 347a132ad..17d65cf2c 100644 --- a/src/StaticPHP/Exception/RegistryException.php +++ b/src/StaticPHP/Exception/RegistryException.php @@ -4,4 +4,7 @@ namespace StaticPHP\Exception; -class RegistryException extends SPCException {} +class RegistryException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index 307cf6cda..7ec27abec 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -20,6 +20,8 @@ */ abstract class SPCException extends \Exception { + protected bool $simple_output = false; + /** @var null|array Package information */ private ?array $package_info = null; @@ -155,6 +157,16 @@ public function getExtraLogFiles(): array return $this->extra_log_files; } + public function isSimpleOutput(): bool + { + return $this->simple_output; + } + + public function setSimpleOutput(bool $simple_output = true): void + { + $this->simple_output = $simple_output; + } + /** * Load stack trace information to detect Package, Builder, and Installer context. */ diff --git a/src/StaticPHP/Exception/WrongUsageException.php b/src/StaticPHP/Exception/WrongUsageException.php index 2044a82c0..631a242ab 100644 --- a/src/StaticPHP/Exception/WrongUsageException.php +++ b/src/StaticPHP/Exception/WrongUsageException.php @@ -10,4 +10,7 @@ * This exception is used to indicate that the SPC is being used incorrectly. * Such as when a command is not supported or an invalid argument is provided. */ -class WrongUsageException extends SPCException {} +class WrongUsageException extends SPCException +{ + protected bool $simple_output = true; +} From ed5a516004355e699f72470c39d824367baf76ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:44:23 +0800 Subject: [PATCH 294/682] Implement check-update functionality for artifacts and enhance download result handling --- src/StaticPHP/Artifact/ArtifactCache.php | 10 ++- src/StaticPHP/Artifact/ArtifactDownloader.php | 39 +++++++++++ .../Artifact/Downloader/DownloadResult.php | 26 +++++--- .../Artifact/Downloader/Type/BitBucketTag.php | 2 +- .../Downloader/Type/CheckUpdateInterface.php | 20 ++++++ .../Downloader/Type/CheckUpdateResult.php | 14 ++++ .../Artifact/Downloader/Type/FileList.php | 38 +++++++---- .../Artifact/Downloader/Type/Git.php | 64 +++++++++++++++++-- .../Downloader/Type/GitHubRelease.php | 19 +++++- .../Downloader/Type/GitHubTarball.php | 20 +++++- .../Downloader/Type/HostedPackageBin.php | 4 +- .../Artifact/Downloader/Type/LocalDir.php | 2 +- .../Artifact/Downloader/Type/PIE.php | 48 +++++++++----- .../Artifact/Downloader/Type/PhpRelease.php | 52 +++++++++++---- .../Artifact/Downloader/Type/Url.php | 2 +- src/StaticPHP/Command/CheckUpdateCommand.php | 64 +++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Exception/ExceptionHandler.php | 13 +--- 18 files changed, 365 insertions(+), 74 deletions(-) create mode 100644 src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php create mode 100644 src/StaticPHP/Command/CheckUpdateCommand.php diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index 3302a37bc..dcd75ef7d 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -18,7 +18,8 @@ class ArtifactCache * filename?: string, * dirname?: string, * extract: null|'&custom'|string, - * hash: null|string + * hash: null|string, + * downloader: null|string * }, * binary: array{ * windows-x86_64?: null|array{ @@ -28,7 +29,8 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, - * version?: null|string + * version?: null|string, + * downloader: null|string * } * } * }> @@ -108,6 +110,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'file') { $obj = [ @@ -118,6 +121,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'git') { $obj = [ @@ -128,6 +132,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'local') { $obj = [ @@ -138,6 +143,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'hash' => null, 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } if ($obj === null) { diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index fd3caeaf1..b2773c808 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,6 +6,8 @@ use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; use StaticPHP\Artifact\Downloader\Type\Git; use StaticPHP\Artifact\Downloader\Type\LocalDir; @@ -323,6 +325,43 @@ public function download(bool $interactive = true): void } } + public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false): CheckUpdateResult + { + $artifact = ArtifactLoader::getArtifactInstance($artifact_name); + if ($artifact === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); + } + if ($bare) { + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + throw new WrongUsageException("Artifact '{$artifact_name}' has no source config for bare update check."); + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); + } + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $config, null, $this); + } + $cache = ApplicationContext::get(ArtifactCache::class); + if ($prefer_source) { + $info = $cache->getSourceInfo($artifact_name) ?? $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()); + } else { + $info = $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()) ?? $cache->getSourceInfo($artifact_name); + } + if ($info === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' is not downloaded yet, cannot check update."); + } + if (is_a($info['downloader'] ?? null, CheckUpdateInterface::class, true)) { + $cls = $info['downloader']; + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); + } + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } + public function getRetry(): int { return $this->retry; diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index 6fa40bed7..2efe6945e 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -17,6 +17,7 @@ class DownloadResult * @param bool $verified Whether the download has been verified (hash check) * @param null|string $version Version of the downloaded artifact (e.g., "1.2.3", "v2.0.0") * @param array $metadata Additional metadata (e.g., commit hash, release notes, etc.) + * @param null|string $downloader Class name of the downloader that performed this download */ private function __construct( public readonly string $cache_type, @@ -27,6 +28,7 @@ private function __construct( public bool $verified = false, public readonly ?string $version = null, public readonly array $metadata = [], + public readonly ?string $downloader = null, ) { switch ($this->cache_type) { case 'archive': @@ -59,11 +61,12 @@ public static function archive( mixed $extract = null, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { // 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); + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } public static function file( @@ -71,10 +74,11 @@ public static function file( array $config, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -85,9 +89,9 @@ public static function file( * @param null|string $version Version string (tag, branch, or commit) * @param array $metadata Additional metadata (e.g., commit hash) */ - public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -98,9 +102,9 @@ public static function git(string $dirname, array $config, mixed $extract = null * @param null|string $version Version string if known * @param array $metadata Additional metadata */ - public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -136,7 +140,8 @@ public function withVersion(string $version): self $this->extract, $this->verified, $version, - $this->metadata + $this->metadata, + $this->downloader, ); } @@ -154,7 +159,8 @@ public function withMeta(string $key, mixed $value): self $this->extract, $this->verified, $this->version, - array_merge($this->metadata, [$key => $value]) + array_merge($this->metadata, [$key => $value]), + $this->downloader, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php index 30942fe17..2ecc48dff 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -36,6 +36,6 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}"); default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php new file mode 100644 index 000000000..184456484 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -0,0 +1,20 @@ +fetchFileList($name, $config, $downloader); + if (isset($config['download-url'])) { + $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); + } else { + $url = $config['url'] . $filename; + } + $filename = end($versions); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchFileList($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + + protected function fetchFileList(string $name, array $config, ArtifactDownloader $downloader): array { logger()->debug("Fetching file list from {$config['url']}"); $page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry()); @@ -33,15 +58,6 @@ public function download(string $name, array $config, ArtifactDownloader $downlo uksort($versions, 'version_compare'); $filename = end($versions); $version = array_key_last($versions); - if (isset($config['download-url'])) { - $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); - } else { - $url = $config['url'] . $filename; - } - $filename = end($versions); - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; - logger()->debug("Downloading {$name} from URL: {$url}"); - default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return [$filename, $version, $versions]; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 83c236eb4..f518b396c 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -10,7 +10,7 @@ use StaticPHP\Util\FileSystem; /** git */ -class Git implements DownloadTypeInterface +class Git implements DownloadTypeInterface, CheckUpdateInterface { public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { @@ -21,8 +21,10 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // 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); + $hash_result = shell(false)->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; + $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } if (!isset($config['regex'])) { throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); @@ -64,8 +66,62 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $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); + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (isset($config['rev'])) { + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url']) . ' ' . escapeshellarg('refs/heads/' . $config['rev'])); + if ($result[0] !== 0 || empty($result[1])) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $new_hash = substr($result[1][0], 0, 40); + $new_version = "dev-{$config['rev']}+{$new_hash}"; + // Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash + $old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_hash === null || $new_hash !== $old_hash, + ); + } + if (!isset($config['regex'])) { + throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); + } + + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url'])); + if ($result[0] !== 0) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $refs = $result[1]; + $matched_version_branch = []; + + $regex = '/^' . $config['regex'] . '$/'; + foreach ($refs as $ref) { + $matches = null; + if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) { + $branch = $matches[1]; + if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) { + $matched_version_branch[$vermatch['version']] = $vermatch[0]; + } + } + } + uksort($matched_version_branch, function ($a, $b) { + return version_compare($b, $a); + }); + if (!empty($matched_version_branch)) { + $version = array_key_first($matched_version_branch); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + throw new DownloaderException("No matching branch found for regex {$config['regex']}."); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 7b0412886..15626089a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -9,7 +9,7 @@ use StaticPHP\Exception\DownloaderException; /** ghrel */ -class GitHubRelease implements DownloadTypeInterface, ValidatorInterface +class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -48,6 +48,7 @@ public function getGitHubReleases(string $name, string $repo, bool $prefer_stabl */ public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array { + logger()->debug("Fetching {$name} GitHub release from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); @@ -95,7 +96,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} asset from URL: {$asset_url}"); default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -117,4 +118,18 @@ public function validate(string $name, array $config, ArtifactDownloader $downlo logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation"); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (!isset($config['match'])) { + throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); + } + $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 8aa1ac694..a9283722f 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -10,7 +10,7 @@ /** ghtar */ /** ghtagtar */ -class GitHubTarball implements DownloadTypeInterface +class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -77,6 +77,22 @@ public function download(string $name, array $config, ArtifactDownloader $downlo [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $rel_type = match ($config['type']) { + 'ghtar' => 'releases', + 'ghtagtar' => 'tags', + default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), + }; + $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php index c5cbb3b50..11caa19db 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -26,7 +26,7 @@ class HostedPackageBin implements DownloadTypeInterface public static function getReleaseInfo(): array { if (empty(self::$release_info)) { - $rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO); + $rel = new GitHubRelease()->getGitHubReleases('hosted', self::BASE_REPO); if (empty($rel)) { throw new DownloaderException('No releases found for hosted package-bin'); } @@ -55,7 +55,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; $headers = $this->getGitHubTokenHeaders(); default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}"); diff --git a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php index 93315ce3a..77ac3d093 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php +++ b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php @@ -13,6 +13,6 @@ class LocalDir implements DownloadTypeInterface public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { logger()->debug("Using local source directory for {$name} from {$config['dirname']}"); - return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null); + return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index e4f1a1173..3a3ccc02e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -9,11 +9,42 @@ use StaticPHP\Exception\DownloaderException; /** pie */ -class PIE implements DownloadTypeInterface +class PIE implements DownloadTypeInterface, CheckUpdateInterface { public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/'; public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + $first = $this->fetchPackagistInfo($name, $config, $downloader); + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $config['repo']); + $version = $first['version'] ?? 'unknown'; + $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $first = $this->fetchPackagistInfo($name, $config, $downloader); + $new_version = $first['version'] ?? null; + if ($new_version === null) { + throw new DownloaderException("failed to find version info for {$name} from packagist"); + } + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || version_compare($new_version, $old_version, '>'), + ); + } + + protected function fetchPackagistInfo(string $name, array $config, ArtifactDownloader $downloader): array { $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); @@ -25,23 +56,10 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { throw new DownloaderException("failed to find {$name} repo info from packagist"); } - // get the first version $first = $data['packages'][$config['repo']][0] ?? []; - // check 'type' => 'php-ext' or contains 'php-ext' key if (!isset($first['php-ext'])) { throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); } - // get download link from dist - $dist_url = $first['dist']['url'] ?? null; - $dist_type = $first['dist']['type'] ?? null; - if (!$dist_url || !$dist_type) { - throw new DownloaderException("failed to find {$name} dist info from packagist"); - } - $name = str_replace('/', '_', $config['repo']); - $version = $first['version'] ?? 'unknown'; - $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; - default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return $first; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index ec6c33fa4..372c7f50a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -8,7 +8,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; -class PhpRelease implements DownloadTypeInterface, ValidatorInterface +class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; @@ -24,16 +24,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $this->sha256 = null; return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); } - - // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); - if ($info === false) { - throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); - } - $info = json_decode($info, true); - if (!is_array($info) || !isset($info['version'])) { - throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); - } + $info = $this->fetchPhpReleaseInfo($name, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -49,7 +40,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -73,4 +64,41 @@ public function validate(string $name, array $config, ArtifactDownloader $downlo logger()->debug("SHA256 checksum validated successfully for {$name}."); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $phpver = $downloader->getOption('with-php', '8.4'); + if ($phpver === 'git') { + // git version: delegate to Git checkUpdate with master branch + return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); + } + $info = $this->fetchPhpReleaseInfo($name, $downloader); + $new_version = $info['version']; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } + + protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloader): array + { + $phpver = $downloader->getOption('with-php', '8.4'); + // Handle 'git' version to clone from php-src repository + if ($phpver === 'git') { + // cannot fetch release info for git version, return empty info to skip validation + throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); + } + + // Fetch PHP release info first + $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + if ($info === false) { + throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); + } + $info = json_decode($info, true); + if (!is_array($info) || !isset($info['version'])) { + throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + } + return $info; + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php index a56f4dc71..02425fe5d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Url.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -18,6 +18,6 @@ public function download(string $name, array $config, ArtifactDownloader $downlo logger()->debug("Downloading {$name} from URL: {$url}"); $version = $config['version'] ?? null; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php new file mode 100644 index 000000000..965fb2018 --- /dev/null +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -0,0 +1,64 @@ +addArgument('artifact', InputArgument::REQUIRED, 'The name of the artifact(s) to check for updates, comma-separated'); + $this->addOption('json', null, null, 'Output result in JSON format'); + $this->addOption('bare', null, null, 'Check update without requiring the artifact to be downloaded first (old version will be null)'); + + // --with-php option for checking updates with a specific PHP version context + $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'); + } + + public function handle(): int + { + $artifacts = parse_comma_list($this->input->getArgument('artifact')); + + try { + $downloader = new ArtifactDownloader($this->input->getOptions()); + $bare = (bool) $this->getOption('bare'); + if ($this->getOption('json')) { + $outputs = []; + foreach ($artifacts as $artifact) { + $result = $downloader->checkUpdate($artifact, bare: $bare); + $outputs[$artifact] = [ + 'need-update' => $result->needUpdate, + 'old' => $result->old, + 'new' => $result->new, + ]; + } + $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::OK; + } + foreach ($artifacts as $artifact) { + $result = $downloader->checkUpdate($artifact, bare: $bare); + if (!$result->needUpdate) { + $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); + } else { + $this->output->writeln("Update available for artifact: {$artifact}"); + $this->output->writeln(" Old version: {$result->old}"); + $this->output->writeln(" New version: {$result->new}"); + } + } + return static::OK; + } catch (SPCException $e) { + $e->setSimpleOutput(); + throw $e; + } + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 023ddf840..a02b38c7d 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\CheckUpdateCommand; use StaticPHP\Command\Dev\DumpCapabilitiesCommand; use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; @@ -63,6 +64,7 @@ public function __construct() new SPCConfigCommand(), new DumpLicenseCommand(), new ResetCommand(), + new CheckUpdateCommand(), // dev commands new ShellCommand(), diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 20cf9395e..9dddc9102 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -29,12 +29,6 @@ class ExceptionHandler RegistryException::class, ]; - public const array MINOR_LOG_EXCEPTIONS = [ - InterruptException::class, - WrongUsageException::class, - RegistryException::class, - ]; - /** @var array Build PHP extra info binding */ private static array $build_php_extra_info = []; @@ -57,10 +51,7 @@ public static function handleSPCException(SPCException $e): int }; self::logError($head_msg); - // ---------------------------------------- - $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); - - if ($minor_logs) { + if ($e->isSimpleOutput()) { return self::getReturnCode($e); } @@ -283,6 +274,6 @@ private static function printModuleErrorInfo(SPCException $e): void self::printArrayInfo($info); } - self::logError("---------------------------------------------------------\n", color: 'none'); + self::logError("-----------------------------------------------------------\n", color: 'none'); } } From 40e36982d342a7abcaba4277f8ebee5ee9a52c02 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:55:52 +0800 Subject: [PATCH 295/682] Add custom binary check-update support for artifacts --- src/Package/Artifact/go_xcaddy.php | 21 ++++++++++++++ src/Package/Artifact/zig.php | 29 +++++++++++++++++++ src/StaticPHP/Artifact/Artifact.php | 21 ++++++++++++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 9 ++++-- .../Artifact/CustomBinaryCheckUpdate.php | 11 +++++++ src/StaticPHP/Registry/ArtifactLoader.php | 22 ++++++++++++++ 6 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 5fa7327e3..056a10e38 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -6,8 +6,10 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\GlobalEnvManager; @@ -65,6 +67,25 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); } + #[CustomBinaryCheckUpdate('go-xcaddy', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new \RuntimeException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + #[AfterBinaryExtract('go-xcaddy', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 9a1430637..0d334e5f1 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -6,8 +6,10 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; @@ -59,6 +61,33 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); } + #[CustomBinaryCheckUpdate('zig', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); + $index_json = json_decode($index_json ?: '', true); + $latest_version = null; + foreach ($index_json as $version => $data) { + if ($version !== 'master') { + $latest_version = $version; + break; + } + } + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Zig version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || version_compare($latest_version, $old_version, '>'), + ); + } + #[AfterBinaryExtract('zig', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 6dc35ad58..8bdd86a85 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -30,6 +30,9 @@ class Artifact /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; + /** @var array Bind custom binary check-update callbacks */ + protected array $custom_binary_check_update_callbacks = []; + /** @var null|callable Bind custom source extract callback (completely takes over extraction) */ protected mixed $source_extract_callback = null; @@ -433,6 +436,24 @@ public function setCustomBinaryCallback(string $target_os, callable $callback): $this->custom_binary_callbacks[$target_os] = $callback; } + /** + * Set custom binary check-update callback for a specific target OS. + * + * @param string $target_os Target OS platform string (e.g. linux-x86_64) + * @param callable $callback Custom binary check-update callback + */ + public function setCustomBinaryCheckUpdateCallback(string $target_os, callable $callback): void + { + ConfigValidator::validatePlatformString($target_os); + $this->custom_binary_check_update_callbacks[$target_os] = $callback; + } + + public function getCustomBinaryCheckUpdateCallback(): ?callable + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_check_update_callbacks[$current_platform] ?? null; + } + // ==================== Extraction Callbacks ==================== /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b2773c808..7740d27e5 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -358,8 +358,13 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, /** @var CheckUpdateInterface $downloader */ $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); - } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } // custom binary: delegate to registered check-update callback + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); } public function getRetry(): int diff --git a/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php new file mode 100644 index 000000000..aa59af1a2 --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php @@ -0,0 +1,11 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); + self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); self::processBinaryExtractAttribute($ref, $method, $class_instance); self::processAfterSourceExtractAttribute($ref, $method, $class_instance); @@ -118,6 +120,26 @@ private static function processCustomBinaryAttribute(\ReflectionClass $ref, \Ref } } + /** + * Process #[CustomBinaryCheckUpdate] attribute. + */ + private static function processCustomBinaryCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomBinaryCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomBinaryCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + foreach ($instance->support_os as $os) { + self::$artifacts[$artifact_name]->setCustomBinaryCheckUpdateCallback($os, [$class_instance, $method->getName()]); + } + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinaryCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[SourceExtract] attribute. * This attribute allows completely taking over the source extraction process. From 550f6cad6048ec5dea7c7752d73d0cc1ac0a7fdb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:02:32 +0800 Subject: [PATCH 296/682] Replace RuntimeException with DownloaderException for Go version retrieval failure --- src/Package/Artifact/go_xcaddy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 056a10e38..ca61bd465 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -77,7 +77,7 @@ public function checkUpdateBinary(?string $old_version, ArtifactDownloader $down { [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); if ($version === '') { - throw new \RuntimeException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } return new CheckUpdateResult( old: $old_version, From 0a07f6b27cfc1454f9eb3e51cb656521d66b50f2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:07:05 +0800 Subject: [PATCH 297/682] cs fix --- src/StaticPHP/Artifact/ArtifactDownloader.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 7740d27e5..219dfb8b2 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -364,7 +364,8 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, ArtifactDownloader::class => $this, 'old_version' => $info['version'], ]); - } throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); } public function getRetry(): int From a7b04d908144c2a17e6fc65d82cfe7f1815852ef Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:16:57 +0800 Subject: [PATCH 298/682] Update src/StaticPHP/Artifact/Downloader/Type/Git.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/Git.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index f518b396c..4a7120052 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -21,7 +21,8 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // direct branch clone if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $hash_result = shell(false)->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); From 64b0e7290873ee1ec5df1452ec4d8049882b8c87 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:17:48 +0800 Subject: [PATCH 299/682] Update src/StaticPHP/Artifact/Downloader/Type/PIE.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/PIE.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index 3a3ccc02e..a84cffe51 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -27,7 +27,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null, downloader: static::class); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); } public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult From 6ef5e9e067f9fb21169efbb230dc3a1b47668915 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:18:32 +0800 Subject: [PATCH 300/682] Update src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php index 184456484..1adcdfeae 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -13,7 +13,7 @@ interface CheckUpdateInterface * * @param string $name the name of the artifact * @param array $config the configuration for the artifact - * @param string $old_version old version or identifier of the artifact to compare against + * @param null|string $old_version old version or identifier of the artifact to compare against * @param ArtifactDownloader $downloader the artifact downloader instance */ public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult; From 28f4a5c52387e40d02112fdc9fcc128ed03c56e2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:35:48 +0800 Subject: [PATCH 301/682] Add support for custom source check-update callbacks in artifacts --- src/StaticPHP/Artifact/Artifact.php | 16 +++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 71 ++++++++++++++++--- .../Artifact/CustomSourceCheckUpdate.php | 11 +++ src/StaticPHP/Registry/ArtifactLoader.php | 20 ++++++ 4 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 8bdd86a85..841775e36 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -27,6 +27,9 @@ class Artifact /** @var null|callable Bind custom source fetcher callback */ protected mixed $custom_source_callback = null; + /** @var null|callable Bind custom source check-update callback */ + protected mixed $custom_source_check_update_callback = null; + /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; @@ -408,6 +411,19 @@ public function getCustomSourceCallback(): ?callable return $this->custom_source_callback ?? null; } + /** + * Set custom source check-update callback. + */ + public function setCustomSourceCheckUpdateCallback(callable $callback): void + { + $this->custom_source_check_update_callback = $callback; + } + + public function getCustomSourceCheckUpdateCallback(): ?callable + { + return $this->custom_source_check_update_callback ?? null; + } + public function getCustomBinaryCallback(): ?callable { $current_platform = SystemTarget::getCurrentPlatformString(); diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 219dfb8b2..86c06c19f 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -332,17 +332,14 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); } if ($bare) { - $config = $artifact->getDownloadConfig('source'); - if (!is_array($config)) { - throw new WrongUsageException("Artifact '{$artifact_name}' has no source config for bare update check."); + [$first, $second] = $prefer_source + ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name)] + : [fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name), fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name)]; + $result = $first() ?? $second(); + if ($result !== null) { + return $result; } - $cls = $this->downloaders[$config['type']] ?? null; - if (!is_a($cls, CheckUpdateInterface::class, true)) { - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); - } - /** @var CheckUpdateInterface $downloader */ - $downloader = new $cls(); - return $downloader->checkUpdate($artifact_name, $config, null, $this); + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); } $cache = ApplicationContext::get(ArtifactCache::class); if ($prefer_source) { @@ -358,7 +355,15 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, /** @var CheckUpdateInterface $downloader */ $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); - } // custom binary: delegate to registered check-update callback + } + // custom source: delegate to registered check-update callback + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } + // custom binary: delegate to registered check-update callback if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, @@ -383,6 +388,50 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + return null; + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $config, null, $this); + } + + private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + // custom binary callback takes precedence over config-based binary + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $binary_config = $artifact->getDownloadConfig('binary'); + $platform_config = is_array($binary_config) ? ($binary_config[SystemTarget::getCurrentPlatformString()] ?? null) : null; + if (!is_array($platform_config)) { + return null; + } + $cls = $this->downloaders[$platform_config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $platform_config, null, $this); + } + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int { $queue = $this->generateQueue($artifact); diff --git a/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php new file mode 100644 index 000000000..df6e07d65 --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php @@ -0,0 +1,11 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); + self::processCustomSourceCheckUpdateAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); @@ -100,6 +102,24 @@ private static function processCustomSourceAttribute(\ReflectionClass $ref, \Ref } } + /** + * Process #[CustomSourceCheckUpdate] attribute. + */ + private static function processCustomSourceCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomSourceCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomSourceCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setCustomSourceCheckUpdateCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSourceCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[CustomBinary] attribute. */ From 029f8efa120672547e40862b7c2d0fc9b5ce5739 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:55:18 +0800 Subject: [PATCH 302/682] Avoid empty output --- src/StaticPHP/Command/CheckUpdateCommand.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 965fb2018..9a2d5f5cc 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -51,8 +51,9 @@ public function handle(): int $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); } else { $this->output->writeln("Update available for artifact: {$artifact}"); - $this->output->writeln(" Old version: {$result->old}"); - $this->output->writeln(" New version: {$result->new}"); + [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; + $this->output->writeln(" Old version: {$old}"); + $this->output->writeln(" New version: {$new}"); } } return static::OK; From 4f2ca17bde89314b6fbadb41f059aa2321dfd520 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 15:16:31 +0800 Subject: [PATCH 303/682] cs fix --- src/StaticPHP/Artifact/ArtifactDownloader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 86c06c19f..b5e540786 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -356,14 +356,14 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); } - // custom source: delegate to registered check-update callback + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, 'old_version' => $info['version'], ]); } - // custom binary: delegate to registered check-update callback + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, From 174ef3dba7dcae8f87adc02defc6a21e3f985f87 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 15:36:04 +0800 Subject: [PATCH 304/682] Refactor ReturnCode constants for clarity and consistency --- src/StaticPHP/Command/ReturnCode.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/StaticPHP/Command/ReturnCode.php b/src/StaticPHP/Command/ReturnCode.php index d152101ef..5accebc3a 100644 --- a/src/StaticPHP/Command/ReturnCode.php +++ b/src/StaticPHP/Command/ReturnCode.php @@ -17,26 +17,28 @@ trait ReturnCode { public const int OK = 0; - public const SUCCESS = 0; // alias of OK + public const SUCCESS = 0; // alias - public const int INTERNAL_ERROR = 1; // unsorted or internal error + public const FAILURE = 1; // generic failure - /** @deprecated Use specified error code instead */ - public const FAILURE = 1; + // 64-69: reserved for standard errors + public const int USER_ERROR = 64; // wrong usage, bad arguments - public const int USER_ERROR = 2; // wrong usage or user error + public const int VALIDATION_ERROR = 65; // invalid input or config values - public const int ENVIRONMENT_ERROR = 3; // environment not suitable for operation + public const int ENVIRONMENT_ERROR = 69; // required tools/env not available - public const int VALIDATION_ERROR = 4; // validation failed + // 70+: application-specific errors + public const int INTERNAL_ERROR = 70; // internal logic error or unexpected state - public const int FILE_SYSTEM_ERROR = 5; // file system related error + public const int BUILD_ERROR = 72; // build / compile process failed - public const int DOWNLOAD_ERROR = 6; // network related error + public const int PATCH_ERROR = 73; // patching or modifying files failed - public const int BUILD_ERROR = 7; // build process error + public const int FILE_SYSTEM_ERROR = 74; // filesystem / IO error - public const int PATCH_ERROR = 8; // patching process error + public const int DOWNLOAD_ERROR = 75; // network / remote resource error - public const int INTERRUPT_SIGNAL = 130; // process interrupted by user (e.g., Ctrl+C) + // 128+: reserved for standard signals and interrupts + public const int INTERRUPT_SIGNAL = 130; // SIGINT (Ctrl+C) } From dc0a80975f9d878a13c3d3428d363b16571287a7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 17:07:24 +0800 Subject: [PATCH 305/682] Add PECL download type and support for PECL artifacts --- config/downloader.php | 2 + config/pkg/ext/builtin-extensions.yml | 29 +++++++ config/pkg/ext/ext-amqp.yml | 6 +- config/pkg/ext/ext-apcu.yml | 6 +- config/pkg/ext/ext-ast.yml | 6 ++ config/pkg/ext/ext-mbregex.yml | 10 --- config/pkg/ext/ext-mbstring.yml | 4 - config/pkg/ext/ext-phar.yml | 4 - config/pkg/ext/ext-readline.yml | 11 --- src/StaticPHP/Artifact/ArtifactCache.php | 4 +- .../Artifact/Downloader/Type/Git.php | 4 +- .../Artifact/Downloader/Type/PECL.php | 79 +++++++++++++++++++ src/StaticPHP/Config/ConfigValidator.php | 1 + src/StaticPHP/Runtime/Shell/DefaultShell.php | 9 ++- 14 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 config/pkg/ext/ext-ast.yml delete mode 100644 config/pkg/ext/ext-mbregex.yml delete mode 100644 config/pkg/ext/ext-mbstring.yml delete mode 100644 config/pkg/ext/ext-phar.yml delete mode 100644 config/pkg/ext/ext-readline.yml create mode 100644 src/StaticPHP/Artifact/Downloader/Type/PECL.php diff --git a/config/downloader.php b/config/downloader.php index 48710a888..2d81a57e3 100644 --- a/config/downloader.php +++ b/config/downloader.php @@ -10,6 +10,7 @@ use StaticPHP\Artifact\Downloader\Type\GitHubTarball; use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; +use StaticPHP\Artifact\Downloader\Type\PECL; use StaticPHP\Artifact\Downloader\Type\PhpRelease; use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; @@ -24,6 +25,7 @@ 'ghtagtar' => GitHubTarball::class, 'local' => LocalDir::class, 'pie' => PIE::class, + 'pecl' => PECL::class, 'url' => Url::class, 'php-release' => PhpRelease::class, 'hosted' => HostedPackageBin::class, diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index d12cd2191..0149fe974 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -1,5 +1,19 @@ ext-bcmath: type: php-extension +ext-mbregex: + type: php-extension + depends: + - onig + - ext-mbstring + php-extension: + arg-type: custom + build-shared: false + build-static: true + display-name: mbstring +ext-mbstring: + type: php-extension + php-extension: + arg-type: custom ext-openssl: type: php-extension depends: @@ -10,6 +24,21 @@ ext-openssl: arg-type: custom arg-type@windows: with build-with-php: true +ext-phar: + type: php-extension + depends: + - zlib +ext-readline: + type: php-extension + depends: + - libedit + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path + build-shared: false + build-static: true ext-zlib: type: php-extension depends: diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml index 937569144..1c8023602 100644 --- a/config/pkg/ext/ext-amqp.yml +++ b/config/pkg/ext/ext-amqp.yml @@ -2,10 +2,8 @@ ext-amqp: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/amqp' - extract: php-src/ext/amqp - filename: amqp.tgz + type: pecl + name: amqp metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml index 289de301c..331b04f76 100644 --- a/config/pkg/ext/ext-apcu.yml +++ b/config/pkg/ext/ext-apcu.yml @@ -2,10 +2,8 @@ ext-apcu: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/APCu' - extract: php-src/ext/apcu - filename: apcu.tgz + type: pecl + name: APCu metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml new file mode 100644 index 000000000..0684959dd --- /dev/null +++ b/config/pkg/ext/ext-ast.yml @@ -0,0 +1,6 @@ +ext-ast: + type: php-extension + artifact: + source: + type: pecl + name: ast diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml deleted file mode 100644 index ae59f0235..000000000 --- a/config/pkg/ext/ext-mbregex.yml +++ /dev/null @@ -1,10 +0,0 @@ -ext-mbregex: - type: php-extension - depends: - - onig - - ext-mbstring - php-extension: - arg-type: custom - build-shared: false - build-static: true - display-name: mbstring diff --git a/config/pkg/ext/ext-mbstring.yml b/config/pkg/ext/ext-mbstring.yml deleted file mode 100644 index 6583ca616..000000000 --- a/config/pkg/ext/ext-mbstring.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-mbstring: - type: php-extension - php-extension: - arg-type: custom diff --git a/config/pkg/ext/ext-phar.yml b/config/pkg/ext/ext-phar.yml deleted file mode 100644 index 3625d2c00..000000000 --- a/config/pkg/ext/ext-phar.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-phar: - type: php-extension - depends: - - zlib diff --git a/config/pkg/ext/ext-readline.yml b/config/pkg/ext/ext-readline.yml deleted file mode 100644 index 19b1886c8..000000000 --- a/config/pkg/ext/ext-readline.yml +++ /dev/null @@ -1,11 +0,0 @@ -ext-readline: - type: php-extension - depends: - - libedit - php-extension: - support: - Windows: wip - BSD: wip - arg-type: with-path - build-shared: false - build-static: true diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index dcd75ef7d..ea3bde412 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -163,7 +163,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}"); } // save cache to file - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** @@ -281,7 +281,7 @@ public function removeBinary(string $artifact_name, string $platform, bool $dele */ public function save(): void { - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 4a7120052..1ee9da4da 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -22,7 +22,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); - $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD'); $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); @@ -80,7 +80,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A if ($result[0] !== 0 || empty($result[1])) { throw new DownloaderException("Failed to ls-remote from {$config['url']}"); } - $new_hash = substr($result[1][0], 0, 40); + $new_hash = substr($result[1][0], 0, 7); $new_version = "dev-{$config['rev']}+{$new_hash}"; // Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash $old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null; diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php new file mode 100644 index 000000000..0b14b05d4 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -0,0 +1,79 @@ +VERSIONSTATE per release */ + private const string PECL_REST_URL = 'https://pecl.php.net/rest/r/%s/allreleases.xml'; + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchPECLInfo($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + [$filename, $version] = $this->fetchPECLInfo($name, $config, $downloader); + $url = self::PECL_BASE_URL . '/get/' . $filename; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + $extract = $config['extract'] ?? ('php-src/ext/' . $this->getExtractName($name)); + return DownloadResult::archive($filename, $config, $extract, version: $version, downloader: static::class); + } + + protected function fetchPECLInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $peclName = strtolower($config['name'] ?? $this->getExtractName($name)); + $url = sprintf(self::PECL_REST_URL, $peclName); + logger()->debug("Fetching PECL release list for {$name} from REST API"); + $xml = default_shell()->executeCurl($url, retries: $downloader->getRetry()); + if ($xml === false) { + throw new DownloaderException("Failed to fetch PECL release list for {$name}"); + } + // Match VERSIONSTATE + preg_match_all('/(?P[^<]+)<\/v>(?P[^<]+)<\/s><\/r>/', $xml, $matches); + if (empty($matches['version'])) { + throw new DownloaderException("Failed to parse PECL release list for {$name}"); + } + $versions = []; + logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL"); + foreach ($matches['version'] as $i => $version) { + if ($matches['state'][$i] !== 'stable') { + continue; + } + $versions[$version] = $peclName . '-' . $version . '.tgz'; + } + if (empty($versions)) { + throw new DownloaderException("No stable releases found for {$name} on PECL"); + } + uksort($versions, 'version_compare'); + $filename = end($versions); + $version = array_key_last($versions); + return [$filename, $version, $versions]; + } + + /** + * Derive the lowercase PECL package / extract name from the artifact name. + * e.g. "ext-apcu" -> "apcu", "ext-ast" -> "ast" + */ + private function getExtractName(string $name): string + { + return strtolower(preg_replace('/^ext-/i', '', $name)); + } +} diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index fbf883213..83f9ca41c 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -89,6 +89,7 @@ class ConfigValidator 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], + 'pecl' => [['name'], ['extract']], 'php-release' => [[], ['extract']], 'custom' => [[], ['func']], ]; diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 5b50d1528..66dfb7ab0 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -25,7 +25,7 @@ public function exec(string $cmd): static /** * Execute a cURL command to fetch data from a URL. */ - public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): false|string + public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0, bool $compressed = false): false|string { foreach ($hooks as $hook) { $hook($method, $url, $headers); @@ -39,7 +39,8 @@ public function executeCurl(string $url, string $method = 'GET', array $headers }; $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; - $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}"; + $compressed_arg = $compressed ? '--compressed' : ''; + $cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}"; $this->logCommandInfo($cmd); $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); @@ -72,7 +73,7 @@ public function executeCurlDownload(string $url, string $path, array $headers = $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; $check = $this->console_putput ? '#' : 's'; - $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); + $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL --max-time 3600 {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); $this->logCommandInfo($cmd); logger()->debug('[CURL DOWNLOAD] ' . $cmd); $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); @@ -93,7 +94,7 @@ public function executeGitClone(string $url, string $branch, string $path, bool $path_arg = escapeshellarg($path); $shallow_arg = $shallow ? '--depth 1 --single-branch' : ''; $submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : ''); - $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); + $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --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); From 12d4009a21c418c6c303125e8901c32b5c79e0bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Mar 2026 16:32:16 +0800 Subject: [PATCH 306/682] Update PHP release handling to use configurable mirror and improve URL management --- config/artifact/php-src.yml | 4 +++ .../Artifact/Downloader/Type/PhpRelease.php | 28 +++++++++++++------ src/StaticPHP/Config/ConfigValidator.php | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/config/artifact/php-src.yml b/config/artifact/php-src.yml index 32bcb6cf0..e304db9db 100644 --- a/config/artifact/php-src.yml +++ b/config/artifact/php-src.yml @@ -5,3 +5,7 @@ php-src: license: PHP-3.01 source: type: php-release + domain: 'https://www.php.net' + source-mirror: + type: php-release + domain: 'https://phpmirror.static-php.dev' diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index 372c7f50a..b1fad70e8 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -10,9 +10,15 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { - public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; + public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; - public const string DOWNLOAD_URL = 'https://www.php.net/distributions/php-{version}.tar.xz'; + public const string API_URL = '/releases/index.php?json&version={version}'; + + public const string DOWNLOAD_URL = '/distributions/php-{version}.tar.xz'; + + public const string GIT_URL = 'https://github.com/php/php-src.git'; + + public const string GIT_REV = 'master'; private ?string $sha256 = ''; @@ -22,9 +28,9 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // Handle 'git' version to clone from php-src repository if ($phpver === 'git') { $this->sha256 = null; - return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); + return (new Git())->download($name, ['url' => self::GIT_URL, 'rev' => self::GIT_REV], $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $downloader); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -36,7 +42,8 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (!isset($filename)) { throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); } - $url = str_replace('{version}', $version, self::DOWNLOAD_URL); + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= str_replace('{version}', $version, self::DOWNLOAD_URL); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); @@ -72,7 +79,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A // git version: delegate to Git checkUpdate with master branch return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $downloader); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $new_version = $info['version']; return new CheckUpdateResult( old: $old_version, @@ -81,7 +88,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A ); } - protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloader): array + protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): array { $phpver = $downloader->getOption('with-php', '8.4'); // Handle 'git' version to clone from php-src repository @@ -90,8 +97,13 @@ protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloa throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); } + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= self::API_URL; + $url = str_replace('{version}', $phpver, $url); + logger()->debug("Fetching PHP release info for version {$phpver} from {$url}"); + // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + $info = default_shell()->executeCurl($url, retries: $downloader->getRetry()); if ($info === false) { throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 83f9ca41c..f011482c7 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -90,7 +90,7 @@ class ConfigValidator 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], 'pecl' => [['name'], ['extract']], - 'php-release' => [[], ['extract']], + 'php-release' => [['domain'], ['extract']], 'custom' => [[], ['func']], ]; From 671ebd258284856fc5fa5770732233f2330b259d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Mar 2026 16:32:49 +0800 Subject: [PATCH 307/682] Use gmp mirror site --- config/pkg/lib/gmp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml index bdc13b559..c14697748 100644 --- a/config/pkg/lib/gmp.yml +++ b/config/pkg/lib/gmp.yml @@ -3,7 +3,7 @@ gmp: artifact: source: type: filelist - url: 'https://gmplib.org/download/gmp/' + url: 'https://ftp.gnu.org/gnu/gmp/' regex: '/href="(?gmp-(?[^"]+)\.tar\.xz)"/' source-mirror: type: url From 00c08e0c0ca27cb37848b1f1d57aa66c783c8966 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:11:41 +0800 Subject: [PATCH 308/682] Use no optional libs for libxml2 --- config/pkg/lib/libxml2.yml | 3 +-- src/Package/Library/libxml2.php | 12 ++++-------- src/StaticPHP/Artifact/ArtifactCache.php | 6 ++++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index db88e8b14..7e86b5af5 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -10,9 +10,8 @@ libxml2: license: MIT depends@unix: - libiconv - suggests@unix: - - xz - zlib + - xz headers: - libxml2 pkg-configs: diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php index 7c35d6855..3f8b3e71f 100644 --- a/src/Package/Library/libxml2.php +++ b/src/Package/Library/libxml2.php @@ -17,17 +17,13 @@ class libxml2 public function buildForLinux(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) - ->optionalPackage( - 'zlib', - '-DLIBXML2_WITH_ZLIB=ON ' . - "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a " . - "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", - '-DLIBXML2_WITH_ZLIB=OFF', - ) - ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) ->addConfigureArgs( '-DLIBXML2_WITH_ICONV=ON', '-DIconv_IS_BUILT_IN=OFF', + '-DLIBXML2_WITH_ZLIB=ON', + "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a", + "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", + '-DLIBXML2_WITH_LZMA=ON', '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md '-DLIBXML2_WITH_PYTHON=OFF', '-DLIBXML2_WITH_PROGRAMS=OFF', diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index dcd75ef7d..2cdd0d0ae 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -19,6 +19,7 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, + * time: int, * downloader: null|string * }, * binary: array{ @@ -29,6 +30,7 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, + * time: int, * version?: null|string, * downloader: null|string * } @@ -108,6 +110,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -119,6 +122,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -130,6 +134,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -141,6 +146,7 @@ public function lock(Artifact|string $artifact, string $lock_type, DownloadResul 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => null, + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, From f7277cc01238a4306e574853a16ab89447a72aa0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:17:13 +0800 Subject: [PATCH 309/682] Improve output formatting for update checks in CheckUpdateCommand --- src/StaticPHP/Command/CheckUpdateCommand.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 9a2d5f5cc..4fac0f63b 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -48,12 +48,10 @@ public function handle(): int foreach ($artifacts as $artifact) { $result = $downloader->checkUpdate($artifact, bare: $bare); if (!$result->needUpdate) { - $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); + $this->output->writeln("Artifact {$artifact} is already up to date ({$result->new})"); } else { - $this->output->writeln("Update available for artifact: {$artifact}"); [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; - $this->output->writeln(" Old version: {$old}"); - $this->output->writeln(" New version: {$new}"); + $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); } } return static::OK; From 715f33ac4dc1300b208996fc07fc9cd8f8c391ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:17:41 +0800 Subject: [PATCH 310/682] Add log filtering to prevent sensitive data leakage --- .../Downloader/Type/GitHubTokenSetupTrait.php | 2 ++ src/StaticPHP/Exception/ExceptionHandler.php | 2 +- src/StaticPHP/Runtime/Shell/Shell.php | 26 +++++++++---------- src/bootstrap.php | 2 +- src/globals/functions.php | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index 90c425075..34e350d4d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -16,10 +16,12 @@ public static function getGitHubTokenHeadersStatic(): array // GITHUB_TOKEN support if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); + spc_add_log_filter([$user, $token]); return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")]; } if (($token = getenv('GITHUB_TOKEN')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + spc_add_log_filter($token); return ["Authorization: Bearer {$token}"]; } return []; diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 9dddc9102..053d82a3f 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -115,7 +115,7 @@ private static function logError($message, int $indent_space = 0, bool $output_l $msg = explode("\n", (string) $message); foreach ($msg as $v) { $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); - fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + spc_write_log($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error'); } diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index 2d0d90b8c..f9f4f1759 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -114,22 +114,22 @@ protected function logCommandInfo(string $cmd): void if (!$this->enable_log_file) { return; } - // write executed command to log file using fwrite + // write executed command to log file using spc_write_log $log_file = fopen(SPC_SHELL_LOG, 'a'); - fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); - fwrite($log_file, "> Executing command: {$cmd}\n"); + spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + spc_write_log($log_file, "> Executing command: {$cmd}\n"); // get the backtrace to find the file and line number $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { $file = $backtrace[1]['file']; $line = $backtrace[1]['line']; - fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + spc_write_log($log_file, "> Called from: {$file} at line {$line}\n"); } - fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + spc_write_log($log_file, "> Environment variables: {$this->getEnvString()}\n"); if ($this->cd !== null) { - fwrite($log_file, "> Working dir: {$this->cd}\n"); + spc_write_log($log_file, "> Working dir: {$this->cd}\n"); } - fwrite($log_file, "\n"); + spc_write_log($log_file, "\n"); } /** @@ -154,7 +154,7 @@ protected function passthru( ): array { $file_res = null; if ($this->enable_log_file) { - // write executed command to the log file using fwrite + // write executed command to the log file using spc_write_log $file_res = fopen(SPC_SHELL_LOG, 'a'); } if ($console_output) { @@ -194,10 +194,10 @@ protected function passthru( foreach ([$pipes[1], $pipes[2]] as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; @@ -207,7 +207,7 @@ protected function passthru( // check exit code if ($throw_on_error && $status['exitcode'] !== 0) { if ($file_res !== null) { - fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + spc_write_log($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); } throw new ExecutionException( cmd: $original_command ?? $cmd, @@ -238,10 +238,10 @@ protected function passthru( foreach ($read as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; diff --git a/src/bootstrap.php b/src/bootstrap.php index 95384b719..7856c0b29 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -52,7 +52,7 @@ $log_file_fd = fopen(SPC_OUTPUT_LOG, 'a'); $ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) { if ($log_file_fd) { - fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); + spc_write_log($log_file_fd, strip_ansi_colors($output) . "\n"); } return true; }); diff --git a/src/globals/functions.php b/src/globals/functions.php index 712cf621e..ee279328d 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -132,6 +132,32 @@ function patch_point(): string return ''; } +// Add log filter value(s) to prevent secret leak +function spc_add_log_filter(array|string $filter): void +{ + global $spc_log_filters; + if (!is_array($spc_log_filters)) { + $spc_log_filters = []; + } + if (is_string($filter)) { + if (!in_array($filter, $spc_log_filters, true)) { + $spc_log_filters[] = $filter; + } + } elseif (is_array($filter)) { + $spc_log_filters = array_values(array_unique(array_merge($spc_log_filters, $filter))); + } +} + +function spc_write_log(mixed $stream, string $data): false|int +{ + // get filter + global $spc_log_filters; + if (is_array($spc_log_filters)) { + $data = str_replace($spc_log_filters, '***', $data); + } + return fwrite($stream, $data); +} + function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException { return new InterruptException(message: $msg, code: $retcode); From 5298ee4f971a510908026b518a532d6e6240e16f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:21:44 +0800 Subject: [PATCH 311/682] Use constant back due to config validation problem --- config/downloader.php | 32 ------------------- src/StaticPHP/Artifact/ArtifactDownloader.php | 25 ++++++++++++++- 2 files changed, 24 insertions(+), 33 deletions(-) delete mode 100644 config/downloader.php diff --git a/config/downloader.php b/config/downloader.php deleted file mode 100644 index 2d81a57e3..000000000 --- a/config/downloader.php +++ /dev/null @@ -1,32 +0,0 @@ - */ -return [ - 'bitbuckettag' => BitBucketTag::class, - 'filelist' => FileList::class, - 'git' => Git::class, - 'ghrel' => GitHubRelease::class, - 'ghtar' => GitHubTarball::class, - 'ghtagtar' => GitHubTarball::class, - 'local' => LocalDir::class, - 'pie' => PIE::class, - 'pecl' => PECL::class, - 'url' => Url::class, - 'php-release' => PhpRelease::class, - 'hosted' => HostedPackageBin::class, -]; diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b5e540786..8b64b60ca 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,11 +6,19 @@ use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\BitBucketTag; use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; +use StaticPHP\Artifact\Downloader\Type\FileList; use StaticPHP\Artifact\Downloader\Type\Git; +use StaticPHP\Artifact\Downloader\Type\GitHubRelease; +use StaticPHP\Artifact\Downloader\Type\GitHubTarball; +use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; +use StaticPHP\Artifact\Downloader\Type\PECL; +use StaticPHP\Artifact\Downloader\Type\PhpRelease; +use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; use StaticPHP\Artifact\Downloader\Type\ValidatorInterface; use StaticPHP\DI\ApplicationContext; @@ -31,6 +39,21 @@ */ class ArtifactDownloader { + public const array DOWNLOADERS = [ + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar' => GitHubTarball::class, + 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'pecl' => PECL::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, + ]; + /** @var array> */ protected array $downloaders = []; @@ -198,7 +221,7 @@ public function __construct(protected array $options = []) $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; // load downloaders - $this->downloaders = require ROOT_DIR . '/config/downloader.php'; + $this->downloaders = self::DOWNLOADERS; } /** From abdaaab6e67672864b7d548c267ae1ff8fa7e871 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 11:11:02 +0800 Subject: [PATCH 312/682] Refactor CheckUpdateResult logic to simplify version comparison --- src/Package/Artifact/zig.php | 5 ++++- src/StaticPHP/Artifact/Downloader/Type/FileList.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/Git.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/PECL.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/PIE.php | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 0d334e5f1..b42eee3ae 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -72,6 +72,9 @@ public function checkUpdateBinary(?string $old_version, ArtifactDownloader $down $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); $index_json = json_decode($index_json ?: '', true); $latest_version = null; + if (!is_array($index_json)) { + throw new DownloaderException('Failed to fetch Zig version index for update check'); + } foreach ($index_json as $version => $data) { if ($version !== 'master') { $latest_version = $version; @@ -84,7 +87,7 @@ public function checkUpdateBinary(?string $old_version, ArtifactDownloader $down return new CheckUpdateResult( old: $old_version, new: $latest_version, - needUpdate: $old_version === null || version_compare($latest_version, $old_version, '>'), + needUpdate: $old_version === null || $latest_version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/FileList.php b/src/StaticPHP/Artifact/Downloader/Type/FileList.php index b32645330..b868210de 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/FileList.php +++ b/src/StaticPHP/Artifact/Downloader/Type/FileList.php @@ -32,7 +32,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 1ee9da4da..d5822e697 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -120,7 +120,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } throw new DownloaderException("No matching branch found for regex {$config['regex']}."); diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php index 0b14b05d4..78ceed3a8 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PECL.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -22,7 +22,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index a84cffe51..14996c5af 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -40,7 +40,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A return new CheckUpdateResult( old: $old_version, new: $new_version, - needUpdate: $old_version === null || version_compare($new_version, $old_version, '>'), + needUpdate: $old_version === null || $new_version !== $old_version, ); } From 84f6dab882544dd2f0011229e4196463fcc55025 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 11:11:31 +0800 Subject: [PATCH 313/682] Add parallel update checking and improve artifact update handling --- src/StaticPHP/Artifact/ArtifactCache.php | 10 +++ src/StaticPHP/Artifact/ArtifactDownloader.php | 86 ++++++++++++++++++- .../Downloader/Type/CheckUpdateResult.php | 3 +- src/StaticPHP/Command/CheckUpdateCommand.php | 34 ++++++-- src/globals/defines.php | 2 +- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index cd9ad640d..7626831af 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -282,6 +282,16 @@ public function removeBinary(string $artifact_name, string $platform, bool $dele logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]"); } + /** + * Get the names of all artifacts that have at least one downloaded entry (source or binary). + * + * @return array Artifact names + */ + public function getCachedArtifactNames(): array + { + return array_keys($this->cache); + } + /** * Save cache to file. */ diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 8b64b60ca..a9a259157 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -362,7 +362,8 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, if ($result !== null) { return $result; } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); } $cache = ApplicationContext::get(ArtifactCache::class); if ($prefer_source) { @@ -393,7 +394,33 @@ public function checkUpdate(string $artifact_name, bool $prefer_source = false, 'old_version' => $info['version'], ]); } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); + } + + /** + * Check updates for multiple artifacts, with optional parallel processing. + * + * @param array $artifact_names Artifact names to check + * @param bool $prefer_source Whether to prefer source over binary + * @param bool $bare Check without requiring artifact to be downloaded first + * @param null|callable $onResult Called immediately with (string $name, CheckUpdateResult) as each result arrives + * @return array Results keyed by artifact name + */ + public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null): array + { + if ($this->parallel > 1 && count($artifact_names) > 1) { + return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult); + } + $results = []; + foreach ($artifact_names as $name) { + $result = $this->checkUpdate($name, $prefer_source, $bare); + $results[$name] = $result; + if ($onResult !== null) { + ($onResult)($name, $result); + } + } + return $results; } public function getRetry(): int @@ -411,6 +438,61 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } + private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult): array + { + $results = []; + $fiber_pool = []; + $remaining = $artifact_names; + + Shell::passthruCallback(function () { + \Fiber::suspend(); + }); + + try { + while (!empty($remaining) || !empty($fiber_pool)) { + // fill pool + while (count($fiber_pool) < $this->parallel && !empty($remaining)) { + $name = array_shift($remaining); + $fiber = new \Fiber(function () use ($name, $prefer_source, $bare) { + return [$name, $this->checkUpdate($name, $prefer_source, $bare)]; + }); + $fiber->start(); + $fiber_pool[$name] = $fiber; + } + // check pool + foreach ($fiber_pool as $fiber_name => $fiber) { + if ($fiber->isTerminated()) { + // getReturn() re-throws if the fiber threw — propagates immediately + [$artifact_name, $result] = $fiber->getReturn(); + $results[$artifact_name] = $result; + if ($onResult !== null) { + ($onResult)($artifact_name, $result); + } + unset($fiber_pool[$fiber_name]); + } else { + $fiber->resume(); + } + } + } + } catch (\Throwable $e) { + // terminate all still-suspended fibers so their curl processes don't hang + foreach ($fiber_pool as $fiber) { + if (!$fiber->isTerminated()) { + try { + $fiber->throw($e); + } catch (\Throwable) { + // ignore — we only care about stopping them + } + } + } + throw $e; + } finally { + Shell::passthruCallback(null); + } + + return $results; + } + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult { if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php index 468b643b0..7e46e4ad7 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php @@ -8,7 +8,8 @@ { public function __construct( public ?string $old, - public string $new, + public ?string $new, public bool $needUpdate, + public bool $unsupported = false, ) {} } diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 4fac0f63b..1663337c6 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -4,7 +4,10 @@ namespace StaticPHP\Command; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactDownloader; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -17,9 +20,10 @@ class CheckUpdateCommand extends BaseCommand public function configure(): void { - $this->addArgument('artifact', InputArgument::REQUIRED, 'The name of the artifact(s) to check for updates, comma-separated'); + $this->addArgument('artifact', InputArgument::OPTIONAL, 'The name of the artifact(s) to check for updates, comma-separated (default: all downloaded artifacts)'); $this->addOption('json', null, null, 'Output result in JSON format'); $this->addOption('bare', null, null, 'Check update without requiring the artifact to be downloaded first (old version will be null)'); + $this->addOption('parallel', 'p', InputOption::VALUE_REQUIRED, 'Number of parallel update checks (default: 10)', 10); // --with-php option for checking updates with a specific PHP version context $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'); @@ -27,17 +31,27 @@ public function configure(): void public function handle(): int { - $artifacts = parse_comma_list($this->input->getArgument('artifact')); + $artifact_arg = $this->input->getArgument('artifact'); + if ($artifact_arg === null) { + $artifacts = ApplicationContext::get(ArtifactCache::class)->getCachedArtifactNames(); + if (empty($artifacts)) { + $this->output->writeln('No downloaded artifacts found.'); + return static::OK; + } + } else { + $artifacts = parse_comma_list($artifact_arg); + } try { $downloader = new ArtifactDownloader($this->input->getOptions()); $bare = (bool) $this->getOption('bare'); if ($this->getOption('json')) { + $results = $downloader->checkUpdates($artifacts, bare: $bare); $outputs = []; - foreach ($artifacts as $artifact) { - $result = $downloader->checkUpdate($artifact, bare: $bare); + foreach ($results as $artifact => $result) { $outputs[$artifact] = [ 'need-update' => $result->needUpdate, + 'unsupported' => $result->unsupported, 'old' => $result->old, 'new' => $result->new, ]; @@ -45,15 +59,17 @@ public function handle(): int $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); return static::OK; } - foreach ($artifacts as $artifact) { - $result = $downloader->checkUpdate($artifact, bare: $bare); - if (!$result->needUpdate) { - $this->output->writeln("Artifact {$artifact} is already up to date ({$result->new})"); + $downloader->checkUpdates($artifacts, bare: $bare, onResult: function (string $artifact, CheckUpdateResult $result) { + if ($result->unsupported) { + $this->output->writeln("Artifact {$artifact} does not support update checking, skipped"); + } elseif (!$result->needUpdate) { + $ver = $result->new ? "({$result->new})" : ''; + $this->output->writeln("Artifact {$artifact} is already up to date {$ver}"); } else { [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); } - } + }); return static::OK; } catch (SPCException $e) { $e->setSimpleOutput(); diff --git a/src/globals/defines.php b/src/globals/defines.php index dbcb63f22..38490046e 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -104,7 +104,7 @@ 'local' => 'local dir', 'pie' => 'PHP Installer for Extensions (PIE)', 'url' => 'url', - 'php-release' => 'php.net', + 'php-release' => 'PHP website release', 'custom' => 'custom downloader', ]; From 055bc7bc3cad7139f156633e9d5b9557fa09911a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 13:46:55 +0800 Subject: [PATCH 314/682] Add condition for ffi patch --- src/SPC/builder/extension/ffi.php | 10 ++++++++++ src/SPC/store/SourcePatcher.php | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/ffi.php b/src/SPC/builder/extension/ffi.php index 985477232..8e192beab 100644 --- a/src/SPC/builder/extension/ffi.php +++ b/src/SPC/builder/extension/ffi.php @@ -5,11 +5,21 @@ namespace SPC\builder\extension; use SPC\builder\Extension; +use SPC\builder\linux\SystemUtil; +use SPC\store\SourcePatcher; use SPC\util\CustomExt; #[CustomExt('ffi')] class ffi extends Extension { + public function patchBeforeBuildconf(): bool + { + if (PHP_OS_FAMILY === 'Linux' && SystemUtil::getOSRelease()['dist'] === 'centos') { + return SourcePatcher::patchFfiCentos7FixO3strncmp(); + } + return false; + } + public function getUnixConfigureArg(bool $shared = false): string { return '--with-ffi' . ($shared ? '=shared' : '') . ' --enable-zend-signals'; diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index 0068f53e3..233a2e157 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -22,7 +22,7 @@ public static function init(): void FileSystem::addSourceExtractHook('swoole', [__CLASS__, 'patchSwoole']); FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchPhpLibxml212']); FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchGDWin32']); - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); + // FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']); From 8c4e3d58a38c14c6e9504f81cc2bc378a0cc597b Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 6 Mar 2026 15:25:38 +0900 Subject: [PATCH 315/682] Add php-src mirror and use gmp mirror site (#1048) --- config/source.json | 2 +- src/SPC/store/source/PhpSource.php | 43 +++++++++++++++++++----------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/config/source.json b/config/source.json index 114118bb4..040197cb1 100644 --- a/config/source.json +++ b/config/source.json @@ -361,7 +361,7 @@ }, "gmp": { "type": "filelist", - "url": "https://gmplib.org/download/gmp/", + "url": "https://ftp.gnu.org/gnu/gmp/", "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/", "provide-pre-built": true, "alt": { diff --git a/src/SPC/store/source/PhpSource.php b/src/SPC/store/source/PhpSource.php index 27e8bb891..02ba87554 100644 --- a/src/SPC/store/source/PhpSource.php +++ b/src/SPC/store/source/PhpSource.php @@ -6,11 +6,17 @@ use JetBrains\PhpStorm\ArrayShape; use SPC\exception\DownloaderException; +use SPC\exception\SPCException; use SPC\store\Downloader; class PhpSource extends CustomSourceBase { - public const NAME = 'php-src'; + public const string NAME = 'php-src'; + + public const array WEB_PHP_DOMAINS = [ + 'https://www.php.net', + 'https://phpmirror.static-php.dev', + ]; public function fetch(bool $force = false, ?array $config = null, int $lock_as = SPC_DOWNLOAD_SOURCE): void { @@ -28,21 +34,26 @@ public function fetch(bool $force = false, ?array $config = null, int $lock_as = #[ArrayShape(['type' => 'string', 'path' => 'string', 'rev' => 'string', 'url' => 'string'])] public function getLatestPHPInfo(string $major_version): array { - // 查找最新的小版本号 - $info = json_decode(Downloader::curlExec( - url: "https://www.php.net/releases/index.php?json&version={$major_version}", - retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 - ), true); - if (!isset($info['version'])) { - throw new DownloaderException("Version {$major_version} not found."); + foreach (self::WEB_PHP_DOMAINS as $domain) { + try { + $info = json_decode(Downloader::curlExec( + url: "{$domain}/releases/index.php?json&version={$major_version}", + retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 + ), true); + if (!isset($info['version'])) { + throw new DownloaderException("Version {$major_version} not found."); + } + $version = $info['version']; + return [ + 'type' => 'url', + 'url' => "{$domain}/distributions/php-{$version}.tar.xz", + ]; + } catch (SPCException) { + logger()->warning('Failed to fetch latest PHP version for major version {$major_version} from {$domain}, trying next mirror if available.'); + continue; + } } - - $version = $info['version']; - - // 从官网直接下载 - return [ - 'type' => 'url', - 'url' => "https://www.php.net/distributions/php-{$version}.tar.xz", - ]; + // exception if all mirrors failed + throw new DownloaderException("Failed to fetch latest PHP version for major version {$major_version} from all tried mirrors."); } } From 32b7fee8d85555f15eb98f0ce7ee70532cbf5956 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 16:32:44 +0800 Subject: [PATCH 316/682] Fix version extraction to fallback on repository name if tag name is absent --- src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index a9283722f..61517a9e0 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -42,12 +42,12 @@ public function getGitHubTarballInfo(string $name, string $repo, string $rel_typ } if ($match_url === null) { $url = $rel['tarball_url'] ?? null; - $version = $rel['tag_name'] ?? null; + $version = $rel['tag_name'] ?? $rel['name'] ?? null; break; } if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) { $url = $rel['tarball_url']; - $version = $rel['tag_name'] ?? null; + $version = $rel['tag_name'] ?? $rel['name'] ?? null; break; } } From 368ce75261b44687c36b090370ccbe78b85bc1b9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:20:16 +0800 Subject: [PATCH 317/682] Fix configuration retrieval by using the extension's name instead of a formatted version --- src/StaticPHP/Package/PhpExtensionPackage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 07bc6abd8..06b622093 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -80,7 +80,7 @@ public function getPhpConfigureArg(string $os, bool $shared): string } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; $name = str_replace('_', '-', $this->getExtensionName()); - $ext_config = PackageConfig::get($name, 'php-extension', []); + $ext_config = PackageConfig::get($this->getName(), 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { 'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable', From d0b6a02432cb15e2156f27304f6878779283c7f2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:20:55 +0800 Subject: [PATCH 318/682] Add patchBeforeBuild method to modify Makefile CFLAGS and enhance build process --- src/Package/Library/bzip2.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php index 7f554ab48..403773dab 100644 --- a/src/Package/Library/bzip2.php +++ b/src/Package/Library/bzip2.php @@ -6,19 +6,30 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; +use StaticPHP\Util\FileSystem; #[Library('bzip2')] class bzip2 { + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib, PackageBuilder $builder): void { - shell()->cd($lib->getSourceDir())->initializeEnv($lib) - ->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") - ->exec("make -j{$builder->concurrency} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") + $shell = shell()->cd($lib->getSourceDir())->initializeEnv($lib); + $env = $shell->getEnvString(); + $cc_env = 'CC=' . escapeshellarg(getenv('CC') ?: 'cc') . ' AR=' . escapeshellarg(getenv('AR') ?: 'ar'); + + $shell->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") + ->exec("make -j{$builder->concurrency} {$cc_env} {$env} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") ->exec('cp libbz2.a ' . $lib->getLibDir()) ->exec('cp bzlib.h ' . $lib->getIncludeDir()); } From 5e84fed19a8d05c27a05d3874553cc9ce1085236 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:22:59 +0800 Subject: [PATCH 319/682] Add ext-brotli --- config/pkg/ext/ext-brotli.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 config/pkg/ext/ext-brotli.yml diff --git a/config/pkg/ext/ext-brotli.yml b/config/pkg/ext/ext-brotli.yml new file mode 100644 index 000000000..147ecb636 --- /dev/null +++ b/config/pkg/ext/ext-brotli.yml @@ -0,0 +1,13 @@ +ext-brotli: + type: php-extension + artifact: + source: + type: git + extract: php-src/ext/brotli + rev: master + url: 'https://github.com/kjdev/php-ext-brotli' + metadata: + license-files: [LICENSE] + license: MIT + depends: + - brotli From 58c02dfab376af7c8949a2ca8b52730cf6c788d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:23:08 +0800 Subject: [PATCH 320/682] Add ext-bz2 --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 0149fe974..6a4474445 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -1,5 +1,12 @@ ext-bcmath: type: php-extension +ext-bz2: + type: php-extension + depends: + - bzip2 + php-extension: + arg-type@unix: with-path + arg-type@windows: with ext-mbregex: type: php-extension depends: From fc807ec7c99cff0b2b856a409a8c0dc6cc7fcfb7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:24:48 +0800 Subject: [PATCH 321/682] Add ext-calendar --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 6a4474445..7f60fa3d8 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -7,6 +7,8 @@ ext-bz2: php-extension: arg-type@unix: with-path arg-type@windows: with +ext-calendar: + type: php-extension ext-mbregex: type: php-extension depends: From fbbed6d5c1be308615aecf563a4d56048ce564d2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:27:22 +0800 Subject: [PATCH 322/682] Add ext-ctype --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 7f60fa3d8..8b5a926d0 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -9,6 +9,8 @@ ext-bz2: arg-type@windows: with ext-calendar: type: php-extension +ext-ctype: + type: php-extension ext-mbregex: type: php-extension depends: From 780232fa608a5ef9ed9b6190848d52f94db97149 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 7 Mar 2026 00:42:34 +0800 Subject: [PATCH 323/682] Enhance dependency resolution for virtual-target packages --- src/StaticPHP/Util/DependencyResolver.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index 2db74abd5..9833ae107 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -45,6 +45,27 @@ public static function resolve(array $packages, array $dependency_overrides = [] } } + // Virtual-target packages (e.g. php-fpm) are built as part of their real parent's + // build step, so any dependency they declare must be available before the real parent + // is built. Promote those deps directly onto the real parent's dependency list so + // that the topological sort places them before the parent. + foreach ($dep_list_clean as $pkg_name => $pkg_item) { + if (PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { + continue; + } + foreach ($pkg_item['depends'] as $dep_name) { + if (isset($dep_list_clean[$dep_name]) && PackageConfig::get($dep_name, 'type') !== 'virtual-target') { + // $dep_name is the real parent; add all other deps of this virtual-target to it + $other_deps = array_values(array_filter($pkg_item['depends'], fn ($d) => $d !== $dep_name)); + if (!empty($other_deps)) { + $dep_list_clean[$dep_name]['depends'] = array_values(array_unique( + array_merge($dep_list_clean[$dep_name]['depends'], $other_deps) + )); + } + } + } + } + $resolved = self::doVisitPlat($packages, $dep_list_clean); // Build reverse dependency map if $why is requested From 07fd1bcd033b58adcf23891bcdc45439528078f7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 7 Mar 2026 00:42:49 +0800 Subject: [PATCH 324/682] Patch extension config.m4 files to use PKG_CHECK_MODULES_STATIC --- src/Package/Target/php/unix.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 888ae8bda..d485e2c19 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -47,6 +47,8 @@ public function patchBeforeBuildconf(TargetPackage $package): void // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) + shell()->cd($package->getSourceDir())->exec("grep -rl 'PKG_CHECK_MODULES(' ext/ | xargs sed -i 's/PKG_CHECK_MODULES(/PKG_CHECK_MODULES_STATIC(/g' || true"); } #[Stage] From 0548aba24880a901692242c27b0bdedce9258275 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 7 Mar 2026 21:20:34 +0800 Subject: [PATCH 325/682] Add ext-curl --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ src/Package/Library/curl.php | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 8b5a926d0..074f6ef92 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -11,6 +11,15 @@ ext-calendar: type: php-extension ext-ctype: type: php-extension +ext-curl: + type: php-extension + depends: + - curl + depends@windows: + - ext-zlib + - ext-openssl + php-extension: + arg-type: with ext-mbregex: type: php-extension depends: diff --git a/src/Package/Library/curl.php b/src/Package/Library/curl.php index 283c765a1..0edca93fd 100644 --- a/src/Package/Library/curl.php +++ b/src/Package/Library/curl.php @@ -55,6 +55,12 @@ public function build(LibraryPackage $lib): void // patch pkgconf $lib->patchPkgconfPrefix(['libcurl.pc']); + // curl's CMake embeds krb5 link flags directly without following Requires.private chain, + // so -lkrb5support (from mit-krb5.pc Libs.private) is missing from libcurl.pc. + $pc_path = "{$lib->getLibDir()}/pkgconfig/libcurl.pc"; + if (str_contains(FileSystem::readFile($pc_path), '-lgssapi_krb5')) { + FileSystem::replaceFileRegex($pc_path, '/-lcom_err$/m', '-lcom_err -lkrb5support'); + } shell()->cd("{$lib->getLibDir()}/cmake/CURL/") ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); } From b0b322071696daa0e3bd8062c7162b88469bd495 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:00:15 +0800 Subject: [PATCH 326/682] Fix zlib configure arg --- src/Package/Extension/zlib.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Package/Extension/zlib.php diff --git a/src/Package/Extension/zlib.php b/src/Package/Extension/zlib.php new file mode 100644 index 000000000..14ab656d7 --- /dev/null +++ b/src/Package/Extension/zlib.php @@ -0,0 +1,22 @@ += 80400 ? '' : ' --with-zlib-dir=' . $builder->getBuildRootPath(); + return '--with-zlib' . $zlib_dir; + } +} From 88af4a719f20f690f3638db39e2d81fe468aeacf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:00:33 +0800 Subject: [PATCH 327/682] Prefer cache extract path in getSourceDir method --- src/StaticPHP/Artifact/Artifact.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 841775e36..0b5d8a6de 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -292,8 +292,11 @@ public function getDownloadConfig(string $type): mixed */ public function getSourceDir(): string { - // defined in config - $extract = $this->config['source']['extract'] ?? null; + // Prefer cache extract path, fall back to config + $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + $extract = is_string($cache_info['extract'] ?? null) + ? $cache_info['extract'] + : ($this->config['source']['extract'] ?? null); if ($extract === null) { return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); From 19d6d669c07676dadeb5a26d29564927f2c801df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:01:29 +0800 Subject: [PATCH 328/682] Move arg-type def in config --- config/pkg/ext/ext-glfw.yml | 2 ++ src/Package/Extension/glfw.php | 8 -------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/config/pkg/ext/ext-glfw.yml b/config/pkg/ext/ext-glfw.yml index dc8844a8f..1be1e75f5 100644 --- a/config/pkg/ext/ext-glfw.yml +++ b/config/pkg/ext/ext-glfw.yml @@ -3,3 +3,5 @@ ext-glfw: artifact: glfw depends: - glfw + php-extension: + arg-type@unix: '--enable-glfw --with-glfw-dir=@build_root_path@' diff --git a/src/Package/Extension/glfw.php b/src/Package/Extension/glfw.php index 61d722e14..2a9c7ee51 100644 --- a/src/Package/Extension/glfw.php +++ b/src/Package/Extension/glfw.php @@ -6,7 +6,6 @@ use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; -use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PhpExtensionPackage; @@ -49,11 +48,4 @@ public function patchBeforeConfigure(): void putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=' . $extra_ldflags); } } - - #[CustomPhpConfigureArg('Darwin')] - #[CustomPhpConfigureArg('Linux')] - public function getUnixConfigureArg(bool $shared = false): string - { - return '--enable-glfw --with-glfw-dir=' . BUILD_ROOT_PATH; - } } From 2676875ccd573a298b8a161059c7000f816e498b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:01:44 +0800 Subject: [PATCH 329/682] Refactor PKG_CHECK_MODULES replacement and reuse make vars for configure --- src/Package/Target/php/unix.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index d485e2c19..d8236ebfa 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -48,7 +48,9 @@ public function patchBeforeBuildconf(TargetPackage $package): void // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) - shell()->cd($package->getSourceDir())->exec("grep -rl 'PKG_CHECK_MODULES(' ext/ | xargs sed -i 's/PKG_CHECK_MODULES(/PKG_CHECK_MODULES_STATIC(/g' || true"); + foreach (glob("{$package->getSourceDir()}/ext/*/*.m4") as $m4file) { + FileSystem::replaceFileStr($m4file, 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + } } #[Stage] @@ -110,11 +112,15 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $static_extension_str = $this->makeStaticExtensionString($installer); + // reuse the same make vars so configure conftest links use the same LIBS (incl. -framework flags) + $vars = $this->makeVars($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'), + 'LIBS' => $vars['EXTRA_LIBS'] ?? '', ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } From 0c86d82b98846ed5ff235562fc11bbeb3440aac3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:02:04 +0800 Subject: [PATCH 330/682] Update getDistName method to use display-name from config --- src/StaticPHP/Package/PhpExtensionPackage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 06b622093..bae117489 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -148,11 +148,11 @@ public function buildShared(): void /** * Get the dist name used for `--ri` check in smoke test. - * Reads from config `dist-name` field, defaults to extension name. + * Reads from config `display-name` field, defaults to extension name. */ public function getDistName(): string { - return $this->extension_config['dist-name'] ?? $this->getExtensionName(); + return $this->extension_config['display-name'] ?? $this->getExtensionName(); } /** @@ -166,7 +166,7 @@ public function runSmokeTestCliUnix(): void } $distName = $this->getDistName(); - // empty dist-name → no --ri check (e.g. password_argon2) + // empty display-name → no --ri check (e.g. password_argon2) if ($distName === '') { return; } From 8f10e0d0704cc638e54160c41602d62c49a3dfdd Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:02:15 +0800 Subject: [PATCH 331/682] Add before and after build hooks for phar extension to replace file strings --- src/Package/Extension/phar.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Package/Extension/phar.php b/src/Package/Extension/phar.php index dd9b1dff9..2220f7278 100644 --- a/src/Package/Extension/phar.php +++ b/src/Package/Extension/phar.php @@ -9,6 +9,8 @@ use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; #[Extension('phar')] @@ -26,4 +28,24 @@ public function afterMicroUnixBuild(): void { SourcePatcher::unpatchMicroPhar(); } + + #[BeforeStage('ext-phar', 'build')] + public function beforeBuildShared(PhpExtensionPackage $pkg): void + { + FileSystem::replaceFileStr( + "{$pkg->getSourceDir()}/config.m4", + ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'], + ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'] + ); + } + + #[AfterStage('ext-phar', 'build')] + public function afterBuildShared(PhpExtensionPackage $pkg): void + { + FileSystem::replaceFileStr( + "{$pkg->getSourceDir()}/config.m4", + ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'], + ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'] + ); + } } From ad0118718f9ce3629e4b5fefb98887b5e9d21ce7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:02:22 +0800 Subject: [PATCH 332/682] Update arg-type definition in builtin-extensions.yml to include specific options --- config/pkg/ext/builtin-extensions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 074f6ef92..52f149247 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -56,7 +56,7 @@ ext-readline: support: Windows: wip BSD: wip - arg-type: with-path + arg-type: '--with-libedit --without-readline' build-shared: false build-static: true ext-zlib: From a9e6e4a22630024323a8c4bb163e5e3499cbf61a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 16:32:31 +0800 Subject: [PATCH 333/682] Add dba --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ src/Package/Extension/dba.php | 28 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/Package/Extension/dba.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 52f149247..b71e44aca 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -20,6 +20,12 @@ ext-curl: - ext-openssl php-extension: arg-type: with +ext-dba: + type: php-extension + suggests: + - qdbm + php-extension: + arg-type: custom ext-mbregex: type: php-extension depends: diff --git a/src/Package/Extension/dba.php b/src/Package/Extension/dba.php new file mode 100644 index 000000000..d16d979d1 --- /dev/null +++ b/src/Package/Extension/dba.php @@ -0,0 +1,28 @@ +getLibraryPackage('qdbm')) ? (" --with-qdbm={$qdbm->getBuildRootPath()}") : ''; + return '--enable-dba' . ($shared ? '=shared' : '') . $qdbm; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(PackageInstaller $installer): string + { + $qdbm = $installer->getLibraryPackage('qdbm') ? ' --with-qdbm' : ''; + return '--with-dba' . $qdbm; + } +} From 6d2c43d3e536dd3956ef47b9087ff71a80e2c4bc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 16:43:53 +0800 Subject: [PATCH 334/682] Add license metadata for ast extension --- config/pkg/ext/ext-ast.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml index 0684959dd..776b82dff 100644 --- a/config/pkg/ext/ext-ast.yml +++ b/config/pkg/ext/ext-ast.yml @@ -4,3 +4,6 @@ ext-ast: source: type: pecl name: ast + metadata: + license-files: [LICENSE] + license: BSD-3-Clause From 247a254af4aed1600448bdc49a7fc4697473a6e3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 17:46:14 +0800 Subject: [PATCH 335/682] Add ext-dio --- config/pkg/ext/ext-dio.yml | 9 +++++++++ src/Package/Extension/dio.php | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 config/pkg/ext/ext-dio.yml create mode 100644 src/Package/Extension/dio.php diff --git a/config/pkg/ext/ext-dio.yml b/config/pkg/ext/ext-dio.yml new file mode 100644 index 000000000..b445940c3 --- /dev/null +++ b/config/pkg/ext/ext-dio.yml @@ -0,0 +1,9 @@ +ext-dio: + type: php-extension + artifact: + source: + type: pecl + name: dio + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/src/Package/Extension/dio.php b/src/Package/Extension/dio.php new file mode 100644 index 000000000..70ac387e3 --- /dev/null +++ b/src/Package/Extension/dio.php @@ -0,0 +1,23 @@ +getSourceDir()}/php_dio.h")) { + FileSystem::writeFile("{$this->getSourceDir()}/php_dio.h", FileSystem::readFile("{$this->getSourceDir()}/src/php_dio.h")); + } + } +} From b90356bc1d2311a75e0c79304e03ea8630150ff4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 17:47:09 +0800 Subject: [PATCH 336/682] Enhancement for bin/spc dev:info command --- src/StaticPHP/Artifact/ArtifactCache.php | 11 + .../Command/Dev/PackageInfoCommand.php | 246 +++++++++++++++++- src/StaticPHP/Registry/PackageLoader.php | 158 +++++++++++ 3 files changed, 404 insertions(+), 11 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index 7626831af..5a2c8bac7 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -203,6 +203,17 @@ public function getBinaryInfo(string $artifact_name, string $platform): ?array return $this->cache[$artifact_name]['binary'][$platform] ?? null; } + /** + * Get all binary cache entries for an artifact, keyed by platform string. + * + * @param string $artifact_name Artifact name + * @return array Map of platform → cache info (may be empty) + */ + public function getAllBinaryInfo(string $artifact_name): array + { + return $this->cache[$artifact_name]['binary'] ?? []; + } + /** * Get the full path to the cached file/directory. * diff --git a/src/StaticPHP/Command/Dev/PackageInfoCommand.php b/src/StaticPHP/Command/Dev/PackageInfoCommand.php index 7c8691993..ba621c5e9 100644 --- a/src/StaticPHP/Command/Dev/PackageInfoCommand.php +++ b/src/StaticPHP/Command/Dev/PackageInfoCommand.php @@ -4,10 +4,14 @@ namespace StaticPHP\Command\Dev; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Command\BaseCommand; use StaticPHP\Config\ArtifactConfig; use StaticPHP\Config\PackageConfig; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Registry\Registry; +use StaticPHP\Runtime\SystemTarget; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -34,18 +38,26 @@ public function handle(): int } $pkgConfig = PackageConfig::get($packageName); - $artifactConfig = ArtifactConfig::get($packageName); + // Resolve the actual artifact name: + // - string field → named reference (e.g. php → php-src) + // - array field → inline artifact, key is package name + // - null → no artifact, or may match by package name + $artifactField = $pkgConfig['artifact'] ?? null; + $artifactName = is_string($artifactField) ? $artifactField : $packageName; + $artifactConfig = ArtifactConfig::get($artifactName); $pkgInfo = Registry::getPackageConfigInfo($packageName); - $artifactInfo = Registry::getArtifactConfigInfo($packageName); + $artifactInfo = Registry::getArtifactConfigInfo($artifactName); + $annotationInfo = PackageLoader::getPackageAnnotationInfo($packageName); + $cacheInfo = $this->resolveCacheInfo($artifactName, $artifactConfig); if ($this->getOption('json')) { - return $this->outputJson($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + return $this->outputJson($packageName, $artifactName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo, $annotationInfo, $cacheInfo); } - return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo, $annotationInfo, $cacheInfo); } - private function outputJson(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + private function outputJson(string $name, string $artifactName, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo, ?array $annotationInfo, ?array $cacheInfo): int { $data = [ 'name' => $name, @@ -55,15 +67,24 @@ private function outputJson(string $name, array $pkgConfig, ?array $artifactConf ]; if ($artifactConfig !== null) { + $data['artifact_name'] = $artifactName !== $name ? $artifactName : null; $data['artifact_config_file'] = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : null; $data['artifact'] = $this->splitArtifactConfig($artifactConfig); } + if ($annotationInfo !== null) { + $data['annotations'] = $annotationInfo; + } + + if ($cacheInfo !== null) { + $data['cache'] = $cacheInfo; + } + $this->output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); return static::SUCCESS; } - private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo, ?array $annotationInfo, ?array $cacheInfo): int { $type = $pkgConfig['type'] ?? 'unknown'; $registry = $pkgInfo['registry'] ?? 'unknown'; @@ -86,12 +107,15 @@ private function outputTerminal(string $name, array $pkgConfig, ?array $artifact // Artifact config if ($artifactConfig !== null) { $artifactFile = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : 'unknown'; - $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); - - // Check if artifact config is inline (embedded in pkg config) or separate - $inlineArtifact = $pkgConfig['artifact'] ?? null; - if (is_array($inlineArtifact)) { + $artifactField = $pkgConfig['artifact'] ?? null; + if (is_string($artifactField)) { + // Named reference: show the artifact name it points to + $this->output->writeln("── Artifact Config ── artifact: {$artifactField} file: {$artifactFile}"); + } elseif (is_array($artifactField)) { + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); $this->output->writeln(' (inline in package config)'); + } else { + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); } $split = $this->splitArtifactConfig($artifactConfig); @@ -107,9 +131,122 @@ private function outputTerminal(string $name, array $pkgConfig, ?array $artifact $this->output->writeln(''); } + // Annotation section + $this->outputAnnotationSection($name, $annotationInfo); + + // Cache status section + $this->outputCacheSection($cacheInfo); + return static::SUCCESS; } + private function outputAnnotationSection(string $packageName, ?array $annotationInfo): void + { + if ($annotationInfo === null) { + $this->output->writeln('── Annotations ── (no annotation class registered)'); + $this->output->writeln(''); + return; + } + + $shortClass = $this->classBaseName($annotationInfo['class']); + $this->output->writeln("── Annotations ── class: {$shortClass}"); + $this->output->writeln(" {$annotationInfo['class']}"); + + // Method-level hooks + $methods = $annotationInfo['methods']; + if (!empty($methods)) { + $this->output->writeln(''); + $this->output->writeln(' Method hooks:'); + foreach ($methods as $methodName => $attrs) { + $attrList = implode(' ', array_map(fn ($a) => $this->formatAttr($a), $attrs)); + $this->output->writeln(" {$methodName}() {$attrList}"); + } + } + + // Before-stage hooks targeting this package (inbound) + $beforeStages = $annotationInfo['before_stages']; + if (!empty($beforeStages)) { + $this->output->writeln(''); + $this->output->writeln(' Before-stage hooks (inbound):'); + foreach ($beforeStages as $stage => $hooks) { + foreach ($hooks as $hook) { + $source = $this->classBaseName($hook['class']) . '::' . $hook['method'] . '()'; + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" {$stage} ← {$source}{$cond}"); + } + } + } + + // After-stage hooks targeting this package (inbound) + $afterStages = $annotationInfo['after_stages']; + if (!empty($afterStages)) { + $this->output->writeln(''); + $this->output->writeln(' After-stage hooks (inbound):'); + foreach ($afterStages as $stage => $hooks) { + foreach ($hooks as $hook) { + $source = $this->classBaseName($hook['class']) . '::' . $hook['method'] . '()'; + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" {$stage} ← {$source}{$cond}"); + } + } + } + + // Outbound hooks: stages this package's class registers on other packages (exclude self-hooks) + $outboundBefore = $annotationInfo['outbound_before_stages'] ?? []; + $outboundAfter = $annotationInfo['outbound_after_stages'] ?? []; + // Filter out entries targeting the same package — those are already shown inbound + $outboundBefore = array_filter($outboundBefore, fn ($pkg) => $pkg !== $packageName, ARRAY_FILTER_USE_KEY); + $outboundAfter = array_filter($outboundAfter, fn ($pkg) => $pkg !== $packageName, ARRAY_FILTER_USE_KEY); + if (!empty($outboundBefore) || !empty($outboundAfter)) { + $this->output->writeln(''); + $this->output->writeln(' Hooks on other packages (outbound):'); + foreach ($outboundBefore as $targetPkg => $stages) { + foreach ($stages as $stage => $hooks) { + foreach ($hooks as $hook) { + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" #[BeforeStage] → {$targetPkg} {$stage} {$hook['method']}(){$cond}"); + } + } + } + foreach ($outboundAfter as $targetPkg => $stages) { + foreach ($stages as $stage => $hooks) { + foreach ($hooks as $hook) { + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" #[AfterStage] → {$targetPkg} {$stage} {$hook['method']}(){$cond}"); + } + } + } + } + + $this->output->writeln(''); + } + + /** + * Format a single attribute entry (from annotation_map) as a colored inline string. + * + * @param array{attr: string, args: array} $attr + */ + private function formatAttr(array $attr): string + { + $name = $attr['attr']; + $args = $attr['args']; + if (empty($args)) { + return "#[{$name}]"; + } + $argStr = implode(', ', array_map( + fn ($v) => is_string($v) ? "'{$v}'" : (string) $v, + array_values($args) + )); + return "#[{$name}({$argStr})]"; + } + + /** Return the trailing class name component without the namespace. */ + private function classBaseName(string $fqcn): string + { + $parts = explode('\\', $fqcn); + return end($parts); + } + /** * Split artifact config into logical sections for cleaner display. * @@ -190,4 +327,91 @@ private function toRelativePath(string $absolutePath): string } return $normalized; } + + /** + * Build cache status data for display/JSON. + * Returns null when there is no artifact config for this package. + */ + private function resolveCacheInfo(string $name, ?array $artifactConfig): ?array + { + if ($artifactConfig === null) { + return null; + } + $cache = ApplicationContext::get(ArtifactCache::class); + $currentPlatform = SystemTarget::getCurrentPlatformString(); + $hasSource = array_key_exists('source', $artifactConfig) || array_key_exists('source-mirror', $artifactConfig); + $hasBinary = array_key_exists('binary', $artifactConfig) || array_key_exists('binary-mirror', $artifactConfig); + return [ + 'current_platform' => $currentPlatform, + 'has_source' => $hasSource, + 'has_binary' => $hasBinary, + 'source' => $hasSource ? [ + 'downloaded' => $cache->isSourceDownloaded($name), + 'info' => $cache->getSourceInfo($name), + ] : null, + 'binary' => $hasBinary ? $cache->getAllBinaryInfo($name) : null, + ]; + } + + private function outputCacheSection(?array $cacheInfo): void + { + if ($cacheInfo === null) { + $this->output->writeln('── Cache Status ── (no artifact config)'); + $this->output->writeln(''); + return; + } + + $platform = $cacheInfo['current_platform']; + $this->output->writeln("── Cache Status ── current platform: {$platform}"); + + // Source + $this->output->writeln(''); + $this->output->writeln(' source:'); + if (!$cacheInfo['has_source']) { + $this->output->writeln(' ─ not applicable'); + } elseif ($cacheInfo['source']['downloaded'] && $cacheInfo['source']['info'] !== null) { + $this->output->writeln(' ✓ downloaded ' . $this->formatCacheEntry($cacheInfo['source']['info'])); + } else { + $this->output->writeln(' ✗ not downloaded'); + } + + // Binary + $this->output->writeln(''); + $this->output->writeln(' binary:'); + if (!$cacheInfo['has_binary']) { + $this->output->writeln(' ─ not applicable'); + } elseif (empty($cacheInfo['binary'])) { + $this->output->writeln(" ✗ {$platform} (current — not cached)"); + } else { + $allBinary = $cacheInfo['binary']; + foreach ($allBinary as $binPlatform => $binInfo) { + $isCurrent = $binPlatform === $platform; + $tag = $isCurrent ? ' (current)' : ''; + if ($binInfo !== null) { + $this->output->writeln(" ✓ {$binPlatform}{$tag} " . $this->formatCacheEntry($binInfo)); + } else { + $this->output->writeln(" ✗ {$binPlatform}{$tag}"); + } + } + // Show current platform if not already listed + if (!array_key_exists($platform, $allBinary)) { + $this->output->writeln(" ✗ {$platform} (current — not cached)"); + } + } + + $this->output->writeln(''); + } + + private function formatCacheEntry(array $info): string + { + $type = $info['cache_type'] ?? '?'; + $version = $info['version'] !== null ? " {$info['version']}" : ''; + $time = isset($info['time']) ? ' ' . date('Y-m-d H:i', (int) $info['time']) : ''; + $file = match ($type) { + 'archive', 'file' => isset($info['filename']) ? " {$info['filename']}" : '', + 'git', 'local' => isset($info['dirname']) ? " {$info['dirname']}" : '', + default => '', + }; + return "[{$type}]{$version}{$time}{$file}"; + } } diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 573ad7da8..421403c90 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -40,6 +40,42 @@ class PackageLoader /** @var array Track loaded classes to prevent duplicates */ private static array $loaded_classes = []; + /** + * Annotation metadata keyed by package name, capturing the defining class and its method-level attributes. + * + * @var array}>>}> + */ + private static array $annotation_map = []; + + /** + * Source metadata for #[BeforeStage] hooks, keyed by target package name → stage name. + * + * @var array>> + */ + private static array $before_stage_meta = []; + + /** + * Source metadata for #[AfterStage] hooks, keyed by target package name → stage name. + * + * @var array>> + */ + private static array $after_stage_meta = []; + + /** + * Reverse index of #[BeforeStage] hooks, keyed by registering class → target package → stage. + * Enables O(1) "outbound hook" lookup: what stages does a given class hook into on other packages? + * + * @var array>>> + */ + private static array $class_before_stage_meta = []; + + /** + * Reverse index of #[AfterStage] hooks, keyed by registering class → target package → stage. + * + * @var array>>> + */ + private static array $class_after_stage_meta = []; + public static function initPackageInstances(): void { if (self::$packages !== null) { @@ -213,8 +249,19 @@ public static function loadFromClass(mixed $class): void Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]), default => null, }; + + // Capture annotation metadata for inspection (dev:info, future event-trace commands) + $meta_attr = self::annotationShortName($method_attribute->getName()); + if ($meta_attr !== null) { + self::$annotation_map[$pkg->getName()]['methods'][$method->getName()][] = [ + 'attr' => $meta_attr, + 'args' => self::annotationArgs($method_instance), + ]; + } } } + // Record which class defines this package (set once; IS_REPEATABLE may loop more than once) + self::$annotation_map[$pkg->getName()]['class'] ??= $class_name; // register package self::$packages[$pkg->getName()] = $pkg; } @@ -260,6 +307,63 @@ public static function getAllAfterStages(): array return self::$after_stages; } + /** + * Get annotation metadata for a specific package. + * + * Returns null if no annotation class was loaded for this package (config-only package). + * The returned structure includes the defining class name, per-method attribute list, + * inbound BeforeStage/AfterStage hooks targeting this package, and outbound hooks that + * this package's class registers on other packages. + * + * @return null|array{ + * class: string, + * methods: array}>>, + * before_stages: array>, + * after_stages: array>, + * outbound_before_stages: array>>, + * outbound_after_stages: array>> + * } + */ + public static function getPackageAnnotationInfo(string $name): ?array + { + $class_info = self::$annotation_map[$name] ?? null; + if ($class_info === null) { + return null; + } + $class = $class_info['class']; + return [ + 'class' => $class, + 'methods' => $class_info['methods'], + 'before_stages' => self::$before_stage_meta[$name] ?? [], + 'after_stages' => self::$after_stage_meta[$name] ?? [], + 'outbound_before_stages' => self::$class_before_stage_meta[$class] ?? [], + 'outbound_after_stages' => self::$class_after_stage_meta[$class] ?? [], + ]; + } + + /** + * Get all annotation metadata keyed by package name. + * Useful for future event-trace commands or cross-package inspection. + * + * @return array + */ + public static function getAllAnnotations(): array + { + $result = []; + foreach (self::$annotation_map as $name => $info) { + $class = $info['class']; + $result[$name] = [ + 'class' => $class, + 'methods' => $info['methods'], + 'before_stages' => self::$before_stage_meta[$name] ?? [], + 'after_stages' => self::$after_stage_meta[$name] ?? [], + 'outbound_before_stages' => self::$class_before_stage_meta[$class] ?? [], + 'outbound_after_stages' => self::$class_after_stage_meta[$class] ?? [], + ]; + } + return $result; + } + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { // match condition @@ -385,6 +489,16 @@ private static function addBeforeStage(\ReflectionMethod $method, ?Package $pkg, } $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + $registering_class = get_class($instance_class); + self::$before_stage_meta[$package_name][$stage][] = [ + 'class' => $registering_class, + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + self::$class_before_stage_meta[$registering_class][$package_name][$stage][] = [ + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; } private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void @@ -400,5 +514,49 @@ private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, } $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + $registering_class = get_class($instance_class); + self::$after_stage_meta[$package_name][$stage][] = [ + 'class' => $registering_class, + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + self::$class_after_stage_meta[$registering_class][$package_name][$stage][] = [ + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + } + + /** + * Map a fully-qualified attribute class name to a short display name for metadata storage. + * Returns null for attributes that are not tracked in the annotation map. + */ + private static function annotationShortName(string $attr): ?string + { + return match ($attr) { + Stage::class => 'Stage', + BuildFor::class => 'BuildFor', + PatchBeforeBuild::class => 'PatchBeforeBuild', + CustomPhpConfigureArg::class => 'CustomPhpConfigureArg', + InitPackage::class => 'InitPackage', + ResolveBuild::class => 'ResolveBuild', + Info::class => 'Info', + Validate::class => 'Validate', + default => null, + }; + } + + /** + * Extract the meaningful constructor arguments from an attribute instance as a key-value array. + * + * @return array + */ + private static function annotationArgs(object $inst): array + { + return match (true) { + $inst instanceof Stage => array_filter(['function' => $inst->function], fn ($v) => $v !== null), + $inst instanceof BuildFor => ['os' => $inst->os], + $inst instanceof CustomPhpConfigureArg => array_filter(['os' => $inst->os], fn ($v) => $v !== ''), + default => [], + }; } } From 424228d81e39a31d29162f2a55c03427fa42ef34 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 22:33:00 +0800 Subject: [PATCH 337/682] Add ext-dom, ext-xml --- config/pkg/ext/builtin-extensions.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index b71e44aca..8f7739000 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -26,6 +26,14 @@ ext-dba: - qdbm php-extension: arg-type: custom +ext-dom: + type: php-extension + depends: + - libxml2 + - ext-xml + php-extension: + arg-type: '--enable-dom@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with ext-mbregex: type: php-extension depends: @@ -65,6 +73,17 @@ ext-readline: arg-type: '--with-libedit --without-readline' build-shared: false build-static: true +ext-xml: + type: php-extension + depends: + - libxml2 + depends@windows: + - libxml2 + - ext-iconv + php-extension: + arg-type: '--enable-xml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true ext-zlib: type: php-extension depends: From 1f768ffc644e362058f81e7ac8c866020738dbb1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 22:33:44 +0800 Subject: [PATCH 338/682] Mark transitive PHP extension dependencies of static extensions as static --- src/Package/Target/php.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 54d41dc5c..351622c99 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -27,6 +27,7 @@ use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ToolchainManager; +use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\V2CompatLayer; @@ -215,6 +216,25 @@ public function resolveBuild(TargetPackage $package, PackageInstaller $installer } } + // Mark transitive PHP extension dependencies of static extensions as static too + if (!empty($static_extensions)) { + $static_ext_pkgs = array_map(fn ($x) => "ext-{$x}", $static_extensions); + $transitive_deps = DependencyResolver::resolve($static_ext_pkgs); + foreach ($transitive_deps as $dep_name) { + if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { + continue; + } + $dep_instance = PackageLoader::getPackage($dep_name); + if (!$dep_instance instanceof PhpExtensionPackage || $dep_instance->isBuildStatic() || $dep_instance->isBuildShared()) { + continue; + } + $dep_config = PackageConfig::get($dep_name, 'php-extension', []); + if (($dep_config['build-static'] ?? true) !== false) { + $dep_instance->setBuildStatic(); + } + } + } + // building shared extensions need embed SAPI if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') { $installer->addBuildPackage('php-embed'); @@ -266,7 +286,8 @@ public function info(Package $package, PackageInstaller $installer): array $installer->isPackageResolved('php-embed') ? 'embed' : null, $installer->isPackageResolved('frankenphp') ? 'frankenphp' : null, ]); - $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic()); + $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && + $x->isBuildStatic()); $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); $install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-')); return [ From 77e129881a6d663464e85bd27666990428b15fd5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 11:04:18 +0800 Subject: [PATCH 339/682] Move all interactive input to construct --- src/Package/Target/php/unix.php | 22 ++++++- src/StaticPHP/Artifact/ArtifactDownloader.php | 24 ++++---- .../Command/InstallPackageCommand.php | 4 +- src/StaticPHP/Config/ConfigValidator.php | 2 + src/StaticPHP/Doctor/Doctor.php | 12 ++-- src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 8 +-- src/StaticPHP/Doctor/Item/PkgConfigCheck.php | 4 +- .../Doctor/Item/Re2cVersionCheck.php | 4 +- .../Doctor/Item/WindowsToolCheck.php | 14 ++--- src/StaticPHP/Doctor/Item/ZigCheck.php | 4 +- src/StaticPHP/Package/PackageInstaller.php | 59 ++++++++++++------- 11 files changed, 96 insertions(+), 61 deletions(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index d8236ebfa..40961b5e1 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -443,7 +443,27 @@ public function build(TargetPackage $package): void $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); - $package->runStage([$this, 'smokeTestForUnix']); + } + + #[Stage('postInstall')] + public function postInstall(TargetPackage $package, PackageInstaller $installer): void + { + if ($package->getName() === 'frankenphp') { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + return; + } + if ($package->getName() !== 'php') { + return; + } + if (SystemTarget::isUnix()) { + if ($installer->interactive) { + InteractiveTerm::indicateProgress('Running PHP smoke tests'); + } + $package->runStage([$this, 'smokeTestForUnix']); + if ($installer->interactive) { + InteractiveTerm::finish('PHP smoke tests passed'); + } + } } /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index a9a259157..6cc57439e 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -106,7 +106,7 @@ class ArtifactDownloader * no-shallow-clone?: bool * } $options Downloader options */ - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { // Allow setting concurrency via options $this->parallel = max(1, (int) ($options['parallel'] ?? 1)); @@ -273,12 +273,10 @@ public function setParallel(int $parallel): static /** * Download all artifacts, with optional parallel processing. - * - * @param bool $interactive Enable interactive mode with Ctrl+C handling */ - public function download(bool $interactive = true): void + public function download(): void { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(function () { InteractiveTerm::advance(); }); @@ -311,7 +309,7 @@ public function download(bool $interactive = true): void $count = count($this->artifacts); $artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts)); // mute the first line if not interactive - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ..."); } try { @@ -329,19 +327,19 @@ public function download(bool $interactive = true): void $skipped = []; foreach ($this->artifacts as $artifact) { ++$current; - if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { + if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { $skipped[] = $artifact->getName(); continue; } $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; } - if ($interactive) { + if ($this->interactive) { $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true); } } } finally { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(null); keyboard_interrupt_unregister(); } @@ -537,7 +535,7 @@ private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_nam return $dl->checkUpdate($artifact_name, $platform_config, null, $this); } - private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int { $queue = $this->generateQueue($artifact); // already downloaded @@ -558,7 +556,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, }; $try_h = $try ? 'Try downloading' : 'Downloading'; logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); } // is valid download type @@ -597,13 +595,13 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, } // process lock ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : ''; InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} ."); } return SPC_DOWNLOAD_STATUS_SUCCESS; } catch (DownloaderException|ExecutionException $e) { - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 230322618..b5fb8d2cf 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -34,9 +34,9 @@ public function configure(): void public function handle(): int { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], false); $installer->addInstallPackage($this->input->getArgument('package')); - $installer->run(true, true); + $installer->run(true); return static::SUCCESS; } } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index f011482c7..4a4f75db6 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -16,6 +16,7 @@ class ConfigValidator public const array PACKAGE_FIELD_TYPES = [ // package fields 'type' => ConfigType::STRING, + 'description' => ConfigType::STRING, 'depends' => ConfigType::LIST_ARRAY, // @ 'suggests' => ConfigType::LIST_ARRAY, // @ 'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT @@ -43,6 +44,7 @@ class ConfigValidator public const array PACKAGE_FIELDS = [ 'type' => true, + 'description' => false, 'depends' => false, // @ 'suggests' => false, // @ 'artifact' => false, diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index fc69cc2a8..1239a30c8 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -18,7 +18,7 @@ readonly class Doctor { - public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT) + public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT, public readonly bool $interactive = true) { // debug shows all loaded doctor items $items = DoctorLoader::getDoctorItems(); @@ -53,13 +53,13 @@ public static function markPassed(): void * Check all valid check items. * @return bool true if all checks passed, false otherwise */ - public function checkAll(bool $interactive = true): bool + public function checkAll(): bool { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { - if (!$this->checkItem($check, $interactive)) { + if (!$this->checkItem($check)) { return false; } } @@ -72,7 +72,7 @@ public function checkAll(bool $interactive = true): bool * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ - public function checkItem(CheckItem|string $check, bool $interactive = true): bool + public function checkItem(CheckItem|string $check): bool { if (is_string($check)) { $found = null; @@ -88,7 +88,7 @@ public function checkItem(CheckItem|string $check, bool $interactive = true): bo } $check = $found; } - $prepend = $interactive ? ' - ' : ''; + $prepend = $this->interactive ? ' - ' : ''; $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index b01b7b7b6..df3b5241c 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -59,8 +59,8 @@ public function checkMuslCrossMake(): ?CheckResult #[FixItem('fix-musl-wrapper')] public function fixMusl(): bool { - $downloader = new ArtifactDownloader(); - $downloader->add('musl-wrapper')->download(false); + $downloader = new ArtifactDownloader(interactive: false); + $downloader->add('musl-wrapper')->download(); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); $extractor->extract('musl-wrapper'); @@ -96,9 +96,9 @@ public function fixMuslCrossMake(): bool Shell::passthruCallback(function () { InteractiveTerm::advance(); }); - $downloader = new ArtifactDownloader(); + $downloader = new ArtifactDownloader(interactive: false); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); - $downloader->add('musl-toolchain')->download(false); + $downloader->add('musl-toolchain')->download(); $extractor->extract('musl-toolchain'); $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; f_passthru("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); diff --git a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php index 4a0ba498d..888651636 100644 --- a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php +++ b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php @@ -45,9 +45,9 @@ public function checkFunctional(): CheckResult public function fix(): bool { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller(['dl-binary-only' => true]); + $installer = new PackageInstaller(['dl-binary-only' => true], interactive: false); $installer->addInstallPackage('pkg-config'); - $installer->run(false, true); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php index fce3350be..3316be3f9 100644 --- a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -30,9 +30,9 @@ public function checkRe2cVersion(): ?CheckResult #[FixItem('build-re2c')] public function buildRe2c(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('re2c'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php index e6a042d3b..08e140f47 100644 --- a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -107,7 +107,7 @@ public function installPerl(): bool { $installer = new PackageInstaller(); $installer->addInstallPackage('strawberry-perl'); - $installer->run(false); + $installer->run(true); GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl'); return true; } @@ -116,27 +116,27 @@ public function installPerl(): bool public function installSDK(): bool { FileSystem::removeDir(getenv('PHP_SDK_PATH')); - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('php-sdk-binary-tools'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-nasm')] public function installNasm(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('nasm'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-vswhere')] public function installVSWhere(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('vswhere'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index c3a6aa9f3..baa6d4cbc 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -34,9 +34,9 @@ public function checkZig(): CheckResult #[FixItem('install-zig')] public function installZig(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('zig'); - $installer->run(false); + $installer->run(true); return $installer->isPackageInstalled('zig'); } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 628900fa2..d8d745f61 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -46,7 +46,7 @@ class PackageInstaller /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */ protected ?BuildRootTracker $tracker = null; - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { ApplicationContext::set(PackageInstaller::class, $this); $builder = new PackageBuilder($options); @@ -143,7 +143,7 @@ public function printBuildPackageOutputs(): void /** * Run the package installation process. */ - public function run(bool $interactive = true, bool $disable_delay_msg = false): void + public function run(bool $disable_delay_msg = false): void { // apply build toolchain envs GlobalEnvManager::afterInit(); @@ -153,7 +153,7 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): $this->resolvePackages(); } - if ($interactive && !$disable_delay_msg) { + if ($this->interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output $this->printInstallerInfo(); @@ -167,14 +167,17 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); - $downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]); - $downloader->addArtifacts($this->getArtifacts())->download($interactive); + $downloader = new ArtifactDownloader( + [...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))], + $this->interactive + ); + $downloader->addArtifacts($this->getArtifacts())->download(); } else { logger()->notice('Skipping download (--no-download option enabled)'); } // extract sources - $this->extractSourceArtifacts(interactive: $interactive); + $this->extractSourceArtifacts(); // validate packages foreach ($this->packages as $package) { @@ -183,7 +186,7 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): } // build/install packages - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Building/Installing packages ...'); keyboard_interrupt_register(function () { InteractiveTerm::finish('Build/Install process interrupted by user!', false); @@ -198,7 +201,7 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): $has_source = $package->hasSource(); if (!$is_to_build && $should_use_binary) { // install binary - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -210,17 +213,17 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -243,22 +246,20 @@ public function run(bool $interactive = true, bool $disable_delay_msg = false): } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } } } - $this->dumpLicenseFiles($this->packages); - if ($interactive) { - InteractiveTerm::success('Exported package licenses', true); - } + // perform after-install actions and emit post-install events + $this->emitPostInstallEvents(); } public function isBuildPackage(Package|string $package): bool @@ -311,6 +312,17 @@ public function isPackageInstalled(Package|string $package_name): bool return false; } + public function emitPostInstallEvents(): void + { + foreach ($this->packages as $package) { + if ($package->hasStage('postInstall')) { + $package->runStage('postInstall'); + } + } + + $this->dumpLicenseFiles($this->packages); + } + /** * Returns the download status of all artifacts for the resolved packages. * @@ -368,7 +380,7 @@ public function getArtifacts(): array /** * Extract all artifacts for resolved packages. */ - public function extractSourceArtifacts(bool $interactive = true): void + public function extractSourceArtifacts(): void { FileSystem::createDir(SOURCE_PATH); $packages = array_values($this->packages); @@ -403,7 +415,7 @@ public function extractSourceArtifacts(bool $interactive = true): void } // Extract each artifact - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...'); InteractiveTerm::indicateProgress('Extracting artifacts'); } @@ -411,7 +423,7 @@ public function extractSourceArtifacts(bool $interactive = true): void try { V2CompatLayer::beforeExtsExtractHook(); foreach ($artifacts as $artifact) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName())); } if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { @@ -423,12 +435,12 @@ public function extractSourceArtifacts(bool $interactive = true): void } } V2CompatLayer::afterExtsExtractHook(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Extracted all sources successfully.'); echo PHP_EOL; } } catch (\Throwable $e) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Artifact extraction failed!', false); echo PHP_EOL; } @@ -525,6 +537,9 @@ private function dumpLicenseFiles(array $packages): void } } $dumper->dump(BUILD_ROOT_PATH . '/license'); + if ($this->interactive) { + InteractiveTerm::success('Exported package licenses', true); + } } /** From b185d27ad75f6f583e8160a45ac488d2f84c51da Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 14:30:48 +0800 Subject: [PATCH 340/682] Add ext-ds --- config/pkg/ext/ext-ds.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/pkg/ext/ext-ds.yml diff --git a/config/pkg/ext/ext-ds.yml b/config/pkg/ext/ext-ds.yml new file mode 100644 index 000000000..0c0a4b3c4 --- /dev/null +++ b/config/pkg/ext/ext-ds.yml @@ -0,0 +1,9 @@ +ext-ds: + type: php-extension + artifact: + source: + type: pecl + name: ds + metadata: + license-files: [LICENSE] + license: MIT From 8cc5877f3c4a74936a7eb6a6a8bdcecc7bc86262 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 14:35:42 +0800 Subject: [PATCH 341/682] Add ext-ev,ext-sockets --- config/pkg/ext/builtin-extensions.yml | 2 ++ config/pkg/ext/ext-ev.yml | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-ev.yml diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 8f7739000..67d879a14 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -73,6 +73,8 @@ ext-readline: arg-type: '--with-libedit --without-readline' build-shared: false build-static: true +ext-sockets: + type: php-extension ext-xml: type: php-extension depends: diff --git a/config/pkg/ext/ext-ev.yml b/config/pkg/ext/ext-ev.yml new file mode 100644 index 000000000..174e5f843 --- /dev/null +++ b/config/pkg/ext/ext-ev.yml @@ -0,0 +1,13 @@ +ext-ev: + type: php-extension + artifact: + source: + type: pecl + name: ev + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - ext-sockets + php-extension: + arg-type@windows: with From a678d908d50fa86b1ab0f1966f2e7f14fb26bcc9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 14:45:36 +0800 Subject: [PATCH 342/682] Add ext-event --- config/pkg/ext/ext-event.yml | 19 ++++++++++++++ src/Package/Extension/event.php | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 config/pkg/ext/ext-event.yml create mode 100644 src/Package/Extension/event.php diff --git a/config/pkg/ext/ext-event.yml b/config/pkg/ext/ext-event.yml new file mode 100644 index 000000000..dd9c1c8ec --- /dev/null +++ b/config/pkg/ext/ext-event.yml @@ -0,0 +1,19 @@ +ext-event: + type: php-extension + artifact: + source: + type: url + url: 'https://bitbucket.org/osmanov/pecl-event/get/3.1.4.tar.gz' + extract: php-src/ext/event + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libevent + - ext-openssl + suggests: + - ext-sockets + php-extension: + support: + Windows: wip + arg-type: custom diff --git a/src/Package/Extension/event.php b/src/Package/Extension/event.php new file mode 100644 index 000000000..db1192745 --- /dev/null +++ b/src/Package/Extension/event.php @@ -0,0 +1,46 @@ +getBuilder()->getBuildRootPath()}"; + if ($installer->getLibraryPackage('openssl')) { + $arg .= " --with-event-openssl={$this->getBuilder()->getBuildRootPath()}"; + } + if ($installer->getPhpExtensionPackage('ext-sockets')) { + $arg .= ' --enable-event-sockets'; + } else { + $arg .= ' --disable-event-sockets'; + } + return $arg; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-event')] + #[PatchDescription('Prevent event extension compile error on macOS')] + public function patchBeforeMake(PackageInstaller $installer): void + { + // Prevent event extension compile error on macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + FileSystem::replaceFileRegex("{$php_src}/main/php_config.h", '/^#define HAVE_OPENPTY 1$/m', ''); + } + } +} From 552a8a1ea291c92af1f43d8f189c959699112ad3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:00:06 +0800 Subject: [PATCH 343/682] Add ext-excimer (closes #1019) --- config/pkg/ext/ext-excimer.yml | 9 +++++++++ src/Package/Extension/excimer.php | 19 +++++++++++++++++++ src/StaticPHP/Util/System/UnixUtil.php | 6 +----- 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 config/pkg/ext/ext-excimer.yml create mode 100644 src/Package/Extension/excimer.php diff --git a/config/pkg/ext/ext-excimer.yml b/config/pkg/ext/ext-excimer.yml new file mode 100644 index 000000000..3d0858882 --- /dev/null +++ b/config/pkg/ext/ext-excimer.yml @@ -0,0 +1,9 @@ +ext-excimer: + type: php-extension + artifact: + source: + type: pecl + name: excimer + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/src/Package/Extension/excimer.php b/src/Package/Extension/excimer.php new file mode 100644 index 000000000..9780a2ecd --- /dev/null +++ b/src/Package/Extension/excimer.php @@ -0,0 +1,19 @@ + Date: Mon, 9 Mar 2026 15:02:23 +0800 Subject: [PATCH 344/682] Add ext-exif --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 67d879a14..c5d832f8c 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -34,6 +34,8 @@ ext-dom: php-extension: arg-type: '--enable-dom@shared_suffix@ --with-libxml=@build_root_path@' arg-type@windows: with +ext-exif: + type: php-extension ext-mbregex: type: php-extension depends: From cf2e1d9819a1fd98d8fef7d9ed0ea008b69cb65d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:13:07 +0800 Subject: [PATCH 345/682] Add ext-ffi --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ src/Package/Artifact/php_src.php | 11 ---------- src/Package/Extension/ffi.php | 29 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 src/Package/Extension/ffi.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index c5d832f8c..a9f357f37 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -36,6 +36,13 @@ ext-dom: arg-type@windows: with ext-exif: type: php-extension +ext-ffi: + type: php-extension + depends@unix: + - libffi + php-extension: + arg-type@unix: '--with-ffi=@shared_suffix@ --enable-zend-signals' + arg-type@windows: with ext-mbregex: type: php-extension depends: diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php index ae9488d69..119f9056e 100644 --- a/src/Package/Artifact/php_src.php +++ b/src/Package/Artifact/php_src.php @@ -7,7 +7,6 @@ use Package\Target\php; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; @@ -52,16 +51,6 @@ public function patchGDWin32(): void } } - #[AfterSourceExtract('php-src')] - #[PatchDescription('Patch FFI extension on CentOS 7 with -O3 optimization (strncmp issue)')] - public function patchFfiCentos7FixO3strncmp(): void - { - spc_skip_if(!($ver = SystemTarget::getLibcVersion()) || version_compare($ver, '2.17', '>')); - $ver_id = php::getPHPVersionID(return_null_if_failed: true); - spc_skip_if($ver_id === null || $ver_id < 80316); - SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); - } - #[AfterSourceExtract('php-src')] #[PatchDescription('Add LICENSE file to IMAP extension if missing')] public function patchImapLicense(): void diff --git a/src/Package/Extension/ffi.php b/src/Package/Extension/ffi.php new file mode 100644 index 000000000..dc5287e0a --- /dev/null +++ b/src/Package/Extension/ffi.php @@ -0,0 +1,29 @@ +')); + $ver_id = php::getPHPVersionID(return_null_if_failed: true); + spc_skip_if($ver_id === null || $ver_id < 80316); + spc_skip_if(LinuxUtil::getOSRelease()['dist'] !== 'centos'); + SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); + } +} From 659b75cedd10c056371eee502775b79aab48d76d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:18:03 +0800 Subject: [PATCH 346/682] Remove redundant dependency for specific virtual target (php-fpm) --- src/StaticPHP/Util/DependencyResolver.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index 9833ae107..129468f92 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -45,12 +45,20 @@ public static function resolve(array $packages, array $dependency_overrides = [] } } + // Build a lookup set of explicitly requested packages for the promotion step below. + $input_package_set = []; + foreach ($packages as $pkg) { + $input_package_set[is_string($pkg) ? $pkg : $pkg->getName()] = true; + } + // Virtual-target packages (e.g. php-fpm) are built as part of their real parent's // build step, so any dependency they declare must be available before the real parent // is built. Promote those deps directly onto the real parent's dependency list so // that the topological sort places them before the parent. + // Only applies to virtual-targets that are in the input request — if a virtual-target + // is not being built, its deps must not be injected into the parent. foreach ($dep_list_clean as $pkg_name => $pkg_item) { - if (PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { + if (!isset($input_package_set[$pkg_name]) || PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { continue; } foreach ($pkg_item['depends'] as $dep_name) { From 8fdfcf8fcd3ec4eb5246fc84d428d8ee8c17192e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:29:12 +0800 Subject: [PATCH 347/682] Fix suggested extensions not passing when using `--with-suggests` --- src/Package/Target/php.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 351622c99..484998106 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -219,7 +219,7 @@ public function resolveBuild(TargetPackage $package, PackageInstaller $installer // Mark transitive PHP extension dependencies of static extensions as static too if (!empty($static_extensions)) { $static_ext_pkgs = array_map(fn ($x) => "ext-{$x}", $static_extensions); - $transitive_deps = DependencyResolver::resolve($static_ext_pkgs); + $transitive_deps = DependencyResolver::resolve($static_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); foreach ($transitive_deps as $dep_name) { if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { continue; From 38715bba21a8d821e3178728796647f243ee5c45 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:29:38 +0800 Subject: [PATCH 348/682] Add ext-fileinfo,ext-filter,ext-ftp --- config/pkg/ext/builtin-extensions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index a9f357f37..ee92025ca 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -43,6 +43,14 @@ ext-ffi: php-extension: arg-type@unix: '--with-ffi=@shared_suffix@ --enable-zend-signals' arg-type@windows: with +ext-fileinfo: + type: php-extension +ext-filter: + type: php-extension +ext-ftp: + type: php-extension + suggests: + - ext-openssl ext-mbregex: type: php-extension depends: From 61d50cd28bef972c1b6f80b19d03182cf2a348e1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:55:01 +0800 Subject: [PATCH 349/682] Add ext-gd --- config/pkg/ext/builtin-extensions.yml | 13 +++++++++++++ src/Package/Extension/gd.php | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/Package/Extension/gd.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index ee92025ca..2d7888a20 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -51,6 +51,19 @@ ext-ftp: type: php-extension suggests: - ext-openssl +ext-gd: + type: php-extension + depends: + - zlib + - libpng + - ext-zlib + suggests: + - libavif + - libwebp + - libjpeg + - freetype + php-extension: + arg-type: custom ext-mbregex: type: php-extension depends: diff --git a/src/Package/Extension/gd.php b/src/Package/Extension/gd.php new file mode 100644 index 000000000..5e815b5da --- /dev/null +++ b/src/Package/Extension/gd.php @@ -0,0 +1,26 @@ +getLibraryPackage('freetype') ? ' --with-freetype' : ''; + $arg .= $installer->getLibraryPackage('libjpeg') ? ' --with-jpeg' : ''; + $arg .= $installer->getLibraryPackage('libwebp') ? ' --with-webp' : ''; + $arg .= $installer->getLibraryPackage('libavif') ? ' --with-avif' : ''; + return $arg; + } +} From 4a572a1372b2ba75eaec8851efab92e00f0e762d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:59:27 +0800 Subject: [PATCH 350/682] Add ext-gettext --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 2d7888a20..79ca05379 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -64,6 +64,12 @@ ext-gd: - freetype php-extension: arg-type: custom +ext-gettext: + type: php-extension + depends: + - gettext + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From 7856f7e03a575b24ec222b2f0196207d5a2d7d57 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 16:30:00 +0800 Subject: [PATCH 351/682] Add ext-gmp --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 79ca05379..742358ebc 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -70,6 +70,12 @@ ext-gettext: - gettext php-extension: arg-type: with-path +ext-gmp: + type: php-extension + depends: + - gmp + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From 404195a38bbb9af9217edcc2b4abb14d5bdd6fae Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 16:30:35 +0800 Subject: [PATCH 352/682] Add ext-gmssl --- config/pkg/ext/ext-gmssl.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-gmssl.yml diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml new file mode 100644 index 000000000..7ed8981d7 --- /dev/null +++ b/config/pkg/ext/ext-gmssl.yml @@ -0,0 +1,12 @@ +ext-gmssl: + type: php-extension + artifact: + source: + type: ghtar + repo: gmssl/GmSSL-PHP + extract: php-src/ext/gmssl + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - gmssl From ad356b4a230d75f00e119bff6cf8da26084e306e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 20:12:14 +0800 Subject: [PATCH 353/682] Fix grpc build --- config/ext.json | 3 +++ src/SPC/builder/extension/grpc.php | 2 ++ src/globals/test-extensions.php | 10 +++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/config/ext.json b/config/ext.json index 4352f2e2c..ed1210f3e 100644 --- a/config/ext.json +++ b/config/ext.json @@ -255,6 +255,9 @@ "zlib", "openssl", "libcares" + ], + "frameworks": [ + "CoreFoundation" ] }, "iconv": { diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index 42dcdd5b8..86be8d3df 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -33,6 +33,8 @@ public function patchBeforeBuildconf(): bool 'GRPC_LIBDIR=' . BUILD_LIB_PATH . "\n" . 'LDFLAGS="$LDFLAGS -framework CoreFoundation"' ); } + FileSystem::replaceFileStr("{$this->source_dir}/config.m4", "CFLAGS=\"-std=c11 -g -O2\"\n", ''); + file_put_contents("{$this->source_dir}/php_grpc.h", '#include "src/php/ext/grpc/php_grpc.h"'); return true; } diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index ba02e672d..bfae094a3 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -16,15 +16,15 @@ // '8.1', // '8.2', // '8.3', - // '8.4', + '8.4', '8.5', // 'git', ]; // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - // 'macos-15-intel', // bin/spc for x86_64 - // 'macos-15', // bin/spc for arm64 + 'macos-15-intel', // bin/spc for x86_64 + 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 'ubuntu-24.04', // bin/spc for x86_64 @@ -35,7 +35,7 @@ ]; // whether enable thread safe -$zts = true; +$zts = false; $no_strip = false; @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'pgsql', + 'Linux', 'Darwin' => 'grpc', 'Windows' => 'com_dotnet', }; From 16e772e1a802afa52195eff0c36ae157500e095f Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 10 Mar 2026 08:42:17 +0700 Subject: [PATCH 354/682] add back in zig workaround as 0.16.x is not released yet --- src/SPC/builder/traits/UnixSystemUtilTrait.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SPC/builder/traits/UnixSystemUtilTrait.php b/src/SPC/builder/traits/UnixSystemUtilTrait.php index ff75bf7c2..33f824f36 100644 --- a/src/SPC/builder/traits/UnixSystemUtilTrait.php +++ b/src/SPC/builder/traits/UnixSystemUtilTrait.php @@ -72,6 +72,10 @@ public static function getDynamicExportedSymbols(string $lib_file): ?string if (!is_file($symbol_file)) { throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); } + // https://github.com/ziglang/zig/issues/24662 + if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { + return '-Wl,--export-dynamic'; // needs release 0.16, can be removed then + } // macOS/zig if (SPCTarget::getTargetOS() !== 'Linux' || ToolchainManager::getToolchainClass() === ZigToolchain::class) { return "-Wl,-exported_symbols_list,{$symbol_file}"; From b690566b390bef49188e14341e5482aa1c919103 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 10 Mar 2026 08:43:48 +0700 Subject: [PATCH 355/682] simplify rm command --- src/SPC/builder/unix/library/postgresql.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index 2ad4f51be..a3aed130b 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -93,8 +93,7 @@ protected function build(): void // remove dynamic libs shell()->cd($this->source_dir . '/build') - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*") - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so*") ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); From f93ad27c172119a38d31ca91e9112dcf46d67830 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 10 Mar 2026 08:47:38 +0700 Subject: [PATCH 356/682] allow using some libs as system provided (work around mssql linking vs system openssl) --- config/lib.json | 14 ++++++++++++++ src/SPC/builder/Extension.php | 3 ++- src/SPC/builder/LibraryBase.php | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/config/lib.json b/config/lib.json index ebbf4b87b..58cbd4ba0 100644 --- a/config/lib.json +++ b/config/lib.json @@ -862,6 +862,9 @@ }, "openssl": { "source": "openssl", + "pkg-configs": [ + "openssl" + ], "static-libs-unix": [ "libssl.a", "libcrypto.a" @@ -974,6 +977,11 @@ }, "unixodbc": { "source": "unixodbc", + "pkg-configs": [ + "odbc", + "odbccr", + "odbcinst" + ], "static-libs-unix": [ "libodbc.a", "libodbccr.a", @@ -1015,6 +1023,9 @@ }, "zlib": { "source": "zlib", + "pkg-configs": [ + "zlib" + ], "static-libs-unix": [ "libz.a" ], @@ -1028,6 +1039,9 @@ }, "zstd": { "source": "zstd", + "pkg-configs": [ + "libzstd" + ], "static-libs-unix": [ "libzstd.a" ], diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index f5a5d9561..9077269e5 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -96,7 +96,8 @@ public function getLibFilesString(): string fn ($x) => $x->getStaticLibFiles(), $this->getLibraryDependencies(recursive: true) ); - return implode(' ', $ret); + $libs = implode(' ', $ret); + return deduplicate_flags($libs); } /** diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index 383faa41a..0cd1c2351 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -365,6 +365,24 @@ protected function installLicense(): void protected function isLibraryInstalled(): bool { + if ($pkg_configs = Config::getLib(static::NAME, 'pkg-configs', [])) { + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_unique(array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path))); + + foreach ($pkg_configs as $name) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$name}.pc")) { + $found = true; + break; + } + } + if (!$found) { + return false; + } + } + return true; // allow using system dependencies if pkg_config_path is explicitly defined + } foreach (Config::getLib(static::NAME, 'static-libs', []) as $name) { if (!file_exists(BUILD_LIB_PATH . "/{$name}")) { return false; From 2277390a1abbebaf0c388f0773071b3ac4e19fa0 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 10 Mar 2026 08:49:56 +0700 Subject: [PATCH 357/682] fix removeConfigureArgs in UnixAutoconfExecutor.php --- src/SPC/util/executor/UnixAutoconfExecutor.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/SPC/util/executor/UnixAutoconfExecutor.php b/src/SPC/util/executor/UnixAutoconfExecutor.php index e04fe4f9c..9d57ef4eb 100644 --- a/src/SPC/util/executor/UnixAutoconfExecutor.php +++ b/src/SPC/util/executor/UnixAutoconfExecutor.php @@ -16,12 +16,11 @@ class UnixAutoconfExecutor extends Executor protected array $configure_args = []; - protected array $ignore_args = []; - public function __construct(protected BSDLibraryBase|LinuxLibraryBase|MacOSLibraryBase $library) { parent::__construct($library); $this->initShell(); + $this->configure_args = $this->getDefaultConfigureArgs(); } /** @@ -29,19 +28,12 @@ public function __construct(protected BSDLibraryBase|LinuxLibraryBase|MacOSLibra */ public function configure(...$args): static { - // remove all the ignored args - $args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args); - $args = array_diff($args, $this->ignore_args); + $args = array_merge($args, $this->configure_args); $configure_args = implode(' ', $args); return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}")); } - public function getConfigureArgsString(): string - { - return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args)); - } - /** * Run make * @@ -111,7 +103,7 @@ public function addConfigureArgs(...$args): static */ public function removeConfigureArgs(...$args): static { - $this->ignore_args = [...$this->ignore_args, ...$args]; + $this->configure_args = array_diff($this->configure_args, $args); return $this; } @@ -133,8 +125,8 @@ public function appendEnv(array $env): static private function getDefaultConfigureArgs(): array { return [ - '--disable-shared', '--enable-static', + '--disable-shared', "--prefix={$this->library->getBuildRootPath()}", '--with-pic', '--enable-pic', From 1edf14e64235303684b409840b9f2bd0046497d2 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 10 Mar 2026 08:52:15 +0700 Subject: [PATCH 358/682] set custom binary name for frankenphp --- src/SPC/builder/unix/UnixBuilderBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 1b532bbcf..301121929 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -458,6 +458,7 @@ protected function buildFrankenphp(): void '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . + '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, From 5d5a50a33cdf31f65f7656b5d6cc7beb585c8233 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 10 Mar 2026 10:57:49 +0700 Subject: [PATCH 359/682] Update src/SPC/builder/LibraryBase.php Co-authored-by: Jerry Ma --- src/SPC/builder/LibraryBase.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php index 0cd1c2351..73b1f9ed9 100644 --- a/src/SPC/builder/LibraryBase.php +++ b/src/SPC/builder/LibraryBase.php @@ -381,7 +381,10 @@ protected function isLibraryInstalled(): bool return false; } } - return true; // allow using system dependencies if pkg_config_path is explicitly defined + // allow using system dependencies if pkg_config_path is explicitly defined + if (count($search_paths) > 1) { + return true; + } } foreach (Config::getLib(static::NAME, 'static-libs', []) as $name) { if (!file_exists(BUILD_LIB_PATH . "/{$name}")) { From b89a29d5f31457550dd263f5125c001ca98d479a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 13:37:09 +0800 Subject: [PATCH 360/682] Add ext-grpc --- config/pkg/ext/ext-grpc.yml | 14 +++++++ src/Package/Extension/grpc.php | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 config/pkg/ext/ext-grpc.yml create mode 100644 src/Package/Extension/grpc.php diff --git a/config/pkg/ext/ext-grpc.yml b/config/pkg/ext/ext-grpc.yml new file mode 100644 index 000000000..ff5bae7b8 --- /dev/null +++ b/config/pkg/ext/ext-grpc.yml @@ -0,0 +1,14 @@ +ext-grpc: + type: php-extension + artifact: + source: + type: pecl + name: grpc + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - grpc + lang: cpp + php-extension: + arg-type@unix: enable-path diff --git a/src/Package/Extension/grpc.php b/src/Package/Extension/grpc.php new file mode 100644 index 000000000..c3b08f161 --- /dev/null +++ b/src/Package/Extension/grpc.php @@ -0,0 +1,70 @@ +getSourceDir()}/src/php/ext/grpc/call.c", + 'zend_exception_get_default(TSRMLS_C),', + 'zend_ce_exception,', + ); + + // custom config.m4 content for grpc extension, to prevent building libgrpc.a again + $config_m4 = <<<'M4' +PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])]) + +if test "$PHP_GRPC" != "no"; then + PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/include) + PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/src/php/ext/grpc) + GRPC_LIBDIR=@@build_lib_path@@ + PHP_ADD_LIBPATH($GRPC_LIBDIR) + PHP_ADD_LIBRARY(grpc,,GRPC_SHARED_LIBADD) + LIBS="-lpthread $LIBS" + PHP_ADD_LIBRARY(pthread) + + case $host in + *darwin*) + PHP_ADD_LIBRARY(c++,1,GRPC_SHARED_LIBADD) + ;; + *) + PHP_ADD_LIBRARY(stdc++,1,GRPC_SHARED_LIBADD) + PHP_ADD_LIBRARY(rt,,GRPC_SHARED_LIBADD) + PHP_ADD_LIBRARY(rt) + ;; + esac + + PHP_NEW_EXTENSION(grpc, @grpc_c_files@, $ext_shared, , -DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1) + PHP_SUBST(GRPC_SHARED_LIBADD) + PHP_INSTALL_HEADERS([ext/grpc], [php_grpc.h]) +fi +M4; + $replace = get_pack_replace(); + // load grpc c files from src/php/ext/grpc + $c_files = glob("{$this->getSourceDir()}/src/php/ext/grpc/*.c"); + $replace['@grpc_c_files@'] = implode(" \\\n ", array_map(fn ($f) => 'src/php/ext/grpc/' . basename($f), $c_files)); + $config_m4 = str_replace(array_keys($replace), array_values($replace), $config_m4); + file_put_contents("{$this->getSourceDir()}/config.m4", $config_m4); + + copy("{$this->getSourceDir()}/src/php/ext/grpc/php_grpc.h", "{$this->getSourceDir()}/php_grpc.h"); + } +} From 4fa5292913bfd78df1d37ca778ed184948904a27 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 13:49:34 +0800 Subject: [PATCH 361/682] Use custom config.m4 for grpc extension --- config/ext.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/ext.json b/config/ext.json index ed1210f3e..79d3831ec 100644 --- a/config/ext.json +++ b/config/ext.json @@ -252,12 +252,10 @@ "arg-type-unix": "enable-path", "cpp-extension": true, "lib-depends": [ + "grpc", "zlib", "openssl", "libcares" - ], - "frameworks": [ - "CoreFoundation" ] }, "iconv": { From 086c855a439801399a0ea1910939861fda7555ad Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 13:49:53 +0800 Subject: [PATCH 362/682] Use custom config.m4 for grpc extension --- src/SPC/builder/extension/grpc.php | 52 +++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index 86be8d3df..ba15518f2 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -11,7 +11,6 @@ use SPC\util\CustomExt; use SPC\util\GlobalEnvManager; use SPC\util\SPCConfigUtil; -use SPC\util\SPCTarget; #[CustomExt('grpc')] class grpc extends Extension @@ -21,20 +20,50 @@ public function patchBeforeBuildconf(): bool if ($this->builder instanceof WindowsBuilder) { throw new ValidationException('grpc extension does not support windows yet'); } + + // Fix deprecated PHP API usage in call.c FileSystem::replaceFileStr( - $this->source_dir . '/src/php/ext/grpc/call.c', + "{$this->source_dir}/src/php/ext/grpc/call.c", 'zend_exception_get_default(TSRMLS_C),', 'zend_ce_exception,', ); - if (SPCTarget::getTargetOS() === 'Darwin') { - FileSystem::replaceFileRegex( - $this->source_dir . '/config.m4', - '/GRPC_LIBDIR=.*$/m', - 'GRPC_LIBDIR=' . BUILD_LIB_PATH . "\n" . 'LDFLAGS="$LDFLAGS -framework CoreFoundation"' - ); - } - FileSystem::replaceFileStr("{$this->source_dir}/config.m4", "CFLAGS=\"-std=c11 -g -O2\"\n", ''); - file_put_contents("{$this->source_dir}/php_grpc.h", '#include "src/php/ext/grpc/php_grpc.h"'); + + $config_m4 = <<<'M4' +PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])]) + +if test "$PHP_GRPC" != "no"; then + PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/include) + PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/src/php/ext/grpc) + GRPC_LIBDIR=@@build_lib_path@@ + PHP_ADD_LIBPATH($GRPC_LIBDIR) + PHP_ADD_LIBRARY(grpc,,GRPC_SHARED_LIBADD) + LIBS="-lpthread $LIBS" + PHP_ADD_LIBRARY(pthread) + + case $host in + *darwin*) + PHP_ADD_LIBRARY(c++,1,GRPC_SHARED_LIBADD) + ;; + *) + PHP_ADD_LIBRARY(stdc++,1,GRPC_SHARED_LIBADD) + PHP_ADD_LIBRARY(rt,,GRPC_SHARED_LIBADD) + PHP_ADD_LIBRARY(rt) + ;; + esac + + PHP_NEW_EXTENSION(grpc, @grpc_c_files@, $ext_shared, , -DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1) + PHP_SUBST(GRPC_SHARED_LIBADD) + PHP_INSTALL_HEADERS([ext/grpc], [php_grpc.h]) +fi +M4; + $replace = get_pack_replace(); + // load grpc c files from src/php/ext/grpc + $c_files = glob($this->source_dir . '/src/php/ext/grpc/*.c'); + $replace['@grpc_c_files@'] = implode(" \\\n ", array_map(fn ($f) => 'src/php/ext/grpc/' . basename($f), $c_files)); + $config_m4 = str_replace(array_keys($replace), array_values($replace), $config_m4); + file_put_contents($this->source_dir . '/config.m4', $config_m4); + + copy($this->source_dir . '/src/php/ext/grpc/php_grpc.h', $this->source_dir . '/php_grpc.h'); return true; } @@ -50,7 +79,6 @@ public function patchBeforeConfigure(): bool public function patchBeforeMake(): bool { parent::patchBeforeMake(); - // add -Wno-strict-prototypes GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes'); return true; } From 465549f97dcfc1c706d9b1d2429f0e9c0f25b1f1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:41:01 +0800 Subject: [PATCH 363/682] Forward-port #1056 on v3 --- src/Package/Library/postgresql.php | 3 +-- src/Package/Target/php/frankenphp.php | 1 + .../Runtime/Executor/UnixAutoconfExecutor.php | 14 ++++---------- src/StaticPHP/Util/System/UnixUtil.php | 6 +++++- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 18893d0ec..682b79e28 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -119,8 +119,7 @@ public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): // remove dynamic libs shell()->cd($this->getSourceDir() . '/build') - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*") - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so*") ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 8b2fb81d4..51304d66e 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -92,6 +92,7 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . + '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index 41bc6e784..c59859cf4 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -20,8 +20,6 @@ class UnixAutoconfExecutor extends Executor protected array $configure_args = []; - protected array $ignore_args = []; - protected PackageInstaller $installer; public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null) @@ -40,6 +38,8 @@ public function __construct(protected LibraryPackage $package, ?PackageInstaller if (!$this->package->hasStage('build')) { throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); } + + $this->configure_args = $this->getDefaultConfigureArgs(); } /** @@ -48,18 +48,12 @@ public function __construct(protected LibraryPackage $package, ?PackageInstaller public function configure(...$args): static { // remove all the ignored args - $args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args); - $args = array_diff($args, $this->ignore_args); + $args = array_merge($args, $this->configure_args); $configure_args = implode(' ', $args); InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (./configure)'); return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}")); } - public function getConfigureArgsString(): string - { - return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args)); - } - /** * Run make * @@ -134,7 +128,7 @@ public function addConfigureArgs(...$args): static */ public function removeConfigureArgs(...$args): static { - $this->ignore_args = [...$this->ignore_args, ...$args]; + $this->configure_args = array_diff($this->configure_args, $args); return $this; } diff --git a/src/StaticPHP/Util/System/UnixUtil.php b/src/StaticPHP/Util/System/UnixUtil.php index 7d7ddfe21..4a41c5244 100644 --- a/src/StaticPHP/Util/System/UnixUtil.php +++ b/src/StaticPHP/Util/System/UnixUtil.php @@ -74,7 +74,11 @@ public static function getDynamicExportedSymbols(string $lib_file): ?string throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); } // https://github.com/ziglang/zig/issues/24662 - if (SystemTarget::getTargetOS() !== 'Linux' || ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + $toolchain = ApplicationContext::get(ToolchainInterface::class); + if ($toolchain instanceof ZigToolchain) { + return '-Wl,--export-dynamic'; // needs release 0.16, can be removed then + } + if (SystemTarget::getTargetOS() !== 'Linux') { return "-Wl,-exported_symbols_list,{$symbol_file}"; } return "-Wl,--dynamic-list={$symbol_file}"; From e0d2ee91f7b318e8a8b893d41a7222f1a406a87d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:52:29 +0800 Subject: [PATCH 364/682] Add ext-gmp --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 79ca05379..742358ebc 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -70,6 +70,12 @@ ext-gettext: - gettext php-extension: arg-type: with-path +ext-gmp: + type: php-extension + depends: + - gmp + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From bc26e3d37c27a2059562e25c56a5fd1720380b3d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:52:36 +0800 Subject: [PATCH 365/682] Add ext-gmssl --- config/pkg/ext/ext-gmssl.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-gmssl.yml diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml new file mode 100644 index 000000000..7ed8981d7 --- /dev/null +++ b/config/pkg/ext/ext-gmssl.yml @@ -0,0 +1,12 @@ +ext-gmssl: + type: php-extension + artifact: + source: + type: ghtar + repo: gmssl/GmSSL-PHP + extract: php-src/ext/gmssl + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - gmssl From 2d906a8145323f9e5c94eaa537a1a9f6fd89ae30 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:56:09 +0800 Subject: [PATCH 366/682] Add ext-iconv --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 742358ebc..eeee7b050 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -76,6 +76,13 @@ ext-gmp: - gmp php-extension: arg-type: with-path +ext-iconv: + type: php-extension + depends@unix: + - libiconv + php-extension: + arg-type@unix: with-path + arg-type@windows: with ext-mbregex: type: php-extension depends: From e73bad9d239dee36c64f8934f35a7b5b641bb4b1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:59:48 +0800 Subject: [PATCH 367/682] Add ext-igbinary --- config/pkg/ext/ext-igbinary.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-igbinary.yml diff --git a/config/pkg/ext/ext-igbinary.yml b/config/pkg/ext/ext-igbinary.yml new file mode 100644 index 000000000..1a80831bb --- /dev/null +++ b/config/pkg/ext/ext-igbinary.yml @@ -0,0 +1,12 @@ +ext-igbinary: + type: php-extension + artifact: + source: + type: pecl + name: igbinary + metadata: + license-files: [COPYING] + license: BSD-3-Clause + suggests: + - ext-session + - ext-apcu From 1400dc649ffddc414217257384c6ec8a69be9953 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:59:54 +0800 Subject: [PATCH 368/682] Add ext-session --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index eeee7b050..a0ef8ac85 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -122,6 +122,8 @@ ext-readline: arg-type: '--with-libedit --without-readline' build-shared: false build-static: true +ext-session: + type: php-extension ext-sockets: type: php-extension ext-xml: From 92f5b56c744d42fb6a4d38c40e09aa7a80490b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 10 Mar 2026 11:57:23 +0100 Subject: [PATCH 369/682] fix: FrankenPHP build args --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 301121929..fd16656ce 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -457,8 +457,8 @@ protected function buildFrankenphp(): void 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . + '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . - '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, From d7eb33ff1ee4df908a0f5a3a8fe675e95dcf5c23 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:01:23 +0800 Subject: [PATCH 370/682] Forward-port #1057 --- src/Package/Target/php/frankenphp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 51304d66e..f513242b2 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -91,8 +91,8 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . + '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . - '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, From e31aeabf122404e66aa982c9ec8e5529e27da232 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:19:01 +0800 Subject: [PATCH 371/682] Add ext-imagick --- config/pkg/ext/ext-imagick.yml | 13 +++++++++++++ src/Package/Extension/imagick.php | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 config/pkg/ext/ext-imagick.yml create mode 100644 src/Package/Extension/imagick.php diff --git a/config/pkg/ext/ext-imagick.yml b/config/pkg/ext/ext-imagick.yml new file mode 100644 index 000000000..e6f9843eb --- /dev/null +++ b/config/pkg/ext/ext-imagick.yml @@ -0,0 +1,13 @@ +ext-imagick: + type: php-extension + artifact: + source: + type: pecl + name: imagick + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - imagemagick + php-extension: + arg-type: custom diff --git a/src/Package/Extension/imagick.php b/src/Package/Extension/imagick.php new file mode 100644 index 000000000..2d2aa0aa1 --- /dev/null +++ b/src/Package/Extension/imagick.php @@ -0,0 +1,21 @@ +getBuildRootPath() . $disable_omp; + } +} From f83565b0589c39974057081128f0f3c49aaa8345 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:25:11 +0800 Subject: [PATCH 372/682] Add ext-intl --- config/pkg/ext/builtin-extensions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index a0ef8ac85..49c73282f 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -83,6 +83,10 @@ ext-iconv: php-extension: arg-type@unix: with-path arg-type@windows: with +ext-intl: + type: php-extension + depends@unix: + - icu ext-mbregex: type: php-extension depends: From d8dda09fb6e0765a54a0fe575cda17d5a8e2cb3c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:38:34 +0800 Subject: [PATCH 373/682] Add ext-ldap --- config/pkg/ext/builtin-extensions.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 49c73282f..381617593 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -87,6 +87,16 @@ ext-intl: type: php-extension depends@unix: - icu +ext-ldap: + type: php-extension + depends: + - ldap + suggests: + - gmp + - libsodium + - ext-openssl + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From c5b11f47c335b2c68ae29275d113b49ffd1a8518 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:41:33 +0800 Subject: [PATCH 374/682] Add ext-libxml --- config/pkg/ext/builtin-extensions.yml | 8 ++++++++ src/StaticPHP/Package/PhpExtensionPackage.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 381617593..7bbb34546 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -97,6 +97,14 @@ ext-ldap: - ext-openssl php-extension: arg-type: with-path +ext-libxml: + type: php-extension + depends: + - ext-xml + php-extension: + build-with-php: true + build-shared: false + arg-type: none ext-mbregex: type: php-extension depends: diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index bae117489..7064d041e 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -94,7 +94,7 @@ public function getPhpConfigureArg(string $os, bool $shared): string 'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}", 'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}", 'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}", - 'custom' => '', + 'custom', 'none' => '', default => $arg_type, }; // customize argument from config string From fa7de0642a9d485dbdfc30c57b47173754f39caf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:47:37 +0800 Subject: [PATCH 375/682] Add ext-lz4 --- config/pkg/ext/ext-lz4.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-lz4.yml diff --git a/config/pkg/ext/ext-lz4.yml b/config/pkg/ext/ext-lz4.yml new file mode 100644 index 000000000..8a3bb4dba --- /dev/null +++ b/config/pkg/ext/ext-lz4.yml @@ -0,0 +1,15 @@ +ext-lz4: + type: php-extension + artifact: + source: + type: ghtagtar + repo: kjdev/php-ext-lz4 + extract: php-src/ext/lz4 + metadata: + license-files: [LICENSE] + license: MIT + depends: + - liblz4 + php-extension: + arg-type@unix: '--enable-lz4=@shared_suffix@ --with-lz4-includedir=@build_root_path@' + arg-type@windows: '--enable-lz4' From f414bd289cafcc15ff22aa1ac1155e4ae95cbe03 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 08:18:25 +0800 Subject: [PATCH 376/682] Add ext-maxminddb --- config/pkg/ext/ext-maxminddb.yml | 13 +++++++++++++ src/Package/Extension/maxminddb.php | 30 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 config/pkg/ext/ext-maxminddb.yml create mode 100644 src/Package/Extension/maxminddb.php diff --git a/config/pkg/ext/ext-maxminddb.yml b/config/pkg/ext/ext-maxminddb.yml new file mode 100644 index 000000000..59d7e4e3d --- /dev/null +++ b/config/pkg/ext/ext-maxminddb.yml @@ -0,0 +1,13 @@ +ext-maxminddb: + type: php-extension + artifact: + source: + type: pecl + name: maxminddb + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - libmaxminddb + php-extension: + arg-type: with diff --git a/src/Package/Extension/maxminddb.php b/src/Package/Extension/maxminddb.php new file mode 100644 index 000000000..bda8d34c7 --- /dev/null +++ b/src/Package/Extension/maxminddb.php @@ -0,0 +1,30 @@ +getSourceDir()}/config.m4")) { + return; + } + // move ext/maxminddb/ext/* to ext/maxminddb/ + $files = FileSystem::scanDirFiles("{$this->getSourceDir()}/ext", false, true); + foreach ($files as $file) { + rename("{$this->getSourceDir()}/ext/{$file}", "{$this->getSourceDir()}/{$file}"); + } + } +} From e49a5d7a50f9b0d0b5b5fd2ef35a2cf9917af77a Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 09:42:39 +0700 Subject: [PATCH 377/682] make php 8.5 default --- src/SPC/store/source/PhpSource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/store/source/PhpSource.php b/src/SPC/store/source/PhpSource.php index 02ba87554..bf33d6b7d 100644 --- a/src/SPC/store/source/PhpSource.php +++ b/src/SPC/store/source/PhpSource.php @@ -20,7 +20,7 @@ class PhpSource extends CustomSourceBase public function fetch(bool $force = false, ?array $config = null, int $lock_as = SPC_DOWNLOAD_SOURCE): void { - $major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.4'; + $major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.5'; if ($major === 'git') { Downloader::downloadSource('php-src', ['type' => 'git', 'url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $force); } else { From 901da8fa41268a8850af08dcee84004972ea48a8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 09:43:02 +0700 Subject: [PATCH 378/682] remove ldtl from odbc libs private (using built in ltdl) --- src/SPC/builder/unix/library/unixodbc.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/SPC/builder/unix/library/unixodbc.php b/src/SPC/builder/unix/library/unixodbc.php index 9a3cb63d1..41cc4cbef 100644 --- a/src/SPC/builder/unix/library/unixodbc.php +++ b/src/SPC/builder/unix/library/unixodbc.php @@ -5,6 +5,7 @@ namespace SPC\builder\unix\library; use SPC\exception\WrongUsageException; +use SPC\store\FileSystem; use SPC\util\executor\UnixAutoconfExecutor; trait unixodbc @@ -31,6 +32,12 @@ protected function build(): void ) ->make(); $this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']); + foreach (['odbc.pc', 'odbccr.pc', 'odbcinst.pc'] as $file) { + FileSystem::replaceFileStr( + BUILD_LIB_PATH . "/pkgconfig/{$file}.pc", + '$(top_build_prefix)libltdl/libltdlc.la', + ''); + } $this->patchLaDependencyPrefix(); } } From ef4b2997a74a09bb00bdba673266ad6506ff9a3b Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 09:45:56 +0700 Subject: [PATCH 379/682] test --- src/SPC/builder/unix/library/unixodbc.php | 7 ++++--- src/globals/test-extensions.php | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/SPC/builder/unix/library/unixodbc.php b/src/SPC/builder/unix/library/unixodbc.php index 41cc4cbef..3729a417a 100644 --- a/src/SPC/builder/unix/library/unixodbc.php +++ b/src/SPC/builder/unix/library/unixodbc.php @@ -31,10 +31,11 @@ protected function build(): void '--enable-gui=no', ) ->make(); - $this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']); - foreach (['odbc.pc', 'odbccr.pc', 'odbcinst.pc'] as $file) { + $pkgConfigs = ['odbc.pc', 'odbccr.pc', 'odbcinst.pc']; + $this->patchPkgconfPrefix($pkgConfigs); + foreach ($pkgConfigs as $file) { FileSystem::replaceFileStr( - BUILD_LIB_PATH . "/pkgconfig/{$file}.pc", + BUILD_LIB_PATH . "/pkgconfig/{$file}", '$(top_build_prefix)libltdl/libltdlc.la', ''); } diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index bfae094a3..46b7cb9db 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'grpc', + 'Linux', 'Darwin' => 'pdo_odbc', 'Windows' => 'com_dotnet', }; From a335d050cff39d1bb2194f86f0817c5f68d56d99 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 09:46:41 +0700 Subject: [PATCH 380/682] cs fix --- src/SPC/builder/unix/library/unixodbc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/library/unixodbc.php b/src/SPC/builder/unix/library/unixodbc.php index 3729a417a..cf923f24b 100644 --- a/src/SPC/builder/unix/library/unixodbc.php +++ b/src/SPC/builder/unix/library/unixodbc.php @@ -37,7 +37,8 @@ protected function build(): void FileSystem::replaceFileStr( BUILD_LIB_PATH . "/pkgconfig/{$file}", '$(top_build_prefix)libltdl/libltdlc.la', - ''); + '' + ); } $this->patchLaDependencyPrefix(); } From 70285cb53bb92d0455b69f79ed634c69451e1fb0 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 09:48:50 +0700 Subject: [PATCH 381/682] actually update to 8.5 --- src/SPC/command/DownloadCommand.php | 2 +- src/SPC/util/ConfigValidator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php index 00bcc1948..b7f2b078b 100644 --- a/src/SPC/command/DownloadCommand.php +++ b/src/SPC/command/DownloadCommand.php @@ -30,7 +30,7 @@ public function configure(): void $this->addArgument('sources', InputArgument::REQUIRED, 'The sources will be compiled, comma separated'); $this->addOption('shallow-clone', null, null, 'Clone shallow'); $this->addOption('with-openssl11', null, null, 'Use openssl 1.1'); - $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'version in major.minor format (default 8.4)', '8.4'); + $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'version in major.minor format (default 8.5)', '8.5'); $this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); $this->addOption('all', 'A', null, 'Fetch all sources that static-php-cli needed'); $this->addOption('custom-url', 'U', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://downloads.php.net/~eric/php-8.3.0beta1.tar.gz"'); diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index 445c6242f..d9f751ef9 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -393,7 +393,7 @@ public static function validateAndParseCraftFile(mixed $craft_file, Command $com } // check php-version if (isset($craft['php-version'])) { - // validdate version, accept 8.x, 7.x, 8.x.x, 7.x.x, 8, 7 + // validate version, accept 8.x, 7.x, 8.x.x, 7.x.x, 8, 7 $version = strval($craft['php-version']); if (!preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', $version, $matches)) { throw new ValidationException('Craft file php-version is invalid'); From f6a9dac504f26f1301bda5265de3edf6c1880a45 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 11:07:25 +0800 Subject: [PATCH 382/682] Fix grpc build error with RPATH --- src/Package/Library/grpc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Library/grpc.php b/src/Package/Library/grpc.php index 86cddcc06..0e2d191af 100644 --- a/src/Package/Library/grpc.php +++ b/src/Package/Library/grpc.php @@ -48,6 +48,7 @@ public function buildUnix(ToolchainInterface $toolchain, LibraryPackage $lib): v '-DgRPC_ZLIB_PROVIDER=package', '-DgRPC_CARES_PROVIDER=package', '-DgRPC_SSL_PROVIDER=package', + '-DCMAKE_SKIP_INSTALL_RPATH=ON', ); if (PHP_OS_FAMILY === 'Linux' && $toolchain->isStatic() && !LinuxUtil::isMuslDist()) { From a232f578a455112bc7a057b630ee877c3c816c42 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 10:11:39 +0700 Subject: [PATCH 383/682] test bulk --- src/globals/test-extensions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 46b7cb9db..3af2b55d3 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -74,7 +74,7 @@ // You can use `common`, `bulk`, `minimal` or `none`. // note: combination is only available for *nix platform. Windows must use `none` combination $base_combination = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'minimal', + 'Linux', 'Darwin' => 'bulk', 'Windows' => 'none', }; @@ -89,7 +89,7 @@ function _getCombination(string $type = 'common'): string 'common' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,gd,gmp,iconv,xml,mbstring,mbregex,' . 'mysqlnd,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,redis,session,simplexml,soap,sockets,' . 'sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip', - 'bulk' => 'apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,event,exif,fileinfo,filter,ftp,gd,gmp,iconv,imagick,imap,' . + 'bulk' => 'apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,event,exif,fileinfo,filter,ftp,gd,gmp,iconv,imagick,' . 'intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,' . 'posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,swoole,sysvmsg,sysvsem,' . 'sysvshm,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib', From 1b8b53d47f1af8fa4479ce1cb9e1ec75f109fb18 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 10:19:08 +0700 Subject: [PATCH 384/682] update swoole args for 6.2 --- src/SPC/builder/extension/swoole.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index 4e292a362..340a8ac39 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -50,19 +50,19 @@ public function getUnixConfigureArg(bool $shared = false): string // commonly used feature: coroutine-time $arg .= ' --enable-swoole-coro-time --with-pic'; + $arg .= ' --enable-swoole-ftp --enable-swoole-ssh'; $arg .= $this->builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; // required features: curl, openssl (but curl hook is buggy for php 8.0) $arg .= $this->builder->getPHPVersionID() >= 80100 ? ' --enable-swoole-curl' : ' --disable-swoole-curl'; - $arg .= ' --enable-openssl'; // additional features that only require libraries $arg .= $this->builder->getLib('libcares') ? ' --enable-cares' : ''; $arg .= $this->builder->getLib('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; $arg .= $this->builder->getLib('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : ''; $arg .= $this->builder->getLib('zstd') ? ' --enable-zstd' : ''; - $arg .= $this->builder->getLib('liburing') ? ' --enable-iouring' : ''; + $arg .= $this->builder->getLib('liburing') ? ' --enable-iouring --enable-uring-socket' : ''; $arg .= $this->builder->getExt('sockets') ? ' --enable-sockets' : ''; // enable additional features that require the pdo extension, but conflict with pdo_* extensions From 1049a3ce66e6037b46813acc665096de542d96c4 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 10:32:58 +0700 Subject: [PATCH 385/682] curl is always supported now (swoole no longer supports php < 8.1) --- src/SPC/builder/extension/swoole.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index 340a8ac39..e79ffc13b 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -50,13 +50,10 @@ public function getUnixConfigureArg(bool $shared = false): string // commonly used feature: coroutine-time $arg .= ' --enable-swoole-coro-time --with-pic'; - $arg .= ' --enable-swoole-ftp --enable-swoole-ssh'; + $arg .= ' --enable-swoole-ftp --enable-swoole-ssh --enable-swoole-curl'; $arg .= $this->builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; - // required features: curl, openssl (but curl hook is buggy for php 8.0) - $arg .= $this->builder->getPHPVersionID() >= 80100 ? ' --enable-swoole-curl' : ' --disable-swoole-curl'; - // additional features that only require libraries $arg .= $this->builder->getLib('libcares') ? ' --enable-cares' : ''; $arg .= $this->builder->getLib('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; From 1fcb74ad9b0dec3ffa65adeb78e3a2efe18312d7 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 13:42:38 +0700 Subject: [PATCH 386/682] swoole-ftp conflicts with ftp --- src/SPC/builder/extension/swoole.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index e79ffc13b..afd309dbe 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -50,7 +50,7 @@ public function getUnixConfigureArg(bool $shared = false): string // commonly used feature: coroutine-time $arg .= ' --enable-swoole-coro-time --with-pic'; - $arg .= ' --enable-swoole-ftp --enable-swoole-ssh --enable-swoole-curl'; + $arg .= ' --enable-swoole-ssh --enable-swoole-curl'; $arg .= $this->builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; @@ -71,6 +71,7 @@ public function getUnixConfigureArg(bool $shared = false): string $config = (new SPCConfigUtil($this->builder))->getLibraryConfig($this->builder->getLib('unixodbc')); $arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"'; } + $arg .= $this->builder->getExt('ftp') ? ' --disable-swoole-ftp' : ' --enable-swoole-ftp'; if ($this->getExtVersion() >= '6.1.0') { $arg .= ' --enable-swoole-stdext'; From 85b0cd8b4bd85389a820ce8c2e00da948e698774 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 11 Mar 2026 13:54:24 +0700 Subject: [PATCH 387/682] only disable when building ftp static, shared is fine --- src/SPC/builder/extension/swoole.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index afd309dbe..86098ec51 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -71,7 +71,7 @@ public function getUnixConfigureArg(bool $shared = false): string $config = (new SPCConfigUtil($this->builder))->getLibraryConfig($this->builder->getLib('unixodbc')); $arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"'; } - $arg .= $this->builder->getExt('ftp') ? ' --disable-swoole-ftp' : ' --enable-swoole-ftp'; + $arg .= $this->builder->getExt('ftp')?->isBuildStatic() ? ' --disable-swoole-ftp' : ' --enable-swoole-ftp'; if ($this->getExtVersion() >= '6.1.0') { $arg .= ' --enable-swoole-stdext'; From cbfeefc8089f80309003ea100a431d763f2239f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 15:12:21 +0800 Subject: [PATCH 388/682] Add ext-inotify --- config/pkg/ext/ext-inotify.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/pkg/ext/ext-inotify.yml diff --git a/config/pkg/ext/ext-inotify.yml b/config/pkg/ext/ext-inotify.yml new file mode 100644 index 000000000..0956f9e40 --- /dev/null +++ b/config/pkg/ext/ext-inotify.yml @@ -0,0 +1,9 @@ +ext-inotify: + type: php-extension + artifact: + source: + type: pecl + name: inotify + metadata: + license-files: [LICENSE] + license: PHP-3.01 From f35f133115ffd0b58580d9e9b3ad8e4acfe48d2b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 15:29:00 +0800 Subject: [PATCH 389/682] Add ext-memcache --- config/pkg/ext/ext-memcache.yml | 14 ++++++ src/Package/Extension/memcache.php | 75 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 config/pkg/ext/ext-memcache.yml create mode 100644 src/Package/Extension/memcache.php diff --git a/config/pkg/ext/ext-memcache.yml b/config/pkg/ext/ext-memcache.yml new file mode 100644 index 000000000..9db51c05b --- /dev/null +++ b/config/pkg/ext/ext-memcache.yml @@ -0,0 +1,14 @@ +ext-memcache: + type: php-extension + artifact: + source: + type: pecl + name: memcache + metadata: + license-files: [LICENSE] + license: PHP-3.0 + depends: + - ext-zlib + - ext-session + php-extension: + arg-type: '--enable-memcache@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/src/Package/Extension/memcache.php b/src/Package/Extension/memcache.php new file mode 100644 index 000000000..a9c58b768 --- /dev/null +++ b/src/Package/Extension/memcache.php @@ -0,0 +1,75 @@ +isBuildStatic()) { + return false; + } + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'if test -d $abs_srcdir/src ; then', + 'if test -d $abs_srcdir/main ; then' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"' + ); + // add for in-tree building + file_put_contents( + "{$this->getSourceDir()}/php_memcache.h", + <<<'EOF' +#ifndef PHP_MEMCACHE_H +#define PHP_MEMCACHE_H + +extern zend_module_entry memcache_module_entry; +#define phpext_memcache_ptr &memcache_module_entry + +#endif +EOF + ); + return true; + } + + #[BeforeStage('ext-memcache', [self::class, 'configureForUnix'])] + #[PatchDescription('Fix memcache extension compile error when building as shared')] + public function patchBeforeSharedConfigure(): bool + { + if (!$this->isBuildShared()) { + return false; + } + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'if test -d $abs_srcdir/main ; then', + 'if test -d $abs_srcdir/src ; then', + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + ); + return true; + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17'; + return $parent; + } +} From 59a8b65f6f1d537b2a9ccb02a5ec5804cdef7080 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 16:14:05 +0800 Subject: [PATCH 390/682] Add ext-memcache,ext-msgpack --- config/pkg/ext/ext-memcached.yml | 23 ++++++++++++++++++++++ config/pkg/ext/ext-msgpack.yml | 14 ++++++++++++++ src/Package/Extension/memcached.php | 30 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 config/pkg/ext/ext-memcached.yml create mode 100644 config/pkg/ext/ext-msgpack.yml create mode 100644 src/Package/Extension/memcached.php diff --git a/config/pkg/ext/ext-memcached.yml b/config/pkg/ext/ext-memcached.yml new file mode 100644 index 000000000..329227f2b --- /dev/null +++ b/config/pkg/ext/ext-memcached.yml @@ -0,0 +1,23 @@ +ext-memcached: + type: php-extension + artifact: + source: + type: pecl + name: memcached + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libmemcached + depends@unix: + - libmemcached + - fastlz + - ext-session + - ext-zlib + suggests: + - zstd + - ext-igbinary + - ext-msgpack + - ext-session + php-extension: + arg-type: '--enable-memcached@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/config/pkg/ext/ext-msgpack.yml b/config/pkg/ext/ext-msgpack.yml new file mode 100644 index 000000000..8b230c31a --- /dev/null +++ b/config/pkg/ext/ext-msgpack.yml @@ -0,0 +1,14 @@ +ext-msgpack: + type: php-extension + artifact: + source: + type: pecl + name: msgpack + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-session + php-extension: + arg-type@unix: with + arg-type@windows: enable diff --git a/src/Package/Extension/memcached.php b/src/Package/Extension/memcached.php new file mode 100644 index 000000000..0453e8ecf --- /dev/null +++ b/src/Package/Extension/memcached.php @@ -0,0 +1,30 @@ +getLibraryPackage('zlib')->getBuildRootPath() . ' ' . + '--with-libmemcached-dir=' . $installer->getLibraryPackage('libmemcached')->getBuildRootPath() . ' ' . + '--disable-memcached-sasl ' . + '--enable-memcached-json ' . + ($installer->getLibraryPackage('zstd') ? '--with-zstd ' : '') . + ($installer->getPhpExtensionPackage('ext-igbinary') ? '--enable-memcached-igbinary ' : '') . + ($installer->getPhpExtensionPackage('ext-session') ? '--enable-memcached-session ' : '') . + ($installer->getPhpExtensionPackage('ext-msgpack') ? '--enable-memcached-msgpack ' : '') . + '--with-system-fastlz'; + } +} From 32bb0aadce48efd461a05007a27f954e3ec58333 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 16:37:25 +0800 Subject: [PATCH 391/682] Add ext-mysqli,ext-mysqlnd --- config/pkg/ext/builtin-extensions.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 7bbb34546..00d1d5930 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -119,6 +119,21 @@ ext-mbstring: type: php-extension php-extension: arg-type: custom +ext-mysqli: + type: php-extension + depends: + - ext-mysqlnd + php-extension: + arg-type: with + build-with-php: true +ext-mysqlnd: + type: php-extension + depends: + - zlib + php-extension: + arg-type@unix: enable + arg-type@windows: with + build-with-php: true ext-openssl: type: php-extension depends: From 13ab3e2b6c7b5b5683fc3bb6f87d94a8266a775f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 17:10:48 +0800 Subject: [PATCH 392/682] Fix transitive extension dependency not enabled bug --- src/Package/Target/php.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 484998106..38e2ad91b 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -216,14 +216,21 @@ public function resolveBuild(TargetPackage $package, PackageInstaller $installer } } - // Mark transitive PHP extension dependencies of static extensions as static too - if (!empty($static_extensions)) { - $static_ext_pkgs = array_map(fn ($x) => "ext-{$x}", $static_extensions); - $transitive_deps = DependencyResolver::resolve($static_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); + // Mark transitive PHP extension dependencies of static/shared extensions as static too. + // For static extensions: their ext deps must also be static. + // For shared extensions: their ext deps that are not themselves shared must be compiled + // into the static PHP build so their headers and symbols are available when linking the .so. + $all_input_ext_pkgs = array_map(fn ($x) => "ext-{$x}", array_values(array_unique([...$static_extensions, ...$shared_extensions]))); + if (!empty($all_input_ext_pkgs)) { + $transitive_deps = DependencyResolver::resolve($all_input_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); foreach ($transitive_deps as $dep_name) { if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { continue; } + $dep_extname = substr($dep_name, 4); + if (in_array($dep_extname, $shared_extensions)) { + continue; // already designated as shared + } $dep_instance = PackageLoader::getPackage($dep_name); if (!$dep_instance instanceof PhpExtensionPackage || $dep_instance->isBuildStatic() || $dep_instance->isBuildShared()) { continue; From e523fff0ab4d6bc98280a30cc494cfb5473b4757 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 17:12:27 +0800 Subject: [PATCH 393/682] Add ext-mysqlnd_ed25519 --- config/pkg/ext/ext-mysqlnd_ed25519.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 config/pkg/ext/ext-mysqlnd_ed25519.yml diff --git a/config/pkg/ext/ext-mysqlnd_ed25519.yml b/config/pkg/ext/ext-mysqlnd_ed25519.yml new file mode 100644 index 000000000..e7aa3de89 --- /dev/null +++ b/config/pkg/ext/ext-mysqlnd_ed25519.yml @@ -0,0 +1,18 @@ +ext-mysqlnd_ed25519: + type: php-extension + artifact: + source: + type: pie + repo: mariadb/mysqlnd_ed25519 + extract: php-src/ext/mysqlnd_ed25519 + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-mysqlnd + - libsodium + suggests: + - openssl + php-extension: + arg-type: '--with-mysqlnd_ed25519=@shared_suffix@' + build-static: false From 91ee94f3497c8c2546631823d5680b239e5130ba Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 14:19:38 +0800 Subject: [PATCH 394/682] Add ext-mongodb --- config/pkg/ext/ext-mongodb.yml | 21 +++++++++++++++++ src/Package/Extension/mongodb.php | 38 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 config/pkg/ext/ext-mongodb.yml create mode 100644 src/Package/Extension/mongodb.php diff --git a/config/pkg/ext/ext-mongodb.yml b/config/pkg/ext/ext-mongodb.yml new file mode 100644 index 000000000..7cbdbb140 --- /dev/null +++ b/config/pkg/ext/ext-mongodb.yml @@ -0,0 +1,21 @@ +ext-mongodb: + type: php-extension + artifact: + source: + type: ghrel + repo: mongodb/mongo-php-driver + match: mongodb.+\.tgz + extract: php-src/ext/mongodb + metadata: + license-files: [LICENSE] + license: PHP-3.01 + suggests: + - icu + - openssl + - zstd + - zlib + frameworks: + - CoreFoundation + - Security + php-extension: + arg-type: custom diff --git a/src/Package/Extension/mongodb.php b/src/Package/Extension/mongodb.php new file mode 100644 index 000000000..3434491d6 --- /dev/null +++ b/src/Package/Extension/mongodb.php @@ -0,0 +1,38 @@ +getLibraryPackage('openssl')) { + $arg .= '--with-mongodb-ssl=openssl'; + } + $arg .= $installer->getLibraryPackage('icu') ? ' --with-mongodb-icu=yes ' : ' --with-mongodb-icu=no '; + $arg .= $installer->getLibraryPackage('zstd') ? ' --with-mongodb-zstd=yes ' : ' --with-mongodb-zstd=no '; + // $arg .= $installer->getLibraryPackage('snappy') ? ' --with-mongodb-snappy=yes ' : ' --with-mongodb-snappy=no '; + $arg .= $installer->getLibraryPackage('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; + return clean_spaces($arg); + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17'; + return $parent; + } +} From 54f53fd1049f62e0759aea9115698f12c7090a42 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 14:29:05 +0800 Subject: [PATCH 395/682] Add ext-mysqlnd_parsec --- config/pkg/ext/ext-mysqlnd_parsec.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 config/pkg/ext/ext-mysqlnd_parsec.yml diff --git a/config/pkg/ext/ext-mysqlnd_parsec.yml b/config/pkg/ext/ext-mysqlnd_parsec.yml new file mode 100644 index 000000000..903d65c40 --- /dev/null +++ b/config/pkg/ext/ext-mysqlnd_parsec.yml @@ -0,0 +1,17 @@ +ext-mysqlnd_parsec: + type: php-extension + artifact: + source: + type: pie + repo: mariadb/mysqlnd_parsec + extract: php-src/ext/mysqlnd_parsec + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-mysqlnd + - libsodium + - openssl + php-extension: + arg-type: '--enable-mysqlnd_parsec' + build-static: false From c7f611fe80af3819e7f6cea74b7ca8c89df9d339 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 14:33:39 +0800 Subject: [PATCH 396/682] Add ext-odbc --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 00d1d5930..7a14dcefd 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -134,6 +134,12 @@ ext-mysqlnd: arg-type@unix: enable arg-type@windows: with build-with-php: true +ext-odbc: + type: php-extension + depends@unix: + - unixodbc + php-extension: + arg-type@unix: '--with-unixODBC@shared_path_suffix@' ext-openssl: type: php-extension depends: From 6f372a74a261564b40efe95f111e0a4ef65cc6ac Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 15:08:02 +0800 Subject: [PATCH 397/682] Remove check for php_micro.c file existence in SourcePatcher --- src/StaticPHP/Util/SourcePatcher.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 6a16f041f..b4e2e1c7b 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -209,9 +209,6 @@ public static function patchPhpSrc(?array $items = null): bool $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'); From 371a1af5724a23cfd46d1d75ae0ae0669430ec97 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 15:08:11 +0800 Subject: [PATCH 398/682] Add ext-opcache --- config/pkg/ext/builtin-extensions.yml | 7 +++ src/Package/Extension/opcache.php | 76 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/Package/Extension/opcache.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 7a14dcefd..da6ec1e94 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -140,6 +140,13 @@ ext-odbc: - unixodbc php-extension: arg-type@unix: '--with-unixODBC@shared_path_suffix@' +ext-opcache: + type: php-extension + php-extension: + arg-type@unix: custom + arg-type@windows: enable + zend-extension: true + display-name: 'Zend Opcache' ext-openssl: type: php-extension depends: diff --git a/src/Package/Extension/opcache.php b/src/Package/Extension/opcache.php new file mode 100644 index 000000000..93cb0a9ff --- /dev/null +++ b/src/Package/Extension/opcache.php @@ -0,0 +1,76 @@ += 8.0 !'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-opcache')] + #[PatchDescription('Fix static opcache build for PHP 8.2.0 to 8.4.x')] + public function patchBeforeBuildconf(PackageInstaller $installer): bool + { + $version = php::getPHPVersion(); + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + if (file_exists("{$php_src}/.opcache_patched")) { + return false; + } + // if 8.2.0 <= PHP_VERSION < 8.2.23, we need to patch from legacy patch file + if (version_compare($version, '8.2.0', '>=') && version_compare($version, '8.2.23', '<')) { + SourcePatcher::patchFile('spc_fix_static_opcache_before_80222.patch', $php_src); + } + // if 8.3.0 <= PHP_VERSION < 8.3.11, we need to patch from legacy patch file + elseif (version_compare($version, '8.3.0', '>=') && version_compare($version, '8.3.11', '<')) { + SourcePatcher::patchFile('spc_fix_static_opcache_before_80310.patch', $php_src); + } + // if 8.3.12 <= PHP_VERSION < 8.5.0-dev, we need to patch from legacy patch file + elseif (version_compare($version, '8.5.0-dev', '<')) { + SourcePatcher::patchPhpSrc(items: ['static_opcache']); + } + // PHP 8.5.0-dev and later supports static opcache without patching + else { + return false; + } + return file_put_contents($php_src . '/.opcache_patched', '1') !== false; + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string + { + $phpVersionID = php::getPHPVersionID(); + $opcache_jit = ' --enable-opcache-jit'; + if ((SystemTarget::getTargetOS() === 'Linux' && + SystemTarget::getLibc() === 'musl' && + $builder->getOption('enable-zts') && + SystemTarget::getTargetArch() === 'x86_64' && + $phpVersionID < 80500) || + $builder->getOption('disable-opcache-jit') + ) { + $opcache_jit = ' --disable-opcache-jit'; + } + return '--enable-opcache' . ($shared ? '=shared' : '') . $opcache_jit; + } +} From 528469514bc0412a86dd7112fcefb4645e9cc466 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 15:38:56 +0800 Subject: [PATCH 399/682] Add ext-opentelemetry --- config/pkg/ext/ext-opentelemetry.yml | 9 +++++++++ src/Package/Extension/opentelemetry.php | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 config/pkg/ext/ext-opentelemetry.yml create mode 100644 src/Package/Extension/opentelemetry.php diff --git a/config/pkg/ext/ext-opentelemetry.yml b/config/pkg/ext/ext-opentelemetry.yml new file mode 100644 index 000000000..5caebef2a --- /dev/null +++ b/config/pkg/ext/ext-opentelemetry.yml @@ -0,0 +1,9 @@ +ext-opentelemetry: + type: php-extension + artifact: + source: + type: pecl + name: opentelemetry + metadata: + license-files: [LICENSE] + license: Apache-2.0 diff --git a/src/Package/Extension/opentelemetry.php b/src/Package/Extension/opentelemetry.php new file mode 100644 index 000000000..632d12571 --- /dev/null +++ b/src/Package/Extension/opentelemetry.php @@ -0,0 +1,21 @@ + Date: Thu, 12 Mar 2026 16:04:25 +0800 Subject: [PATCH 400/682] Add ext-parallel --- config/pkg/ext/ext-parallel.yml | 9 ++++++++ src/Package/Extension/parallel.php | 35 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 config/pkg/ext/ext-parallel.yml create mode 100644 src/Package/Extension/parallel.php diff --git a/config/pkg/ext/ext-parallel.yml b/config/pkg/ext/ext-parallel.yml new file mode 100644 index 000000000..a3e91efe5 --- /dev/null +++ b/config/pkg/ext/ext-parallel.yml @@ -0,0 +1,9 @@ +ext-parallel: + type: php-extension + artifact: + source: + type: pecl + name: parallel + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/src/Package/Extension/parallel.php b/src/Package/Extension/parallel.php new file mode 100644 index 000000000..0ec585951 --- /dev/null +++ b/src/Package/Extension/parallel.php @@ -0,0 +1,35 @@ +getOption('enable-zts')) { + throw new WrongUsageException('ext-parallel must be built with ZTS builds. Use "--enable-zts" option!'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-parallel')] + #[PatchDescription('Fix parallel m4 hardcoded PHP_VERSION check')] + public function patchBeforeBuildconf(): bool + { + FileSystem::replaceFileRegex("{$this->getSourceDir()}/config.m4", '/PHP_VERSION=.*/m', ''); + return true; + } +} From cbc8feebfdfe0233178e6866b8f6eea494aea0eb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 16:54:01 +0800 Subject: [PATCH 401/682] Add patch for SPC_MICRO_PATCHES and update configure.ac handling --- src/Package/Target/php/unix.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 40961b5e1..de439acb0 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -32,10 +32,14 @@ trait unix { #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] + #[PatchDescription('Patch SPC_MICRO_PATCHES defined patches (e.g. cli_checks, disable_huge_page)')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] public function patchBeforeBuildconf(TargetPackage $package): void { + // php-src patches from micro (reads SPC_MICRO_PATCHES env var) + SourcePatcher::patchPhpSrc(); + // patch configure.ac for musl and musl-toolchain $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); @@ -47,6 +51,7 @@ public function patchBeforeBuildconf(TargetPackage $package): void // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) foreach (glob("{$package->getSourceDir()}/ext/*/*.m4") as $m4file) { FileSystem::replaceFileStr($m4file, 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); From 9713b7693558079543f1dd7ebe4ebcd29f5515b2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 17:09:59 +0800 Subject: [PATCH 402/682] Add patch to modify info.c for release builds to hide configure command --- src/Package/Target/php/unix.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index de439acb0..e636883e7 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -161,6 +161,25 @@ public function tryPatchMakefileUnix(): void shell()->cd(SOURCE_PATH . '/php-src')->exec('sed -i "s|//lib|/lib|g" Makefile'); } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch info.c to hide configure command in release builds')] + public function patchInfoCForRelease(): void + { + if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { + FileSystem::replaceFileLineContainsString( + SOURCE_PATH . '/php-src/ext/standard/info.c', + '#ifdef CONFIGURE_COMMAND', + '#ifdef NO_CONFIGURE_COMMAND', + ); + } else { + FileSystem::replaceFileLineContainsString( + SOURCE_PATH . '/php-src/ext/standard/info.c', + '#ifdef NO_CONFIGURE_COMMAND', + '#ifdef CONFIGURE_COMMAND', + ); + } + } + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { From 9d65c491e7e483dcef86951107e2595374cd520b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 21:56:28 +0800 Subject: [PATCH 403/682] Add ext-password-argon2 --- config/pkg/ext/builtin-extensions.yml | 8 +++++ src/Package/Extension/password_argon2.php | 37 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/Package/Extension/password_argon2.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 00d1d5930..76b20644f 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -144,6 +144,14 @@ ext-openssl: arg-type: custom arg-type@windows: with build-with-php: true +ext-password-argon2: + type: php-extension + depends: + - libargon2 + - ext-openssl + php-extension: + arg-type: custom + display-name: '' ext-phar: type: php-extension depends: diff --git a/src/Package/Extension/password_argon2.php b/src/Package/Extension/password_argon2.php new file mode 100644 index 000000000..77122405d --- /dev/null +++ b/src/Package/Extension/password_argon2.php @@ -0,0 +1,37 @@ +execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "assert(defined(\'PASSWORD_ARGON2I\'));"'); + if ($ret !== 0) { + throw new ValidationException('extension ' . $this->getName() . ' failed sanity check', validation_module: 'password_argon2 function check'); + } + } + + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string + { + if ($installer->getLibraryPackage('openssl') !== null) { + if (php::getPHPVersionID() >= 80500 || (php::getPHPVersionID() >= 80400 && !$builder->getOption('enable-zts'))) { + return '--without-password-argon2'; // use --with-openssl-argon2 in openssl extension instead + } + } + return '--with-password-argon2'; + } +} From 7a690fd9a3237fa104f8c2ce1d7b8685b771ad1f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:06:11 +0800 Subject: [PATCH 404/682] Add ext-pcov --- config/pkg/ext/ext-pcov.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-pcov.yml diff --git a/config/pkg/ext/ext-pcov.yml b/config/pkg/ext/ext-pcov.yml new file mode 100644 index 000000000..3fac61d08 --- /dev/null +++ b/config/pkg/ext/ext-pcov.yml @@ -0,0 +1,12 @@ +ext-pcov: + type: php-extension + artifact: + source: + type: pecl + name: pcov + metadata: + license-files: [LICENSE] + license: PHP-3.01 + php-extension: + build-static: false + build-shared: true From 74865025bd868f03a3788040f5b2847241aed683 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:06:42 +0800 Subject: [PATCH 405/682] Add ext-pcntl,ext-pdo,ext-pdo_mysql,ext-pdo_odbc --- config/pkg/ext/builtin-extensions.yml | 22 +++++++++++++++++ src/Package/Extension/pdo_odbc.php | 35 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/Package/Extension/pdo_odbc.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 2fdec855c..78d3d2825 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -165,6 +165,28 @@ ext-password-argon2: php-extension: arg-type: custom display-name: '' +ext-pcntl: + type: php-extension +ext-pdo: + type: php-extension +ext-pdo_mysql: + type: php-extension + depends: + - ext-pdo + - ext-mysqlnd + php-extension: + arg-type: with +ext-pdo_odbc: + type: php-extension + depends: + - ext-pdo + - ext-odbc + depends@unix: + - unixodbc + - ext-pdo + - ext-odbc + php-extension: + arg-type: custom ext-phar: type: php-extension depends: diff --git a/src/Package/Extension/pdo_odbc.php b/src/Package/Extension/pdo_odbc.php new file mode 100644 index 000000000..f8835d134 --- /dev/null +++ b/src/Package/Extension/pdo_odbc.php @@ -0,0 +1,35 @@ +getSourceDir()}/config.m4", 'PDO_ODBC_LDFLAGS="$pdo_odbc_def_ldflags', 'PDO_ODBC_LDFLAGS="-liconv $pdo_odbc_def_ldflags'); + } + + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getUnixConfigureArg(bool $shared): string + { + return '--with-pdo-odbc=' . ($shared ? 'shared,' : '') . 'unixODBC,' . BUILD_ROOT_PATH; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared): string + { + return '--with-pdo-odbc'; + } +} From f85f29e628ea5396d4de39e3303e5666b135a69a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:22:02 +0800 Subject: [PATCH 406/682] Add ext-pgsql,ext-pdo_pgsql --- config/pkg/ext/builtin-extensions.yml | 15 +++++++++ src/Package/Extension/pgsql.php | 48 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/Package/Extension/pgsql.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 78d3d2825..33287bd82 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -187,6 +187,21 @@ ext-pdo_odbc: - ext-odbc php-extension: arg-type: custom +ext-pdo_pgsql: + type: php-extension + depends@unix: + - ext-pdo + - ext-pgsql + - postgresql + php-extension: + arg-type@unix: with-path + arg-type@windows: '--with-pdo-pgsql=yes' +ext-pgsql: + type: php-extension + depends@unix: + - postgresql + php-extension: + arg-type: custom ext-phar: type: php-extension depends: diff --git a/src/Package/Extension/pgsql.php b/src/Package/Extension/pgsql.php new file mode 100644 index 000000000..6e2b8f0b5 --- /dev/null +++ b/src/Package/Extension/pgsql.php @@ -0,0 +1,48 @@ += 80400) { + $libfiles = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true])->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), $builder->getOption('with-suggests'))['libs']; + $libfiles = str_replace("{$builder->getLibDir()}/lib", '-l', $libfiles); + $libfiles = str_replace('.a', '', $libfiles); + return '--with-pgsql' . ($shared ? '=shared' : '') . + ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libfiles . '"'; + } + return '--with-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared, PackageBuilder $builder): string + { + if (php::getPHPVersionID() >= 80400) { + return '--with-pgsql'; + } + return "--with-pgsql={$builder->getBuildRootPath()}"; + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17 -Wno-int-conversion'; + return $parent; + } +} From 6af55323b34f1ff7ef2c4cb9b29422e550518351 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:27:42 +0800 Subject: [PATCH 407/682] Add ext-sqlite3,ext-pdo_sqlite --- config/pkg/ext/builtin-extensions.yml | 16 ++++++++++++++++ src/Package/Extension/pdo_sqlite.php | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/Package/Extension/pdo_sqlite.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 33287bd82..6d239633c 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -196,6 +196,14 @@ ext-pdo_pgsql: php-extension: arg-type@unix: with-path arg-type@windows: '--with-pdo-pgsql=yes' +ext-pdo_sqlite: + type: php-extension + depends: + - ext-pdo + - ext-sqlite3 + - sqlite + php-extension: + arg-type: with ext-pgsql: type: php-extension depends@unix: @@ -221,6 +229,14 @@ ext-session: type: php-extension ext-sockets: type: php-extension +ext-sqlite3: + type: php-extension + depends: + - sqlite + php-extension: + arg-type@unix: with-path + arg-type@windows: with + build-with-php: true ext-xml: type: php-extension depends: diff --git a/src/Package/Extension/pdo_sqlite.php b/src/Package/Extension/pdo_sqlite.php new file mode 100644 index 000000000..b0429f628 --- /dev/null +++ b/src/Package/Extension/pdo_sqlite.php @@ -0,0 +1,25 @@ +getTargetPackage('php')->getSourceDir()}/configure", + '/sqlite3_column_table_name=yes/', + 'sqlite3_column_table_name=no' + ); + } +} From 54e301d55cbdd542648e3b3941aebc8d2a0b0e13 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:32:36 +0800 Subject: [PATCH 408/682] Add ext-sqlsrv,ext-pdo_sqlsrv --- config/pkg/ext/ext-pdo_sqlsrv.yml | 14 ++++++++++++++ config/pkg/ext/ext-sqlsrv.yml | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 config/pkg/ext/ext-pdo_sqlsrv.yml create mode 100644 config/pkg/ext/ext-sqlsrv.yml diff --git a/config/pkg/ext/ext-pdo_sqlsrv.yml b/config/pkg/ext/ext-pdo_sqlsrv.yml new file mode 100644 index 000000000..6d57333b3 --- /dev/null +++ b/config/pkg/ext/ext-pdo_sqlsrv.yml @@ -0,0 +1,14 @@ +ext-pdo_sqlsrv: + type: php-extension + artifact: + source: + type: pecl + name: pdo_sqlsrv + metadata: + license-files: [LICENSE] + license: MIT + depends: + - ext-pdo + - ext-sqlsrv + php-extension: + arg-type: with diff --git a/config/pkg/ext/ext-sqlsrv.yml b/config/pkg/ext/ext-sqlsrv.yml new file mode 100644 index 000000000..603d7a93a --- /dev/null +++ b/config/pkg/ext/ext-sqlsrv.yml @@ -0,0 +1,15 @@ +ext-sqlsrv: + type: php-extension + artifact: + source: + type: pecl + name: sqlsrv + metadata: + license-files: [LICENSE] + license: MIT + depends@linux: + - unixodbc + - ext-pcntl + depends@macos: + - unixodbc + lang: cpp From 63d28bdc014423379e3baab80a72388e83ac36cf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:33:28 +0800 Subject: [PATCH 409/682] Add ext-posix --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 6d239633c..bdb975071 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -214,6 +214,8 @@ ext-phar: type: php-extension depends: - zlib +ext-posix: + type: php-extension ext-readline: type: php-extension depends: From 067749ab1b1b0255b1c22f5bacd03a4715859f27 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:44:57 +0800 Subject: [PATCH 410/682] Add ext-protobuf --- config/pkg/ext/ext-protobuf.yml | 9 +++++++++ src/Package/Extension/protobuf.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/ext/ext-protobuf.yml create mode 100644 src/Package/Extension/protobuf.php diff --git a/config/pkg/ext/ext-protobuf.yml b/config/pkg/ext/ext-protobuf.yml new file mode 100644 index 000000000..020059d39 --- /dev/null +++ b/config/pkg/ext/ext-protobuf.yml @@ -0,0 +1,9 @@ +ext-protobuf: + type: php-extension + artifact: + source: + type: pecl + name: protobuf + metadata: + license-files: [LICENSE] + license: BSD-3-Clause diff --git a/src/Package/Extension/protobuf.php b/src/Package/Extension/protobuf.php new file mode 100644 index 000000000..2c3dd036a --- /dev/null +++ b/src/Package/Extension/protobuf.php @@ -0,0 +1,28 @@ +getPhpExtensionPackage('ext-grpc'); + // protobuf conflicts with grpc + if ($grpc?->isBuildStatic()) { + throw new ValidationException('protobuf conflicts with grpc, please remove grpc or protobuf extension'); + } + } +} From a288533fc372787db479f8f13b0fccd5f52148bf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:58:59 +0800 Subject: [PATCH 411/682] Add ext-rar --- config/pkg/ext/ext-rar.yml | 12 ++++++++++++ src/Package/Extension/rar.php | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 config/pkg/ext/ext-rar.yml create mode 100644 src/Package/Extension/rar.php diff --git a/config/pkg/ext/ext-rar.yml b/config/pkg/ext/ext-rar.yml new file mode 100644 index 000000000..1770788a6 --- /dev/null +++ b/config/pkg/ext/ext-rar.yml @@ -0,0 +1,12 @@ +ext-rar: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/static-php/php-rar.git' + rev: issue-php82 + extract: php-src/ext/rar + metadata: + license-files: [LICENSE] + license: PHP-3.01 + lang: cpp diff --git a/src/Package/Extension/rar.php b/src/Package/Extension/rar.php new file mode 100644 index 000000000..2fc20ed14 --- /dev/null +++ b/src/Package/Extension/rar.php @@ -0,0 +1,27 @@ += 15.0)')] + public function patchBeforeBuildconf(): void + { + // workaround for newer Xcode clang (>= 15.0) + if (SystemTarget::getTargetOS() === 'Darwin') { + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", '-Wall -fvisibility=hidden', '-Wall -Wno-incompatible-function-pointer-types -fvisibility=hidden'); + } + } +} From 935fbbd31a7ca157c15f319dda23f86ab9d85297 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 13 Mar 2026 10:14:05 +0800 Subject: [PATCH 412/682] Add ext-rdkafka --- config/pkg/ext/ext-rdkafka.yml | 15 +++++++++ src/Package/Extension/rdkafka.php | 55 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 config/pkg/ext/ext-rdkafka.yml create mode 100644 src/Package/Extension/rdkafka.php diff --git a/config/pkg/ext/ext-rdkafka.yml b/config/pkg/ext/ext-rdkafka.yml new file mode 100644 index 000000000..1f26e49cb --- /dev/null +++ b/config/pkg/ext/ext-rdkafka.yml @@ -0,0 +1,15 @@ +ext-rdkafka: + type: php-extension + artifact: + source: + type: ghtar + repo: arnaud-lb/php-rdkafka + extract: php-src/ext/rdkafka + metadata: + license-files: [LICENSE] + license: MIT + depends: + - librdkafka + lang: cpp + php-extension: + arg-type: custom diff --git a/src/Package/Extension/rdkafka.php b/src/Package/Extension/rdkafka.php new file mode 100644 index 000000000..4bb28ee57 --- /dev/null +++ b/src/Package/Extension/rdkafka.php @@ -0,0 +1,55 @@ +getSourceDir()}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\n", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm \$RDKAFKA_LIBS\n"); + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\"\n", '-L$RDKAFKA_DIR/$PHP_LIBDIR -lm $RDKAFKA_LIBS"'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", 'PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,', 'AC_CHECK_LIB([$LIBNAME], [$LIBSYMBOL],'); + return true; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-rdkafka')] + #[PatchDescription('Patch rdkafka extension source code to fix build errors with inline builds')] + public function patchBeforeMake(): bool + { + // when compiling rdkafka with inline builds, it shows some errors, I don't know why. + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/rdkafka.c", + "#ifdef HAS_RD_KAFKA_TRANSACTIONS\n#include \"kafka_error_exception.h\"\n#endif", + '#include "kafka_error_exception.h"' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/kafka_error_exception.h", + ['#ifdef HAS_RD_KAFKA_TRANSACTIONS', '#endif'], + '' + ); + return true; + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string + { + $pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->getExtensionConfig($this); + return '--with-rdkafka=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; + } +} From 6ed620683f9099d9b3ba7ce9b2f1464eaad73f79 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 13 Mar 2026 16:53:28 +0800 Subject: [PATCH 413/682] Add ext-redis --- config/pkg/ext/ext-redis.yml | 21 +++++++++++++++ src/Package/Extension/redis.php | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 config/pkg/ext/ext-redis.yml create mode 100644 src/Package/Extension/redis.php diff --git a/config/pkg/ext/ext-redis.yml b/config/pkg/ext/ext-redis.yml new file mode 100644 index 000000000..c05b4ee26 --- /dev/null +++ b/config/pkg/ext/ext-redis.yml @@ -0,0 +1,21 @@ +ext-redis: + type: php-extension + artifact: + source: + type: pecl + name: redis + metadata: + license-files: [LICENSE] + license: PHP-3.01 + suggests: + - ext-session + - ext-igbinary + - ext-msgpack + suggests@unix: + - ext-session + - ext-igbinary + - ext-msgpack + - zstd + - liblz4 + php-extension: + arg-type: custom diff --git a/src/Package/Extension/redis.php b/src/Package/Extension/redis.php new file mode 100644 index 000000000..bfc5cc5e7 --- /dev/null +++ b/src/Package/Extension/redis.php @@ -0,0 +1,47 @@ +isBuildStatic()) { + $arg .= $installer->getPhpExtensionPackage('session')?->isBuildStatic() ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary')?->isBuildStatic() ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + $arg .= $installer->getPhpExtensionPackage('msgpack')?->isBuildStatic() ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; + } else { + $arg .= $installer->getPhpExtensionPackage('session') ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + $arg .= $installer->getPhpExtensionPackage('msgpack') ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; + } + if ($zstd = $installer->getLibraryPackage('zstd')) { + $arg .= ' --enable-redis-zstd --with-libzstd="' . $zstd->getBuildRootPath() . '"'; + } + if ($liblz4 = $installer->getLibraryPackage('liblz4')) { + $arg .= ' --enable-redis-lz4 --with-liblz4="' . $liblz4->getBuildRootPath() . '"'; + } + return $arg; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared, PackageInstaller $installer): string + { + $arg = '--enable-redis'; + $arg .= $installer->getPhpExtensionPackage('session') ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + return $arg; + } +} From 271013f2d651148c72a90f09948840e27e519abe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 13 Mar 2026 17:02:35 +0800 Subject: [PATCH 414/682] Add ext-shmop, fix path slashes --- config/pkg/ext/builtin-extensions.yml | 4 ++++ src/StaticPHP/Config/PackageConfig.php | 1 + src/StaticPHP/Util/FileSystem.php | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index bdb975071..9b39ae8f9 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -229,6 +229,10 @@ ext-readline: build-static: true ext-session: type: php-extension +ext-shmop: + type: php-extension + php-extension: + build-with-php: true ext-sockets: type: php-extension ext-sqlite3: diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index c4f22a528..0e2d0af1c 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -23,6 +23,7 @@ public static function loadFromDir(string $dir, string $registry_name): array if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); } + $dir = rtrim($dir, '/'); $loaded = []; $files = FileSystem::scanDirFiles($dir, false); if (is_array($files)) { diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index c8da5353d..3015b4891 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -481,7 +481,7 @@ public static function replaceFileLineContainsString(string $file, string $find, public static function fullpath(string $path, string $relative_path_base): string { if (FileSystem::isRelativePath($path)) { - $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; + $path = rtrim($relative_path_base, '/') . DIRECTORY_SEPARATOR . $path; } if (!file_exists($path)) { throw new FileSystemException("Path does not exist: {$path}"); From e30a10f60f9c2729fdb858dd713beb0767d420cf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:04:35 +0800 Subject: [PATCH 415/682] Add ext-simdjson, add SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS env var --- config/env.ini | 6 +++ config/pkg/ext/ext-simdjson.yml | 10 +++++ src/Package/Extension/simdjson.php | 70 ++++++++++++++++++++++++++++++ src/Package/Target/php/unix.php | 1 + 4 files changed, 87 insertions(+) create mode 100644 config/pkg/ext/ext-simdjson.yml create mode 100644 src/Package/Extension/simdjson.php diff --git a/config/env.ini b/config/env.ini index 9e295d796..3143efaf5 100644 --- a/config/env.ini +++ b/config/env.ini @@ -121,6 +121,8 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --disable SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_CXXFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_CXX_FLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" @@ -155,5 +157,9 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable- SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_CXXFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_CXX_FLAGS}" +; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.dylib +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" ; minimum compatible macOS version (LLVM vars, availability not guaranteed) MACOSX_DEPLOYMENT_TARGET=12.0 diff --git a/config/pkg/ext/ext-simdjson.yml b/config/pkg/ext/ext-simdjson.yml new file mode 100644 index 000000000..37eeb5f1a --- /dev/null +++ b/config/pkg/ext/ext-simdjson.yml @@ -0,0 +1,10 @@ +ext-simdjson: + type: php-extension + artifact: + source: + type: pecl + name: simdjson + metadata: + license-files: [LICENSE] + license: Apache-2.0 + lang: cpp diff --git a/src/Package/Extension/simdjson.php b/src/Package/Extension/simdjson.php new file mode 100644 index 000000000..e04c415ac --- /dev/null +++ b/src/Package/Extension/simdjson.php @@ -0,0 +1,70 @@ +getTargetPackage('php'); + $php_ver = php::getPHPVersionID(); + FileSystem::replaceFileRegex( + "{$this->getSourceDir()}/config.m4", + '/php_version=(`.*`)$/m', + "php_version={$php_ver}" + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + 'if test -z "$PHP_CONFIG"; then', + 'if false; then' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.w32", + "'yes',", + 'PHP_SIMDJSON_SHARED,' + ); + return true; + } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + $extra = getenv('SPC_COMPILER_EXTRA'); + if (!str_contains((string) $extra, '-lstdc++')) { + f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++')); + } + $env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + $env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + } + return $env; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-simdjson')] + public function patchBeforeMake(): void + { + if (!ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + return; + } + $extra_cflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags . ' -Xclang -target-feature -Xclang +evex512')); + $extra_cxxflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim($extra_cxxflags . ' -Xclang -target-feature -Xclang +evex512')); + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index e636883e7..f16d879f6 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -766,6 +766,7 @@ private function makeVars(PackageInstaller $installer): array return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'EXTRA_CXXFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}", 'EXTRA_LDFLAGS' => $config['ldflags'], 'EXTRA_LIBS' => $libs, From fe302bf8b9059136ac0de4dd80df8407980bf4df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:17:15 +0800 Subject: [PATCH 416/682] Add ext-simplexml --- config/pkg/ext/builtin-extensions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 9b39ae8f9..c10ee59e1 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -233,6 +233,14 @@ ext-shmop: type: php-extension php-extension: build-with-php: true +ext-simplexml: + type: php-extension + depends: + - ext-xml + php-extension: + arg-type@unix: '--enable-simplexml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true ext-sockets: type: php-extension ext-sqlite3: From 15e7678615617c49fb821c23da857eff4f2d1d36 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:17:53 +0800 Subject: [PATCH 417/682] Add missing xml-related patches for windows --- src/Package/Target/php/windows.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index e77b88cd1..74e746e2b 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -22,6 +22,13 @@ trait windows { + #[BeforeStage('php', [self::class, 'buildconfForWindows'])] + #[PatchDescription('Patch for fixing win32 xml related extensions builds')] + public function beforeBuildconfWin(TargetPackage $package): void + { + FileSystem::replaceFileStr("{$package->getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); + } + #[Stage] public function buildconfForWindows(TargetPackage $package): void { From 1670b61ed7492ddb679b6c3139ebd767c2699c2a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:26:53 +0800 Subject: [PATCH 418/682] Add ext-snappy --- config/pkg/ext/ext-snappy.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 config/pkg/ext/ext-snappy.yml diff --git a/config/pkg/ext/ext-snappy.yml b/config/pkg/ext/ext-snappy.yml new file mode 100644 index 000000000..7ddec2618 --- /dev/null +++ b/config/pkg/ext/ext-snappy.yml @@ -0,0 +1,18 @@ +ext-snappy: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/kjdev/php-ext-snappy' + rev: master + extract: php-src/ext/snappy + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - snappy + suggests: + - ext-apcu + lang: cpp + php-extension: + arg-type@unix: '--enable-snappy --with-snappy-includedir=@build_root_path@' From 3f812fe5fcf1bc9f8c685502b2d86b464d11d088 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:48:26 +0800 Subject: [PATCH 419/682] Fix filename generation for GitHub tarballs to handle missing tag names --- src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 61517a9e0..e473c0caf 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -61,7 +61,7 @@ public function getGitHubTarballInfo(string $name, string $repo, string $rel_typ $filename = $matches['filename']; } else { $basename = $basename ?? basename($repo); - $filename = "{$basename}-" . ($rel_type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz'; + $filename = "{$basename}-" . ($rel_type === 'releases' ? ($data['tag_name'] ?? $data['name']) : $data['name']) . '.tar.gz'; } return [$url, $filename]; } From 21e2a0194c6d0db98371c7fa2c9d21beba7c03df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:48:50 +0800 Subject: [PATCH 420/682] Add ext-snmp --- config/pkg/ext/builtin-extensions.yml | 6 +++++ src/Package/Extension/snmp.php | 34 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/Package/Extension/snmp.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index c10ee59e1..62c225fe8 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -241,6 +241,12 @@ ext-simplexml: arg-type@unix: '--enable-simplexml@shared_suffix@ --with-libxml=@build_root_path@' arg-type@windows: with build-with-php: true +ext-snmp: + type: php-extension + depends: + - net-snmp + php-extension: + arg-type: with ext-sockets: type: php-extension ext-sqlite3: diff --git a/src/Package/Extension/snmp.php b/src/Package/Extension/snmp.php new file mode 100644 index 000000000..d161c6026 --- /dev/null +++ b/src/Package/Extension/snmp.php @@ -0,0 +1,34 @@ +getSourceDir()}/config.m4"); + } + $libs = implode(' ', PkgConfigUtil::getLibsArray('netsnmp')); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + 'PHP_EVAL_LIBLINE([$SNMP_LIBS], [SNMP_SHARED_LIBADD])', + "SNMP_LIBS=\"{$libs}\"\nPHP_EVAL_LIBLINE([\$SNMP_LIBS], [SNMP_SHARED_LIBADD])" + ); + return true; + } +} From ba253ea2a593fcb43bb7cce9500aff4ed22f7b79 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:57:43 +0800 Subject: [PATCH 421/682] Add ext-soap --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 62c225fe8..51a02423d 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -247,6 +247,15 @@ ext-snmp: - net-snmp php-extension: arg-type: with +ext-soap: + type: php-extension + depends: + - ext-xml + - ext-session + php-extension: + arg-type@unix: '--enable-soap@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true ext-sockets: type: php-extension ext-sqlite3: From d79128cdbf246b4ba1ac7d080ffc01860433abba Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:57:51 +0800 Subject: [PATCH 422/682] Add ext-sodium --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 51a02423d..34c6f8bf7 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -258,6 +258,12 @@ ext-soap: build-with-php: true ext-sockets: type: php-extension +ext-sodium: + type: php-extension + depends: + - libsodium + php-extension: + arg-type: with ext-sqlite3: type: php-extension depends: From 65c3263b25427eb6f71a1b32545899c6f8276de2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:59:27 +0800 Subject: [PATCH 423/682] Add ext-spx --- config/pkg/ext/ext-spx.yml | 14 ++++++++++ src/Package/Extension/spx.php | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 config/pkg/ext/ext-spx.yml create mode 100644 src/Package/Extension/spx.php diff --git a/config/pkg/ext/ext-spx.yml b/config/pkg/ext/ext-spx.yml new file mode 100644 index 000000000..a379cdd4d --- /dev/null +++ b/config/pkg/ext/ext-spx.yml @@ -0,0 +1,14 @@ +ext-spx: + type: php-extension + artifact: + source: + type: pie + repo: noisebynorthwest/php-spx + extract: php-src/ext/spx + metadata: + license-files: [LICENSE] + license: GPL-3.0-or-later + depends: + - ext-zlib + php-extension: + arg-type: '--enable-SPX@shared_suffix@' diff --git a/src/Package/Extension/spx.php b/src/Package/Extension/spx.php new file mode 100644 index 000000000..bb230ec94 --- /dev/null +++ b/src/Package/Extension/spx.php @@ -0,0 +1,52 @@ +getSourceDir()}/config.m4", + 'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"', + 'CFLAGS="$CFLAGS -pthread"' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/src/php_spx.h", + "extern zend_module_entry spx_module_entry;\n", + "extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n" + ); + FileSystem::copy("{$this->getSourceDir()}/src/php_spx.h", "{$this->getSourceDir()}/php_spx.h"); + return true; + } + + #[BeforeStage('php', [php::class, 'configureForUnix'], 'ext-spx')] + #[PatchDescription('Fix spx extension compile error when configuring')] + public function patchBeforeConfigure(): void + { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/Makefile.frag", + '@cp -r assets/web-ui/*', + "@cp -r {$this->getSourceDir()}/assets/web-ui/*", + ); + } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + $env['SPX_SHARED_LIBADD'] = $env['LIBS']; + return $env; + } +} From b89e941ab26390eb8cdcfd20d43e0e285a423438 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:24:57 +0800 Subject: [PATCH 424/682] Add ext-ssh2 --- config/pkg/ext/ext-ssh2.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-ssh2.yml diff --git a/config/pkg/ext/ext-ssh2.yml b/config/pkg/ext/ext-ssh2.yml new file mode 100644 index 000000000..14c9bf327 --- /dev/null +++ b/config/pkg/ext/ext-ssh2.yml @@ -0,0 +1,15 @@ +ext-ssh2: + type: php-extension + artifact: + source: + type: pecl + name: ssh2 + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libssh2 + - ext-openssl + - ext-zlib + php-extension: + arg-type: with-path From 02d40d197b0645059d2b4c4047e25dcdec22da3a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:25:18 +0800 Subject: [PATCH 425/682] Add ext-sysvmsg,ext-sysvsem,ext-sysvshm --- config/pkg/ext/builtin-extensions.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 34c6f8bf7..3f146687b 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -272,6 +272,23 @@ ext-sqlite3: arg-type@unix: with-path arg-type@windows: with build-with-php: true +ext-sysvmsg: + type: php-extension + php-extension: + support: + Windows: 'no' + BSD: wip +ext-sysvsem: + type: php-extension + php-extension: + support: + Windows: 'no' + BSD: wip +ext-sysvshm: + type: php-extension + php-extension: + support: + BSD: wip ext-xml: type: php-extension depends: From 170371abf754289773b0e9d43bae05c05f969f1e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:27:16 +0800 Subject: [PATCH 426/682] Add ext-tidy --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 3f146687b..f55cf6314 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -289,6 +289,15 @@ ext-sysvshm: php-extension: support: BSD: wip +ext-tidy: + type: php-extension + depends: + - tidy + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path ext-xml: type: php-extension depends: From bfb6fcd436bb3ac256c0d027e2ea7679e6ebb70f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:27:47 +0800 Subject: [PATCH 427/682] Add ext-tokenizer --- config/pkg/ext/builtin-extensions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index f55cf6314..65da4295f 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -298,6 +298,10 @@ ext-tidy: Windows: wip BSD: wip arg-type: with-path +ext-tokenizer: + type: php-extension + php-extension: + build-with-php: true ext-xml: type: php-extension depends: From deef11c86a9c7fcae76532186a17928404392a60 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 12:27:14 +0800 Subject: [PATCH 428/682] Add ext-trader --- config/pkg/ext/ext-trader.yml | 14 ++++++++++++++ src/Package/Extension/trader.php | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/ext/ext-trader.yml create mode 100644 src/Package/Extension/trader.php diff --git a/config/pkg/ext/ext-trader.yml b/config/pkg/ext/ext-trader.yml new file mode 100644 index 000000000..8e16afbbe --- /dev/null +++ b/config/pkg/ext/ext-trader.yml @@ -0,0 +1,14 @@ +ext-trader: + type: php-extension + artifact: + source: + type: pecl + name: trader + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + php-extension: + support: + BSD: wip + Windows: wip + arg-type: enable diff --git a/src/Package/Extension/trader.php b/src/Package/Extension/trader.php new file mode 100644 index 000000000..546b073a1 --- /dev/null +++ b/src/Package/Extension/trader.php @@ -0,0 +1,23 @@ +getSourceDir()}/config.m4", 'PHP_TA', 'PHP_TRADER'); + return true; + } +} From 25bec6b9747a7c98a2b95081f7e077e3fd828e3c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 12:30:02 +0800 Subject: [PATCH 429/682] Add ext-uuid --- config/pkg/ext/ext-uuid.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/pkg/ext/ext-uuid.yml diff --git a/config/pkg/ext/ext-uuid.yml b/config/pkg/ext/ext-uuid.yml new file mode 100644 index 000000000..68080531d --- /dev/null +++ b/config/pkg/ext/ext-uuid.yml @@ -0,0 +1,16 @@ +ext-uuid: + type: php-extension + artifact: + source: + type: pecl + name: uuid + metadata: + license-files: [LICENSE] + license: LGPL-2.1-only + depends: + - libuuid + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path From 22c5403e98d6b576dd21e8f7cd919d8a2141b159 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 12:57:43 +0800 Subject: [PATCH 430/682] Allow unstable for PECL downloads --- src/StaticPHP/Artifact/Downloader/Type/PECL.php | 2 +- src/StaticPHP/Config/ConfigValidator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php index 78ceed3a8..df2da341f 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PECL.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -54,7 +54,7 @@ protected function fetchPECLInfo(string $name, array $config, ArtifactDownloader $versions = []; logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL"); foreach ($matches['version'] as $i => $version) { - if ($matches['state'][$i] !== 'stable') { + if ($matches['state'][$i] !== 'stable' && ($config['prefer-stable'] ?? true) === true) { continue; } $versions[$version] = $peclName . '-' . $version . '.tgz'; diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 4a4f75db6..256e47fd0 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -91,7 +91,7 @@ class ConfigValidator 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], - 'pecl' => [['name'], ['extract']], + 'pecl' => [['name'], ['extract', 'prefer-stable']], 'php-release' => [['domain'], ['extract']], 'custom' => [[], ['func']], ]; From 2327f32e4176492eaea7199d70d840fdef3886bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 13:04:19 +0800 Subject: [PATCH 431/682] Add ext-uv --- config/pkg/ext/ext-uv.yml | 18 ++++++++++++++++++ src/Package/Extension/uv.php | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 config/pkg/ext/ext-uv.yml create mode 100644 src/Package/Extension/uv.php diff --git a/config/pkg/ext/ext-uv.yml b/config/pkg/ext/ext-uv.yml new file mode 100644 index 000000000..f1a3031bf --- /dev/null +++ b/config/pkg/ext/ext-uv.yml @@ -0,0 +1,18 @@ +ext-uv: + type: php-extension + artifact: + source: + type: pecl + name: uv + prefer-stable: false + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libuv + - ext-sockets + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path diff --git a/src/Package/Extension/uv.php b/src/Package/Extension/uv.php new file mode 100644 index 000000000..869f4ad99 --- /dev/null +++ b/src/Package/Extension/uv.php @@ -0,0 +1,36 @@ +getSourceDir()}/Makefile", '/^(LDFLAGS =.*)$/m', '$1 -luv -ldl -lrt -pthread'); + return true; + } +} From 20b693d1fa4678b88faac1ccb724bbe192c0e4b8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 13:16:47 +0800 Subject: [PATCH 432/682] Add ext-xdebug --- config/pkg/ext/ext-xdebug.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 config/pkg/ext/ext-xdebug.yml diff --git a/config/pkg/ext/ext-xdebug.yml b/config/pkg/ext/ext-xdebug.yml new file mode 100644 index 000000000..0374e573b --- /dev/null +++ b/config/pkg/ext/ext-xdebug.yml @@ -0,0 +1,14 @@ +ext-xdebug: + type: php-extension + artifact: + source: + type: pie + repo: xdebug/xdebug + metadata: + license-files: [LICENSE] + license: Xdebug-1.03 + php-extension: + zend-extension: true + build-static: false + build-shared: true + build-with-php: false From ca15ccd4d15ffc03e077fc0c82af62b4c49ab958 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 13:27:37 +0800 Subject: [PATCH 433/682] Add ext-xhprof --- config/pkg/ext/ext-xhprof.yml | 18 ++++++++++++++++ src/Package/Extension/xhprof.php | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 config/pkg/ext/ext-xhprof.yml create mode 100644 src/Package/Extension/xhprof.php diff --git a/config/pkg/ext/ext-xhprof.yml b/config/pkg/ext/ext-xhprof.yml new file mode 100644 index 000000000..b075f65bd --- /dev/null +++ b/config/pkg/ext/ext-xhprof.yml @@ -0,0 +1,18 @@ +ext-xhprof: + type: php-extension + artifact: + source: + type: pecl + name: xhprof + extract: php-src/ext/xhprof-src + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - ext-ctype + php-extension: + support: + Windows: wip + BSD: wip + arg-type: enable + build-with-php: true diff --git a/src/Package/Extension/xhprof.php b/src/Package/Extension/xhprof.php new file mode 100644 index 000000000..91c23facf --- /dev/null +++ b/src/Package/Extension/xhprof.php @@ -0,0 +1,35 @@ +getTargetPackage('php')->getSourceDir(); + $link = "{$php_src}/ext/xhprof"; + if (!is_link($link)) { + shell()->cd("{$php_src}/ext")->exec('ln -s xhprof-src/extension xhprof'); + + // patch config.m4 + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/extension/config.m4", + 'if test -f $phpincludedir/ext/pcre/php_pcre.h; then', + 'if test -f $abs_srcdir/ext/pcre/php_pcre.h; then' + ); + return true; + } + return false; + } +} From 5d309ee998b40e4f817f27a478e5854309a79667 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:13:47 +0800 Subject: [PATCH 434/682] Add ext-zip --- config/pkg/ext/ext-zip.yml | 17 +++++++++++++++++ src/Package/Extension/zip.php | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/ext/ext-zip.yml create mode 100644 src/Package/Extension/zip.php diff --git a/config/pkg/ext/ext-zip.yml b/config/pkg/ext/ext-zip.yml new file mode 100644 index 000000000..a5a9e4b54 --- /dev/null +++ b/config/pkg/ext/ext-zip.yml @@ -0,0 +1,17 @@ +ext-zip: + type: php-extension + artifact: + source: + type: pecl + name: zip + extract: ext-zip + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends@unix: + - libzip + php-extension: + support: + BSD: wip + arg-type: custom + arg-type@windows: enable diff --git a/src/Package/Extension/zip.php b/src/Package/Extension/zip.php new file mode 100644 index 000000000..dc2d29c56 --- /dev/null +++ b/src/Package/Extension/zip.php @@ -0,0 +1,20 @@ + Date: Tue, 17 Mar 2026 15:13:59 +0800 Subject: [PATCH 435/682] Add ext-xlswriter --- config/pkg/ext/ext-xlswriter.yml | 18 ++++++++++++++++++ src/Package/Extension/xlswriter.php | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 config/pkg/ext/ext-xlswriter.yml create mode 100644 src/Package/Extension/xlswriter.php diff --git a/config/pkg/ext/ext-xlswriter.yml b/config/pkg/ext/ext-xlswriter.yml new file mode 100644 index 000000000..24d2fa3ce --- /dev/null +++ b/config/pkg/ext/ext-xlswriter.yml @@ -0,0 +1,18 @@ +ext-xlswriter: + type: php-extension + artifact: + source: + type: pecl + name: xlswriter + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + depends: + - ext-zlib + - ext-zip + suggests: + - openssl + php-extension: + support: + BSD: wip + arg-type: custom diff --git a/src/Package/Extension/xlswriter.php b/src/Package/Extension/xlswriter.php new file mode 100644 index 000000000..b2f25716e --- /dev/null +++ b/src/Package/Extension/xlswriter.php @@ -0,0 +1,25 @@ +getLibraryPackage('openssl')) { + $arg .= ' --with-openssl=' . $installer->getLibraryPackage('openssl')->getBuildRootPath(); + } + return $arg; + } +} From 63bee0db1399403e79bf4f7d7b0106d92c5b75fc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:21:16 +0800 Subject: [PATCH 436/682] Add ext-xmlwriter,ext-xmlreader --- config/pkg/ext/builtin-extensions.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 65da4295f..f7b322fde 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -313,6 +313,20 @@ ext-xml: arg-type: '--enable-xml@shared_suffix@ --with-libxml=@build_root_path@' arg-type@windows: with build-with-php: true +ext-xmlreader: + type: php-extension + depends: + - libxml2 + php-extension: + arg-type: '--enable-xmlreader@shared_suffix@ --with-libxml=@build_root_path@' + build-with-php: true +ext-xmlwriter: + type: php-extension + depends: + - libxml2 + php-extension: + arg-type: '--enable-xmlwriter@shared_suffix@ --with-libxml=@build_root_path@' + build-with-php: true ext-zlib: type: php-extension depends: From 0101e6c52b5f8b8f1655e61a14bd0a858e72d9df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:23:20 +0800 Subject: [PATCH 437/682] Add ext-xsl --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index f7b322fde..b938182c0 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -327,6 +327,15 @@ ext-xmlwriter: php-extension: arg-type: '--enable-xmlwriter@shared_suffix@ --with-libxml=@build_root_path@' build-with-php: true +ext-xsl: + type: php-extension + depends: + - libxslt + - ext-xml + - ext-dom + php-extension: + arg-type: with-path + build-with-php: true ext-zlib: type: php-extension depends: From 0a60ebad17abc02696417811e41281aa89997e53 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:30:54 +0800 Subject: [PATCH 438/682] Add ext-xz --- config/pkg/ext/ext-xz.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-xz.yml diff --git a/config/pkg/ext/ext-xz.yml b/config/pkg/ext/ext-xz.yml new file mode 100644 index 000000000..0d625ad29 --- /dev/null +++ b/config/pkg/ext/ext-xz.yml @@ -0,0 +1,15 @@ +ext-xz: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/codemasher/php-ext-xz' + rev: main + extract: php-src/ext/xz + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - xz + php-extension: + arg-type: with-path From 83c266a71326fdca4ec00f2d43bfca1664f2dc62 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:41:00 +0800 Subject: [PATCH 439/682] Add ext-yac --- config/pkg/ext/ext-yac.yml | 14 ++++++++++++++ src/Package/Extension/yac.php | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 config/pkg/ext/ext-yac.yml create mode 100644 src/Package/Extension/yac.php diff --git a/config/pkg/ext/ext-yac.yml b/config/pkg/ext/ext-yac.yml new file mode 100644 index 000000000..e10bea064 --- /dev/null +++ b/config/pkg/ext/ext-yac.yml @@ -0,0 +1,14 @@ +ext-yac: + type: php-extension + artifact: + source: + type: pecl + name: yac + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends@unix: + - fastlz + - ext-igbinary + php-extension: + arg-type@unix: '--enable-yac@shared_suffix@ --enable-igbinary --enable-json --with-system-fastlz' diff --git a/src/Package/Extension/yac.php b/src/Package/Extension/yac.php new file mode 100644 index 000000000..4bf2cf664 --- /dev/null +++ b/src/Package/Extension/yac.php @@ -0,0 +1,25 @@ +getSourceDir()}/storage/allocator/yac_allocator.h", 'defined(HAVE_SHM_MMAP_ANON)', 'defined(YAC_ALLOCATOR_H)'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/serializer/igbinary.c", '#ifdef YAC_ENABLE_IGBINARY', '#if 1'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/serializer/json.c", '#if YAC_ENABLE_JSON', '#if 1'); + return true; + } +} From 738c61b682dab25dd5d04161682c096f8302110d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:50:12 +0800 Subject: [PATCH 440/682] Add ext-zstd --- config/pkg/ext/ext-zstd.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-zstd.yml diff --git a/config/pkg/ext/ext-zstd.yml b/config/pkg/ext/ext-zstd.yml new file mode 100644 index 000000000..1f004f131 --- /dev/null +++ b/config/pkg/ext/ext-zstd.yml @@ -0,0 +1,15 @@ +ext-zstd: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/kjdev/php-ext-zstd' + rev: master + extract: php-src/ext/zstd + metadata: + license-files: [LICENSE] + license: MIT + depends: + - zstd + php-extension: + arg-type: '--enable-zstd --with-libzstd=@build_root_path@' From 98a618f1cd23572a66e3f6018b2dfa5c3392fd72 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:54:11 +0800 Subject: [PATCH 441/682] Add ext-yaml --- config/pkg/ext/ext-yaml.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/pkg/ext/ext-yaml.yml diff --git a/config/pkg/ext/ext-yaml.yml b/config/pkg/ext/ext-yaml.yml new file mode 100644 index 000000000..a60b62547 --- /dev/null +++ b/config/pkg/ext/ext-yaml.yml @@ -0,0 +1,16 @@ +ext-yaml: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/php/pecl-file_formats-yaml' + rev: php7 + extract: php-src/ext/yaml + metadata: + license-files: [LICENSE] + license: MIT + depends: + - libyaml + php-extension: + arg-type@unix: with-path + arg-type@windows: with From b1a59dad791f4c0e8db29314ad27f100da5e26c1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 12:06:48 +0800 Subject: [PATCH 442/682] Make PhpExtensionPackage::getSharedExtensionLoadString public --- src/StaticPHP/Package/PhpExtensionPackage.php | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 7064d041e..baaa27531 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -327,38 +327,11 @@ public function registerDefaultStages(): void } } - /** - * Splits a given string of library flags into static and shared libraries. - * - * @param string $allLibs A space-separated string of library flags (e.g., -lxyz). - * @return array an array containing two elements: the first is a space-separated string - * of static library flags, and the second is a space-separated string - * of shared library flags - */ - protected function splitLibsIntoStaticAndShared(string $allLibs): array - { - $staticLibString = ''; - $sharedLibString = ''; - $libs = explode(' ', $allLibs); - foreach ($libs as $lib) { - $staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a'; - if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) { - $staticLib = $lib; - } - if ($lib === '-lphp' || !file_exists($staticLib)) { - $sharedLibString .= " {$lib}"; - } else { - $staticLibString .= " {$lib}"; - } - } - return [trim($staticLibString), trim($sharedLibString)]; - } - /** * Builds the `-d extension_dir=... -d extension=...` string for all resolved shared extensions. * Used in CLI smoke test to load shared extension dependencies at runtime. */ - private function getSharedExtensionLoadString(): string + public function getSharedExtensionLoadString(): string { $sharedExts = array_filter( $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), @@ -382,6 +355,33 @@ private function getSharedExtensionLoadString(): string return $ret; } + /** + * Splits a given string of library flags into static and shared libraries. + * + * @param string $allLibs A space-separated string of library flags (e.g., -lxyz). + * @return array an array containing two elements: the first is a space-separated string + * of static library flags, and the second is a space-separated string + * of shared library flags + */ + protected function splitLibsIntoStaticAndShared(string $allLibs): array + { + $staticLibString = ''; + $sharedLibString = ''; + $libs = explode(' ', $allLibs); + foreach ($libs as $lib) { + $staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a'; + if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) { + $staticLib = $lib; + } + if ($lib === '-lphp' || !file_exists($staticLib)) { + $sharedLibString .= " {$lib}"; + } else { + $staticLibString .= " {$lib}"; + } + } + return [trim($staticLibString), trim($sharedLibString)]; + } + /** * Escape PHP test file content for inline `-r` usage. * Strips Date: Wed, 18 Mar 2026 12:07:19 +0800 Subject: [PATCH 443/682] Fix zig-cc strlcpy missing issue with swoole+openssl --- src/StaticPHP/Toolchain/ZigToolchain.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php index 344ce3e9c..e817abd75 100644 --- a/src/StaticPHP/Toolchain/ZigToolchain.php +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -67,6 +67,9 @@ public function afterInit(): void $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no {$extra_vars}"); } + // zig-cc/clang treats strlcpy/strlcat as compiler builtins, so configure link tests pass (HAVE_STRLCPY=1) + $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=ac_cv_func_strlcpy=no ac_cv_func_strlcat=no {$extra_vars}"); } public function getCompilerInfo(): ?string From a24fae7a55e995543ee8a19cf004414920430419 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 12:07:39 +0800 Subject: [PATCH 444/682] Add ext-swoole --- config/pkg/ext/ext-swoole.yml | 72 +++++++++++++++ src/Package/Extension/swoole.php | 150 +++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 config/pkg/ext/ext-swoole.yml create mode 100644 src/Package/Extension/swoole.php diff --git a/config/pkg/ext/ext-swoole.yml b/config/pkg/ext/ext-swoole.yml new file mode 100644 index 000000000..b6499e85e --- /dev/null +++ b/config/pkg/ext/ext-swoole.yml @@ -0,0 +1,72 @@ +ext-swoole: + type: php-extension + artifact: + source: + type: ghtar + repo: swoole/swoole-src + extract: php-src/ext/swoole + match: v6\.+ + prefer-stable: true + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - libcares + - brotli + - nghttp2 + - zlib + - ext-openssl + - ext-curl + suggests: + - zstd + - ext-sockets + - ext-swoole-hook-pgsql + - ext-swoole-hook-mysql + - ext-swoole-hook-sqlite + - ext-swoole-hook-odbc + suggests@linux: + - zstd + - liburing + - ext-sockets + - ext-swoole-hook-pgsql + - ext-swoole-hook-mysql + - ext-swoole-hook-sqlite + - ext-swoole-hook-odbc + lang: cpp + php-extension: + arg-type: custom +ext-swoole-hook-mysql: + type: php-extension + depends: + - ext-mysqlnd + - ext-pdo + - ext-pdo_mysql + suggests: + - ext-mysqli + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-odbc: + type: php-extension + depends: + - ext-pdo + - unixodbc + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-pgsql: + type: php-extension + depends: + - ext-pgsql + - ext-pdo + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-sqlite: + type: php-extension + depends: + - ext-sqlite3 + - ext-pdo + php-extension: + arg-type: none + display-name: swoole diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php new file mode 100644 index 000000000..c269ba544 --- /dev/null +++ b/src/Package/Extension/swoole.php @@ -0,0 +1,150 @@ +getPhpExtensionPackage('swoole-hook-odbc') && $installer->getPhpExtensionPackage('pdo_odbc')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-odbc provides pdo_odbc, if you enable odbc hook for swoole, you must remove pdo_odbc extension.'); + } + // swoole-hook-pgsql conflicts with pdo_pgsql + if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && $installer->getPhpExtensionPackage('pdo_pgsql')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-pgsql provides pdo_pgsql, if you enable pgsql hook for swoole, you must remove pdo_pgsql extension.'); + } + // swoole-hook-sqlite conflicts with pdo_sqlite + if ($installer->getPhpExtensionPackage('swoole-hook-sqlite') && $installer->getPhpExtensionPackage('pdo_sqlite')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-sqlite provides pdo_sqlite, if you enable sqlite hook for swoole, you must remove pdo_sqlite extension.'); + } + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Fix maximum version check for Swoole 6.2')] + public function patchBeforeMake(): void + { + FileSystem::replaceFileStr($this->getSourceDir() . '/ext-src/php_swoole_private.h', 'PHP_VERSION_ID > 80500', 'PHP_VERSION_ID >= 80600'); + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Fix swoole with event extension conflict bug on macOS')] + public function patchBeforeMake2(): void + { + if (SystemTarget::getTargetOS() === 'Darwin') { + // Fix swoole with event extension conflict bug + $util_path = shell()->execWithResult('xcrun --show-sdk-path', false)[1][0] . '/usr/include/util.h'; + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/thirdparty/php/standard/proc_open.cc", + 'include ', + "include \"{$util_path}\"", + ); + } + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string + { + // enable swoole + $arg = '--enable-swoole' . ($shared ? '=shared' : ''); + + // commonly used feature: coroutine-time + $arg .= ' --enable-swoole-coro-time --with-pic'; + + $arg .= $builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; + + // required features: curl, openssl (but curl hook is buggy for php 8.0) + $arg .= php::getPHPVersionID() >= 80100 ? ' --enable-swoole-curl' : ' --disable-swoole-curl'; + $arg .= ' --enable-openssl'; + + // additional features that only require libraries + $arg .= $installer->getLibraryPackage('libcares') ? ' --enable-cares' : ''; + $arg .= $installer->getLibraryPackage('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; + $arg .= $installer->getLibraryPackage('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : ''; + $arg .= $installer->getLibraryPackage('zstd') ? ' --enable-zstd' : ''; + $arg .= $installer->getLibraryPackage('liburing') ? ' --enable-iouring' : ''; + $arg .= $installer->getPhpExtensionPackage('sockets') ? ' --enable-sockets' : ''; + + // enable additional features that require the pdo extension, but conflict with pdo_* extensions + // to make sure everything works as it should, this is done in fake addon extensions + $arg .= $installer->getPhpExtensionPackage('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql'; + $arg .= $installer->getPhpExtensionPackage('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; + $arg .= $installer->getPhpExtensionPackage('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; + if ($installer->getPhpExtensionPackage('swoole-hook-odbc')) { + $config = new SPCConfigUtil()->getLibraryConfig($installer->getLibraryPackage('unixodbc')); + $arg .= " --with-swoole-odbc=unixODBC,{$builder->getBuildRootPath()} SWOOLE_ODBC_LIBS=\"{$config['libs']}\""; + } + + // Get version from source directory + $ver = null; + $file = SOURCE_PATH . '/php-src/ext/swoole/include/swoole_version.h'; + // Match #define SWOOLE_VERSION "5.1.3" + $pattern = '/#define SWOOLE_VERSION "(.+)"/'; + if (preg_match($pattern, file_get_contents($file), $matches)) { + $ver = $matches[1]; + } + + if ($ver && $ver >= '6.1.0') { + $arg .= ' --enable-swoole-stdext'; + } + + if (SystemTarget::getTargetOS() === 'Darwin') { + $arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no'; + } + + return $arg; + } + + #[AfterStage('php', [php::class, 'smokeTestCliForUnix'], 'ext-swoole-hook-mysql')] + public function mysqlTest(PackageInstaller $installer): void + { + [$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $this->getSharedExtensionLoadString() . ' --ri "swoole"', false); + $out = implode('', $out); + if ($ret !== 0) { + throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: 'extension swoole_hook_mysql sanity check'); + } + // mysqlnd + if ($installer->getPhpExtensionPackage('swoole-hook-mysql') && !str_contains($out, 'mysqlnd')) { + throw new ValidationException('swoole mysql hook is not enabled correctly.', validation_module: 'Extension swoole mysql hook availability check'); + } + // coroutine_odbc + if ($installer->getPhpExtensionPackage('swoole-hook-odbc') && !str_contains($out, 'coroutine_odbc')) { + throw new ValidationException('swoole odbc hook is not enabled correctly.', validation_module: 'Extension swoole odbc hook availability check'); + } + // coroutine_pgsql + if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && !str_contains($out, 'coroutine_pgsql')) { + throw new ValidationException( + 'swoole pgsql hook is not enabled correctly.', + validation_module: 'Extension swoole pgsql hook availability check' + ); + } + // coroutine_sqlite + if ($installer->getPhpExtensionPackage('swoole-hook-sqlite') && !str_contains($out, 'coroutine_sqlite')) { + throw new ValidationException( + 'swoole sqlite hook is not enabled correctly.', + validation_module: 'Extension swoole sqlite hook availability check' + ); + } + } +} From 1ee8bc7d3483a5ebd7d25f73cfdaffa8bde72008 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 12:36:22 +0800 Subject: [PATCH 445/682] Add ext-swow --- config/pkg/ext/ext-swow.yml | 18 ++++++++++++++ src/Package/Extension/swow.php | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 config/pkg/ext/ext-swow.yml create mode 100644 src/Package/Extension/swow.php diff --git a/config/pkg/ext/ext-swow.yml b/config/pkg/ext/ext-swow.yml new file mode 100644 index 000000000..11592cd0b --- /dev/null +++ b/config/pkg/ext/ext-swow.yml @@ -0,0 +1,18 @@ +ext-swow: + type: php-extension + artifact: + source: + extract: php-src/ext/swow-src + type: ghtar + repo: swow/swow + prefer-stable: true + metadata: + license: Apache-2.0 + license-files: [LICENSE] + suggests: + - openssl + - curl + - ext-openssl + - ext-curl + php-extension: + arg-type: custom diff --git a/src/Package/Extension/swow.php b/src/Package/Extension/swow.php new file mode 100644 index 000000000..333a3ed7b --- /dev/null +++ b/src/Package/Extension/swow.php @@ -0,0 +1,44 @@ +getLibraryPackage('openssl') ? ' --enable-swow-ssl' : ' --disable-swow-ssl'; + $arg .= $installer->getLibraryPackage('curl') ? ' --enable-swow-curl' : ' --disable-swow-curl'; + return $arg; + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-swow')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-swow')] + public function patchBeforeBuildconf(PackageInstaller $installer): bool + { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + if (php::getPHPVersionID() >= 80000 && !is_link("{$php_src}/ext/swow")) { + if (PHP_OS_FAMILY === 'Windows') { + f_passthru("cd {$php_src}/ext && mklink /D swow swow-src\\ext"); + } else { + f_passthru("cd {$php_src}/ext && ln -s swow-src/ext swow"); + } + } + // replace AC_DEFUN([SWOW_PKG_CHECK_MODULES] to AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC] + FileSystem::replaceFileStr($this->getSourceDir() . '/ext/config.m4', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES]', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC]'); + return false; + } +} From 60b2aea09e7a1a90a8636add22ebe9fb6f5427ec Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 18 Mar 2026 11:57:27 +0700 Subject: [PATCH 446/682] fix libde265 on ancient debian OS? --- src/SPC/builder/unix/library/libde265.php | 1 + src/globals/test-extensions.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/unix/library/libde265.php b/src/SPC/builder/unix/library/libde265.php index 9549a2ec9..04195a5db 100644 --- a/src/SPC/builder/unix/library/libde265.php +++ b/src/SPC/builder/unix/library/libde265.php @@ -11,6 +11,7 @@ trait libde265 protected function build(): void { UnixCMakeExecutor::create($this) + ->appendEnv(['LDFLAGS' => '-lpthread']) ->addConfigureArgs('-DENABLE_SDL=OFF') ->build(); $this->patchPkgconfPrefix(['libde265.pc']); diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index bfae094a3..9fae26255 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'grpc', + 'Linux', 'Darwin' => 'imagick', 'Windows' => 'com_dotnet', }; @@ -62,7 +62,7 @@ }; // If you want to test lib-suggests for all extensions and libraries, set it to true. -$with_suggested_libs = false; +$with_suggested_libs = true; // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { From c1f2fd49a6fdf92cbf680b15d5b3faa8977f6660 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 13:30:29 +0800 Subject: [PATCH 447/682] Add ext-imap --- config/pkg/ext/ext-imap.yml | 15 +++++++ src/Package/Extension/imap.php | 55 +++++++++++++++++++++++ src/Package/Library/imap.php | 65 +++++++++++++++------------- src/StaticPHP/Util/SPCConfigUtil.php | 4 +- 4 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 config/pkg/ext/ext-imap.yml create mode 100644 src/Package/Extension/imap.php diff --git a/config/pkg/ext/ext-imap.yml b/config/pkg/ext/ext-imap.yml new file mode 100644 index 000000000..a6c18daca --- /dev/null +++ b/config/pkg/ext/ext-imap.yml @@ -0,0 +1,15 @@ +ext-imap: + type: php-extension + artifact: + source: + type: pecl + name: imap + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - imap + suggests: + - ext-openssl + php-extension: + arg-type: custom diff --git a/src/Package/Extension/imap.php b/src/Package/Extension/imap.php new file mode 100644 index 000000000..e9879b48b --- /dev/null +++ b/src/Package/Extension/imap.php @@ -0,0 +1,55 @@ +getOption('enable-zts')) { + throw new WrongUsageException('ext-imap is not thread safe, do not build it with ZTS builds'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-imap')] + public function patchBeforeBuildconf(PackageInstaller $installer): void + { + if ($installer->getLibraryPackage('openssl')) { + // sometimes imap with openssl does not contain zlib (required by openssl) + // we need to add it manually + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD"', 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD -lz"'); + } + // c-client is built with PASSWDTYPE=nul so libcrypt is not referenced. + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + " PHP_CHECK_LIBRARY(crypt, crypt,\n [\n PHP_ADD_LIBRARY(crypt,, IMAP_SHARED_LIBADD)\n AC_DEFINE(HAVE_LIBCRYPT,1,[ ])\n ])", + ' dnl Skipped: crypt check not needed (c-client built with PASSWDTYPE=nul)' + ); + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string + { + $arg = "--with-imap={$builder->getBuildRootPath()}"; + if (($ssl = $installer->getLibraryPackage('openssl')) !== null) { + $arg .= " --with-imap-ssl={$ssl->getBuildRootPath()}"; + } + return $arg; + } +} diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index 607d78ee2..3c9f261c9 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -4,8 +4,6 @@ namespace Package\Library; -use Package\Target\php; -use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; @@ -19,15 +17,6 @@ #[Library('imap')] class imap { - #[AfterStage('php', [php::class, 'patchUnixEmbedScripts'], 'imap')] - #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] - public function afterPatchScripts(): void - { - if (SystemTarget::getLibc() === 'glibc') { - FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"'); - } - } - #[PatchBeforeBuild] #[PatchDescription('Patch imap build system for Linux and macOS compatibility')] public function patchBeforeBuild(LibraryPackage $lib): void @@ -66,14 +55,24 @@ public function buildLinux(LibraryPackage $lib, PackageInstaller $installer): vo } $libcVer = SystemTarget::getLibcVersion(); $extraLibs = $libcVer && version_compare($libcVer, '2.17', '<=') ? 'EXTRALDFLAGS="-ldl -lrt -lpthread"' : ''; - shell()->cd($lib->getSourceDir()) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec("yes | make slx {$ssl_options} EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + try { + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + // PASSWDTYPE=nul avoids any crypt() symbol reference in c-client.a; + // zig-cc 0.15+ uses paths_first strategy and cannot find libcrypt outside of buildroot. + ->exec("yes | make slx {$ssl_options} PASSWDTYPE=nul EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + } catch (\Throwable $e) { + // slx target also builds bundled tools (mtest, etc.) which may fail to link -lcrypt dynamically + // (e.g. with zig-cc). We only need c-client.a, so tolerate the failure if it was built. + if (!file_exists("{$lib->getSourceDir()}/c-client/c-client.a")) { + throw $e; + } + } try { shell() ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") @@ -94,16 +93,24 @@ public function buildDarwin(LibraryPackage $lib, PackageInstaller $installer): v $ssl_options = 'SSLTYPE=none'; } $out = shell()->execWithResult('echo "-include $(xcrun --show-sdk-path)/usr/include/poll.h -include $(xcrun --show-sdk-path)/usr/include/time.h -include $(xcrun --show-sdk-path)/usr/include/utime.h"')[1][0]; - shell()->cd($lib->getSourceDir()) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec( - "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" - ); + try { + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec( + "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" + ); + } catch (\Throwable $e) { + // osx target also builds bundled tools (mtest, etc.) which may fail to link. + // We only need c-client.a, so tolerate the failure if it was built. + if (!file_exists("{$lib->getSourceDir()}/c-client/c-client.a")) { + throw $e; + } + } try { shell() ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 32ef3bc64..8b6fe6b37 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -389,7 +389,9 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st } if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { - $lib_names[] = '-lcrypt'; + if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { + $lib_names[] = '-lcrypt'; + } } if (!$use_short_libs) { $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); From c81146bf18a65468ad6d4cd006c292e9ce880d1a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 15:00:44 +0800 Subject: [PATCH 448/682] Add ncursesw --- config/artifact/ncurses.yml | 9 ++++++ config/pkg/lib/ncurses.yml | 14 ++++----- src/Package/Library/ncurses.php | 56 ++++++++++++++++++++------------- 3 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 config/artifact/ncurses.yml diff --git a/config/artifact/ncurses.yml b/config/artifact/ncurses.yml new file mode 100644 index 000000000..52c8f59ff --- /dev/null +++ b/config/artifact/ncurses.yml @@ -0,0 +1,9 @@ +ncurses: + binary: hosted + metadata: + license-files: + - COPYING + source: + type: filelist + url: 'https://ftp.gnu.org/pub/gnu/ncurses/' + regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' diff --git a/config/pkg/lib/ncurses.yml b/config/pkg/lib/ncurses.yml index cbc1ba676..6ce20f917 100644 --- a/config/pkg/lib/ncurses.yml +++ b/config/pkg/lib/ncurses.yml @@ -1,12 +1,10 @@ ncurses: type: library - artifact: - source: - type: filelist - url: 'https://ftp.gnu.org/pub/gnu/ncurses/' - regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' - binary: hosted - metadata: - license-files: [COPYING] + artifact: ncurses static-libs@unix: - libncurses.a +ncursesw: + type: library + artifact: ncurses + static-libs@unix: + - libncursesw.a diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index c7c39dc1f..dd591a6f9 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -13,6 +13,7 @@ use StaticPHP\Util\FileSystem; #[Library('ncurses')] +#[Library('ncursesw')] class ncurses { #[BuildFor('Darwin')] @@ -21,37 +22,48 @@ public function build(LibraryPackage $package, ToolchainInterface $toolchain): v { $dirdiff = new DirDiff(BUILD_BIN_PATH); - UnixAutoconfExecutor::create($package) + $ac = UnixAutoconfExecutor::create($package) ->appendEnv([ 'LDFLAGS' => $toolchain->isStatic() ? '-static' : '', - ]) - ->configure( - '--enable-overwrite', - '--with-curses-h', - '--enable-pc-files', - '--enable-echo', - '--disable-widec', - '--with-normal', - '--with-ticlib', - '--without-tests', - '--without-dlsym', - '--without-debug', - '--enable-symlinks', - "--bindir={$package->getBinDir()}", - "--includedir={$package->getIncludeDir()}", - "--libdir={$package->getLibDir()}", - "--prefix={$package->getBuildRootPath()}", - ) + ]); + $wide = $package->getName() === 'ncurses' ? ['--disable-widec'] : []; + // Include standard system terminfo paths as fallback so binaries linking this ncurses + // (e.g. htop) can find terminfo on any target system without needing TERMINFO_DIRS set. + $terminfo_dirs = implode(':', [ + "{$package->getBuildRootPath()}/share/terminfo", + '/etc/terminfo', + '/lib/terminfo', + '/usr/share/terminfo', + ]); + $ac->configure( + '--enable-overwrite', + '--with-curses-h', + '--enable-pc-files', + '--enable-echo', + '--with-normal', + '--with-ticlib', + '--without-tests', + '--without-dlsym', + '--without-debug', + '--enable-symlinks', + "--with-terminfo-dirs={$terminfo_dirs}", + "--bindir={$package->getBinDir()}", + "--includedir={$package->getIncludeDir()}", + "--libdir={$package->getLibDir()}", + "--prefix={$package->getBuildRootPath()}", + ...$wide, + ) ->make(); $new_files = $dirdiff->getIncrementFiles(true); foreach ($new_files as $file) { @unlink(BUILD_BIN_PATH . '/' . $file); } - shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); - shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); + // shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); + // shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); - $pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc']; + $suffix = $package->getName() === 'ncursesw' ? 'w' : ''; + $pkgconf_list = ["form{$suffix}.pc", "menu{$suffix}.pc", "ncurses++{$suffix}.pc", "ncurses{$suffix}.pc", "panel{$suffix}.pc", "tic{$suffix}.pc"]; $package->patchPkgconfPrefix($pkgconf_list); foreach ($pkgconf_list as $pkgconf) { From 0b0ecd17c324cc98b144ae5bfcadfaaa21024265 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 15:00:58 +0800 Subject: [PATCH 449/682] Allow curl building static executable --- config/pkg/{lib => target}/curl.yml | 4 +++- src/Package/{Library => Target}/curl.php | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) rename config/pkg/{lib => target}/curl.yml (91%) rename src/Package/{Library => Target}/curl.php (93%) diff --git a/config/pkg/lib/curl.yml b/config/pkg/target/curl.yml similarity index 91% rename from config/pkg/lib/curl.yml rename to config/pkg/target/curl.yml index f183b21e0..4daba8c14 100644 --- a/config/pkg/lib/curl.yml +++ b/config/pkg/target/curl.yml @@ -1,5 +1,5 @@ curl: - type: library + type: target artifact: source: type: ghrel @@ -29,5 +29,7 @@ curl: - SystemConfiguration headers: - curl + static-bins@unix: + - curl static-libs@unix: - libcurl.a diff --git a/src/Package/Library/curl.php b/src/Package/Target/curl.php similarity index 93% rename from src/Package/Library/curl.php rename to src/Package/Target/curl.php index 0edca93fd..dbfa8f7a1 100644 --- a/src/Package/Library/curl.php +++ b/src/Package/Target/curl.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace Package\Library; +namespace Package\Target; use StaticPHP\Attribute\Package\BuildFor; -use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; -#[Library('curl')] +#[Target('curl')] class curl { #[PatchBeforeBuild] @@ -48,7 +48,7 @@ public function build(LibraryPackage $lib): void ->optionalPackage('idn2', ...cmake_boolean_args('CURL_USE_IDN2')) ->optionalPackage('libcares', '-DENABLE_ARES=ON') ->addConfigureArgs( - '-DBUILD_CURL_EXE=OFF', + '-DBUILD_CURL_EXE=ON', '-DBUILD_LIBCURL_DOCS=OFF', ) ->build(); @@ -63,5 +63,7 @@ public function build(LibraryPackage $lib): void } shell()->cd("{$lib->getLibDir()}/cmake/CURL/") ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); + + $lib->setOutput('Static curl executable path', BUILD_BIN_PATH . '/curl'); } } From 9e2a5ce188e64c2b93194628bae7f1f457acabca Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 15:01:06 +0800 Subject: [PATCH 450/682] Add target htop --- config/pkg/target/htop.yml | 10 ++++++++++ src/Package/Target/htop.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 config/pkg/target/htop.yml create mode 100644 src/Package/Target/htop.php diff --git a/config/pkg/target/htop.yml b/config/pkg/target/htop.yml new file mode 100644 index 000000000..fcefa70aa --- /dev/null +++ b/config/pkg/target/htop.yml @@ -0,0 +1,10 @@ +htop: + type: target + artifact: + source: + type: ghrel + repo: htop-dev/htop + match: htop.+\.tar\.xz + prefer-stable: true + depends: + - ncursesw diff --git a/src/Package/Target/htop.php b/src/Package/Target/htop.php new file mode 100644 index 000000000..3539d3a76 --- /dev/null +++ b/src/Package/Target/htop.php @@ -0,0 +1,29 @@ +removeConfigureArgs('--disable-shared', '--enable-static') + ->exec('./autogen.sh') + ->addConfigureArgs($toolchain->isStatic() ? '--enable-static' : '--disable-static') + ->configure() + ->make(with_clean: false); + } +} From f2fa29809afe565bb8cedcd414b4ecdcb23ffc5f Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 18 Mar 2026 18:37:16 +0700 Subject: [PATCH 451/682] why is it not failing here? --- src/globals/test-extensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 576a9774e..e589f0f7b 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -74,7 +74,7 @@ // You can use `common`, `bulk`, `minimal` or `none`. // note: combination is only available for *nix platform. Windows must use `none` combination $base_combination = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'bulk', + 'Linux', 'Darwin' => 'minimal', 'Windows' => 'none', }; From 823fe9694233bb421fbc411ce4eb67b502058d6f Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 18 Mar 2026 23:26:57 +0700 Subject: [PATCH 452/682] attempt --- src/SPC/builder/unix/library/libde265.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/unix/library/libde265.php b/src/SPC/builder/unix/library/libde265.php index 04195a5db..184a44261 100644 --- a/src/SPC/builder/unix/library/libde265.php +++ b/src/SPC/builder/unix/library/libde265.php @@ -11,8 +11,10 @@ trait libde265 protected function build(): void { UnixCMakeExecutor::create($this) - ->appendEnv(['LDFLAGS' => '-lpthread']) - ->addConfigureArgs('-DENABLE_SDL=OFF') + ->addConfigureArgs( + '-DENABLE_SDL=OFF', + '-DENABLE_DECODER=OFF' + ) ->build(); $this->patchPkgconfPrefix(['libde265.pc']); } From beeb0b87211bbb826b88bd6ed2c2371ca8072afe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:29:45 +0800 Subject: [PATCH 453/682] Handle failure in fetching Zig version index --- src/Package/Artifact/zig.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index b42eee3ae..95520aa4b 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -26,6 +26,9 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); $index_json = json_decode($index_json ?: '', true); $latest_version = null; + if ($index_json === null) { + throw new DownloaderException('Failed to fetch Zig version index'); + } foreach ($index_json as $version => $data) { if ($version !== 'master') { $latest_version = $version; From b0522205dabe2f9364fd0667132c407a4b006246 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:30:34 +0800 Subject: [PATCH 454/682] Add support for package environment variables and path injection --- src/StaticPHP/Config/ConfigValidator.php | 9 ++++++++ src/StaticPHP/Package/PackageInstaller.php | 25 ++++++++++++++++++++++ src/StaticPHP/Util/GlobalEnvManager.php | 11 ++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 256e47fd0..919de86df 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -40,6 +40,9 @@ class ConfigValidator 'static-libs' => ConfigType::LIST_ARRAY, // @ 'pkg-configs' => ConfigType::LIST_ARRAY, 'static-bins' => ConfigType::LIST_ARRAY, // @ + 'path' => ConfigType::LIST_ARRAY, // @ + 'env' => ConfigType::ASSOC_ARRAY, // @ + 'append-env' => ConfigType::ASSOC_ARRAY, // @ ]; public const array PACKAGE_FIELDS = [ @@ -60,6 +63,9 @@ class ConfigValidator 'static-libs' => false, // @ 'pkg-configs' => false, 'static-bins' => false, // @ + 'path' => false, // @ + 'env' => false, // @ + 'append-env' => false, // @ ]; public const array SUFFIX_ALLOWED_FIELDS = [ @@ -68,6 +74,9 @@ class ConfigValidator 'headers', 'static-libs', 'static-bins', + 'path', + 'env', + 'append-env', ]; public const array PHP_EXTENSION_FIELDS = [ diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index d8d745f61..417c4e1b6 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -9,6 +9,7 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Artifact\DownloaderOptions; +use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; @@ -580,6 +581,30 @@ private function resolvePackages(): void foreach ($resolved_packages as $pkg_name) { $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name); } + + foreach ($this->packages as $package) { + $this->injectPackageEnvs($package); + } + } + + private function injectPackageEnvs(Package $package): void + { + $name = $package->getName(); + + $paths = PackageConfig::get($name, 'path', []); + foreach ($paths as $path) { + GlobalEnvManager::addPathIfNotExists(FileSystem::replacePathVariable($path)); + } + + $envs = PackageConfig::get($name, 'env', []); + foreach ($envs as $k => $v) { + GlobalEnvManager::putenv("{$k}=" . FileSystem::replacePathVariable((string) $v)); + } + + $append_envs = PackageConfig::get($name, 'append-env', []); + foreach ($append_envs as $k => $v) { + GlobalEnvManager::appendEnv($k, FileSystem::replacePathVariable((string) $v)); + } } private function handlePhpTargetPackage(TargetPackage $package): void diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php index 86fcc6524..5b4b16b25 100644 --- a/src/StaticPHP/Util/GlobalEnvManager.php +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -112,6 +112,17 @@ public static function addPathIfNotExists(string $path): void } } + public static function appendEnv(string $key, string $value): void + { + $existing = getenv($key); + if ($existing !== false && $existing !== '') { + $separator = SystemTarget::isUnix() ? ':' : ';'; + self::putenv("{$key}={$value}{$separator}{$existing}"); + } else { + self::putenv("{$key}={$value}"); + } + } + /** * Initialize the toolchain after the environment variables are set. * The toolchain or environment availability check is done here. From 9d748a6e0892ae1721f244331ff1188604231930 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:30:52 +0800 Subject: [PATCH 455/682] Add rust target --- config/pkg/target/rust.yml | 6 +++ src/Package/Artifact/rust.php | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 config/pkg/target/rust.yml create mode 100644 src/Package/Artifact/rust.php diff --git a/config/pkg/target/rust.yml b/config/pkg/target/rust.yml new file mode 100644 index 000000000..f2e107380 --- /dev/null +++ b/config/pkg/target/rust.yml @@ -0,0 +1,6 @@ +rust: + type: target + artifact: + binary: custom + path: + - '{pkg_root_path}/rust/bin' diff --git a/src/Package/Artifact/rust.php b/src/Package/Artifact/rust.php new file mode 100644 index 000000000..e5c9f5259 --- /dev/null +++ b/src/Package/Artifact/rust.php @@ -0,0 +1,85 @@ +executeCurl('https://static.rust-lang.org/dist/channel-rust-stable.toml', retries: $downloader->getRetry()); + // parse toml by regex since we want to avoid adding a toml parser dependency just for this + $cnt = preg_match_all('/^version = "([^"]+)"$/m', $toml_config ?: '', $matches); + if (!$cnt) { + throw new DownloaderException('Failed to parse Rust version from channel config'); + } + $versions = $matches[1]; + // strip version num \d.\d.\d (some version number is like "x.x.x (abcdefg 1970-01-01)" + $versions = array_filter(array_map(fn ($v) => preg_match('/^(\d+\.\d+\.\d+)/', $v, $m) ? $m[1] : null, $versions)); + usort($versions, 'version_compare'); + $latest_version = end($versions); + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Rust version'); + } + + // merge download link + $download_url = "https://static.rust-lang.org/dist/rust-{$latest_version}-{$arch}-unknown-linux-{$distro}.tar.xz"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . basename($download_url); + default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive(basename($path), ['url' => $download_url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/rust-install', verified: false, version: $latest_version); + } + + #[CustomBinaryCheckUpdate('rust', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $toml_config = default_shell()->executeCurl('https://static.rust-lang.org/dist/channel-rust-stable.toml', retries: $downloader->getRetry()); + $cnt = preg_match_all('/^version = "([^"]+)"$/m', $toml_config ?: '', $matches); + if (!$cnt) { + throw new DownloaderException('Failed to parse Rust version from channel config'); + } + $versions = array_filter(array_map(fn ($v) => preg_match('/^(\d+\.\d+\.\d+)/', $v, $m) ? $m[1] : null, $matches[1])); + usort($versions, 'version_compare'); + $latest_version = end($versions); + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Rust version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || $latest_version !== $old_version, + ); + } + + #[AfterBinaryExtract('rust', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function postExtractRust(string $target_path): void + { + $prefix = PKG_ROOT_PATH . '/rust'; + shell()->exec("cd {$target_path} && ./install.sh --prefix={$prefix}"); + } +} From b0eff0ba6e39f69e2637907b0476458fa5b70023 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:31:05 +0800 Subject: [PATCH 456/682] Add protoc target --- config/pkg/target/protoc.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 config/pkg/target/protoc.yml diff --git a/config/pkg/target/protoc.yml b/config/pkg/target/protoc.yml new file mode 100644 index 000000000..b45fb335a --- /dev/null +++ b/config/pkg/target/protoc.yml @@ -0,0 +1,8 @@ +protoc: + type: target + artifact: + binary: + linux-x86_64: { type: ghrel, repo: protocolbuffers/protobuf, match: 'protoc-([0-9.]+)-linux-x86_64\.zip', extract: '{pkg_root_path}/protoc' } + linux-aarch64: { type: ghrel, repo: protocolbuffers/protobuf, match: 'protoc-([0-9.]+)-linux-aarch_64\.zip', extract: '{pkg_root_path}/protoc' } + path: + - '{pkg_root_path}/protoc/bin' From c6207d8c7c2d7aa0fe2a16c8c21c921e8d012e92 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:31:24 +0800 Subject: [PATCH 457/682] Fix interactive install-pkg command --- src/StaticPHP/Command/InstallPackageCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index b5fb8d2cf..864fd3796 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -34,7 +34,7 @@ public function configure(): void public function handle(): int { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], false); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], true); $installer->addInstallPackage($this->input->getArgument('package')); $installer->run(true); return static::SUCCESS; From 11376cc6ade8c9e960f72ad6d8f73dc736e51f26 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:31:43 +0800 Subject: [PATCH 458/682] Use env and path injection instead of old style --- config/pkg/target/go-xcaddy.yml | 6 ++++++ src/Package/Target/go_xcaddy.php | 26 -------------------------- 2 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 src/Package/Target/go_xcaddy.php diff --git a/config/pkg/target/go-xcaddy.yml b/config/pkg/target/go-xcaddy.yml index 89cb4cd02..deafb37d6 100644 --- a/config/pkg/target/go-xcaddy.yml +++ b/config/pkg/target/go-xcaddy.yml @@ -2,5 +2,11 @@ go-xcaddy: type: target artifact: binary: custom + env: + GOROOT: '{pkg_root_path}/go-xcaddy' + GOBIN: '{pkg_root_path}/go-xcaddy/bin' + GOPATH: '{pkg_root_path}/go-xcaddy/go' + path@unix: + - '{pkg_root_path}/go-xcaddy/bin' static-bins: - xcaddy diff --git a/src/Package/Target/go_xcaddy.php b/src/Package/Target/go_xcaddy.php deleted file mode 100644 index 0f8c75537..000000000 --- a/src/Package/Target/go_xcaddy.php +++ /dev/null @@ -1,26 +0,0 @@ - Date: Fri, 20 Mar 2026 11:20:42 +0800 Subject: [PATCH 459/682] Add homebrew llvm version toolchain support --- config/env.ini | 2 ++ src/StaticPHP/Doctor/Item/MacOSToolCheck.php | 15 +++++++++++++ .../Toolchain/ClangBrewToolchain.php | 21 +++++++++++++++++++ src/StaticPHP/Toolchain/ToolchainManager.php | 5 ++++- 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/StaticPHP/Toolchain/ClangBrewToolchain.php diff --git a/config/env.ini b/config/env.ini index 3143efaf5..947971188 100644 --- a/config/env.ini +++ b/config/env.ini @@ -134,6 +134,8 @@ OPENSSLDIR="" ; build target: macho or macho (possibly we could support macho-universal in the future) ; Currently we do not support universal and cross-compilation for macOS. SPC_TARGET=native-macos +; Whether to use brew version of llvm or system version (valid options: 'system', 'brew', default: 'system') +SPC_USE_LLVM=system ; compiler environments (default value is defined by selected toolchain) CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php index b69528ad1..9d51b83ae 100644 --- a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -7,6 +7,7 @@ use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\System\MacOSUtil; class MacOSToolCheck @@ -58,6 +59,20 @@ public function checkCliTools(): ?CheckResult return CheckResult::ok(); } + #[CheckItem('if homebrew llvm are installed', limit_os: 'Darwin')] + public function checkBrewLLVM(): ?CheckResult + { + if (getenv('SPC_USE_LLVM') === 'brew') { + $homebrew_prefix = getenv('HOMEBREW_PREFIX') ?: (SystemTarget::getTargetArch() === 'aarch64' ? '/opt/homebrew' : '/usr/local/homebrew'); + + if (MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"]) === null) { + return CheckResult::fail('Homebrew llvm is not installed', 'brew', ['missing' => ['llvm']]); + } + return CheckResult::ok(); + } + return null; + } + #[CheckItem('if bison version is 3.0 or later', limit_os: 'Darwin')] public function checkBisonVersion(array $command_path = []): ?CheckResult { diff --git a/src/StaticPHP/Toolchain/ClangBrewToolchain.php b/src/StaticPHP/Toolchain/ClangBrewToolchain.php new file mode 100644 index 000000000..5d8963ef4 --- /dev/null +++ b/src/StaticPHP/Toolchain/ClangBrewToolchain.php @@ -0,0 +1,21 @@ + ZigToolchain::class, 'Windows' => MSVCToolchain::class, - 'Darwin' => ClangNativeToolchain::class, + 'Darwin' => match (getenv('SPC_USE_LLVM') ?: 'system') { + 'brew' => ClangBrewToolchain::class, + default => ClangNativeToolchain::class, + }, default => throw new WrongUsageException('Unsupported OS family: ' . PHP_OS_FAMILY), }; } From dc79ac9c9a9efb5dac6963a3fe93497516c8065b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 11:32:15 +0800 Subject: [PATCH 460/682] Correct doctor fix --- src/StaticPHP/Doctor/Item/MacOSToolCheck.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php index 9d51b83ae..54d62e440 100644 --- a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -65,10 +65,10 @@ public function checkBrewLLVM(): ?CheckResult if (getenv('SPC_USE_LLVM') === 'brew') { $homebrew_prefix = getenv('HOMEBREW_PREFIX') ?: (SystemTarget::getTargetArch() === 'aarch64' ? '/opt/homebrew' : '/usr/local/homebrew'); - if (MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"]) === null) { - return CheckResult::fail('Homebrew llvm is not installed', 'brew', ['missing' => ['llvm']]); + if (($path = MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"])) === null) { + return CheckResult::fail('Homebrew llvm is not installed', 'build-tools', ['missing' => ['llvm']]); } - return CheckResult::ok(); + return CheckResult::ok($path); } return null; } From c5efcc0c93c2852dde5734471ed8e49362ac03d8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 16:07:28 +0800 Subject: [PATCH 461/682] Fix wrongly using msys2 tar.exe --- src/StaticPHP/Artifact/ArtifactExtractor.php | 3 +++ src/StaticPHP/Runtime/Shell/DefaultShell.php | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 4d38a84bd..8b73243a4 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -468,6 +468,9 @@ protected function extractArchive(string $filename, string $target): void if ($extname !== 'exe' && !is_dir($target)) { FileSystem::createDir($target); + if (!is_dir($target)) { + throw new FileSystemException("Failed to create target directory: {$target}"); + } } match (SystemTarget::getTargetOS()) { 'Windows' => match ($extname) { diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 66dfb7ab0..77dbf94a0 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -132,7 +132,7 @@ public function executeTarExtract(string $archive_path, string $target_path, str }; $mute = $this->console_putput ? '' : ' 2>/dev/null'; - $cmd = "tar {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; + $cmd = "\"C:\\Windows\\system32\\tar.exe\" {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; $this->logCommandInfo($cmd); logger()->debug("[TAR EXTRACT] {$cmd}"); @@ -187,7 +187,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 -f - -x -C {$target_arg} --strip-components 1"), + 'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | \"C:\\Windows\\system32\\tar.exe\" -f - -x -C {$target_arg} --strip-components 1"), default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"), }; From 3c9e868ce14d159339bcdf32408955dceea8fe9d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 16:08:58 +0800 Subject: [PATCH 462/682] Add spc-debug script on Windows --- bin/spc-debug.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 bin/spc-debug.ps1 diff --git a/bin/spc-debug.ps1 b/bin/spc-debug.ps1 new file mode 100644 index 000000000..015dae9c9 --- /dev/null +++ b/bin/spc-debug.ps1 @@ -0,0 +1,12 @@ +$PHP_Exec = ".\runtime\php.exe" + +if (-not(Test-Path $PHP_Exec)) { + $PHP_Exec = Get-Command php.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Definition + if (-not $PHP_Exec) { + Write-Host "Error: PHP not found, you need to install PHP on your system or use 'bin/setup-runtime'." -ForegroundColor Red + exit 1 + } +} + +& "$PHP_Exec" -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.client_port=9003 -d xdebug.start_with_request=yes ("bin/spc") @args +exit $LASTEXITCODE From 46132ee1c88e1b34dc611c4138176b6aaf6f49d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 16:16:09 +0800 Subject: [PATCH 463/682] Fix doctor lock path on Windows --- src/StaticPHP/Doctor/Doctor.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 1239a30c8..05f3c4a1d 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -147,13 +147,17 @@ private static function getLockPath(): string { if (SystemTarget::getTargetOS() === 'Windows') { $trial_ls = [ - getenv('LOCALAPPDATA') ?: ((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock', + getenv('LOCALAPPDATA') ? + (getenv('LOCALAPPDATA') . '\.spc-doctor.lock') : + (((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock'), sys_get_temp_dir() . '\.spc-doctor.lock', WORKING_DIR . '\.spc-doctor.lock', ]; } else { $trial_ls = [ - getenv('XDG_CACHE_HOME') ?: ((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock', + getenv('XDG_CACHE_HOME') ? + (getenv('XDG_CACHE_HOME') . '/.spc-doctor.lock') + : (((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock'), sys_get_temp_dir() . '/.spc-doctor.lock', WORKING_DIR . '/.spc-doctor.lock', ]; From 1d2916fa8f1636090e793c79537217520f8ea4cc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 16:16:29 +0800 Subject: [PATCH 464/682] Add brotli --- src/Package/Library/brotli.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Package/Library/brotli.php b/src/Package/Library/brotli.php index f22b9ef29..cab05112b 100644 --- a/src/Package/Library/brotli.php +++ b/src/Package/Library/brotli.php @@ -8,11 +8,19 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Util\FileSystem; #[Library('brotli')] class brotli { + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $package): void + { + WindowsCMakeExecutor::create($package)->build(); + // FileSystem::copy("{$package->getLibDir()}\\onig.lib", "{$package->getLibDir()}\\onig_a.lib"); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void From 0c389d906954d575a02aa414576c1cb1d0b2d687 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 18:03:46 +0800 Subject: [PATCH 465/682] Add zlib --- config/pkg/lib/zlib.yml | 3 +++ src/Package/Library/zlib.php | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/config/pkg/lib/zlib.yml b/config/pkg/lib/zlib.yml index cf7f11ba0..b4e71364e 100644 --- a/config/pkg/lib/zlib.yml +++ b/config/pkg/lib/zlib.yml @@ -14,3 +14,6 @@ zlib: - zconf.h static-libs@unix: - libz.a + static-libs@windows: + - zlibstatic.lib + - zlib_a.lib diff --git a/src/Package/Library/zlib.php b/src/Package/Library/zlib.php index 8706dfe9b..f45b942c9 100644 --- a/src/Package/Library/zlib.php +++ b/src/Package/Library/zlib.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('zlib')] class zlib @@ -21,4 +23,28 @@ public function build(LibraryPackage $lib): void // Patch pkg-config file $lib->patchPkgconfPrefix(['zlib.pc'], PKGCONF_PATCH_PREFIX); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib)->build(); + $detect_list = [ + 'zlibstatic.lib', + 'zs.lib', + 'libzs.lib', + ]; + foreach ($detect_list as $item) { + if (file_exists("{$lib->getLibDir()}\\{$item}")) { + FileSystem::copy("{$lib->getLibDir()}\\{$item}", "{$lib->getLibDir()}\\zlib_a.lib"); + FileSystem::copy("{$lib->getLibDir()}\\{$item}", "{$lib->getLibDir()}\\zlibstatic.lib"); + break; + } + } + FileSystem::removeFileIfExists("{$lib->getBinDir()}\\zlib.dll"); + FileSystem::removeFileIfExists("{$lib->getLibDir()}\\zlib.lib"); + FileSystem::removeFileIfExists("{$lib->getLibDir()}\\libz.dll"); + FileSystem::removeFileIfExists("{$lib->getLibDir()}\\libz.lib"); + FileSystem::removeFileIfExists("{$lib->getLibDir()}\\z.lib"); + FileSystem::removeFileIfExists("{$lib->getLibDir()}\\z.dll"); + } } From 464ddeb29d36712b6307b99dad56723b170f833e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 18:03:58 +0800 Subject: [PATCH 466/682] Fix file copy operation to handle identical source and destination paths --- src/StaticPHP/Util/FileSystem.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 3015b4891..144f81eb3 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -142,6 +142,9 @@ public static function copy(string $from, string $to): bool logger()->debug("Copying file from {$from} to {$to}"); $dst_path = FileSystem::convertPath($to); $src_path = FileSystem::convertPath($from); + if ($src_path === $dst_path) { + return true; + } if (!copy($src_path, $dst_path)) { throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path); } From b21d5716e106419c1c772d49db968bfefbc2e6b8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 18:04:18 +0800 Subject: [PATCH 467/682] Add onig --- config/pkg/lib/onig.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/pkg/lib/onig.yml b/config/pkg/lib/onig.yml index fa06524dc..c2ef658af 100644 --- a/config/pkg/lib/onig.yml +++ b/config/pkg/lib/onig.yml @@ -13,3 +13,6 @@ onig: - oniguruma.h static-libs@unix: - libonig.a + static-libs@windows: + - onig.lib + - onig_a.lib From 9f3e353699b1868f59fc4e7dea98ceb65ee73017 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 18:04:30 +0800 Subject: [PATCH 468/682] Add bzip2 --- config/pkg/lib/bzip2.yml | 3 +++ src/Package/Library/bzip2.php | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/config/pkg/lib/bzip2.yml b/config/pkg/lib/bzip2.yml index 1cd36bd7e..f9e1870d8 100644 --- a/config/pkg/lib/bzip2.yml +++ b/config/pkg/lib/bzip2.yml @@ -16,3 +16,6 @@ bzip2: - bzlib.h static-libs@unix: - libbz2.a + static-libs@windows: + - libbz2.lib + - libbz2_a.lib diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php index 403773dab..90fcce7c6 100644 --- a/src/Package/Library/bzip2.php +++ b/src/Package/Library/bzip2.php @@ -20,6 +20,17 @@ public function patchBeforeBuild(LibraryPackage $lib): void FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); } + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $package): void + { + cmd()->cd($package->getSourceDir()) + ->exec('nmake /nologo /f Makefile.msc CFLAGS="-DWIN32 -MT -Ox -D_FILE_OFFSET_BITS=64 -nologo" clean') + ->exec('nmake /nologo /f Makefile.msc CFLAGS="-DWIN32 -MT -Ox -D_FILE_OFFSET_BITS=64 -nologo" lib'); + FileSystem::copy("{$package->getSourceDir()}\\libbz2.lib", "{$package->getLibDir()}\\libbz2.lib"); + FileSystem::copy("{$package->getSourceDir()}\\libbz2.lib", "{$package->getLibDir()}\\libbz2_a.lib"); + FileSystem::copy("{$package->getSourceDir()}\\bzlib.h", "{$package->getIncludeDir()}\\bzlib.h"); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib, PackageBuilder $builder): void From 03cd7e141ceb6ee0674b3a8c8e52066c028b0472 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 18:09:29 +0800 Subject: [PATCH 469/682] Add brotli libs --- config/pkg/lib/brotli.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/pkg/lib/brotli.yml b/config/pkg/lib/brotli.yml index 524f9ddc8..c88b93b5a 100644 --- a/config/pkg/lib/brotli.yml +++ b/config/pkg/lib/brotli.yml @@ -15,3 +15,7 @@ brotli: - libbrotlicommon - libbrotlidec - libbrotlienc + static-libs@windows: + - brotlicommon.lib + - brotlidec.lib + - brotlienc.lib From 54915028d75f2efbc79f4814ea4d43c979692f2b Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 20 Mar 2026 19:35:51 +0800 Subject: [PATCH 470/682] Fix zlib produced lib file names from different zlib version (#1066) --- src/SPC/builder/windows/library/zlib.php | 21 ++++++++++++++++++--- src/globals/test-extensions.php | 18 +++++++++--------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/SPC/builder/windows/library/zlib.php b/src/SPC/builder/windows/library/zlib.php index 03fd033b9..e9f960233 100644 --- a/src/SPC/builder/windows/library/zlib.php +++ b/src/SPC/builder/windows/library/zlib.php @@ -31,8 +31,23 @@ protected function build(): void $this->builder->makeSimpleWrapper('cmake'), "--build build --config Release --target install -j{$this->builder->concurrency}" ); - copy(BUILD_LIB_PATH . '\zlibstatic.lib', BUILD_LIB_PATH . '\zlib_a.lib'); - unlink(BUILD_ROOT_PATH . '\bin\zlib.dll'); - unlink(BUILD_LIB_PATH . '\zlib.lib'); + $detect_list = [ + 'zlibstatic.lib', + 'zs.lib', + 'libzs.lib', + ]; + foreach ($detect_list as $item) { + if (file_exists(BUILD_LIB_PATH . '\\' . $item)) { + FileSystem::copy(BUILD_LIB_PATH . '\\' . $item, BUILD_LIB_PATH . '\zlib_a.lib'); + FileSystem::copy(BUILD_LIB_PATH . '\\' . $item, BUILD_LIB_PATH . '\zlibstatic.lib'); + break; + } + } + FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '\bin\zlib.dll'); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\zlib.lib'); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\libz.dll'); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\libz.lib'); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\z.lib'); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\z.dll'); } } diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 3af2b55d3..8de87d56b 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,7 +13,7 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - // '8.1', + '8.1', // '8.2', // '8.3', '8.4', @@ -23,15 +23,15 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-15-intel', // bin/spc for x86_64 - 'macos-15', // bin/spc for arm64 + // 'macos-15-intel', // bin/spc for x86_64 + // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - 'ubuntu-24.04', // bin/spc for x86_64 - 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - 'ubuntu-24.04-arm', // bin/spc for arm64 + // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + // 'ubuntu-24.04', // bin/spc for x86_64 + // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - // 'windows-2025', + 'windows-2025', // .\bin\spc.ps1 ]; // whether enable thread safe @@ -51,7 +51,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { 'Linux', 'Darwin' => 'pdo_odbc', - 'Windows' => 'com_dotnet', + 'Windows' => 'zlib,phar,mbstring,mbregex,sockets', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). From b970bf8e3a2616fcfde6b67c5e73cf5f86cf76e4 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 20 Mar 2026 20:00:42 +0800 Subject: [PATCH 471/682] Fix gd build on PHP 8.5 (#1043) --- src/SPC/store/SourcePatcher.php | 8 ++- src/globals/extra/gd_config_85.w32 | 94 ++++++++++++++++++++++++++++++ src/globals/test-extensions.php | 12 ++-- 3 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 src/globals/extra/gd_config_85.w32 diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index 233a2e157..7ce2d2002 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -634,7 +634,13 @@ public static function patchGDWin32(): bool FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gd/libgd/gdft.c', '#ifndef MSWIN32', '#ifndef _WIN32'); } // custom config.w32, because official config.w32 is hard-coded many things - $origin = $ver_id >= 80100 ? file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32') : file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32'); + if ($ver_id >= 80500) { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_85.w32'); + } elseif ($ver_id >= 80100) { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32'); + } else { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32'); + } file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32.bak', file_get_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32')); return file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32', $origin) !== false; } diff --git a/src/globals/extra/gd_config_85.w32 b/src/globals/extra/gd_config_85.w32 new file mode 100644 index 000000000..e980b003e --- /dev/null +++ b/src/globals/extra/gd_config_85.w32 @@ -0,0 +1,94 @@ +// vim:ft=javascript + +ARG_WITH("gd", "Bundled GD support", "yes"); + +if (PHP_GD != "no") { + // check for gd.h (required) + if (!CHECK_HEADER_ADD_INCLUDE("gd.h", "CFLAGS_GD", PHP_GD + ";ext\\gd\\libgd")) { + ERROR("gd not enabled; libraries and headers not found"); + } + + // zlib ext support (required) + if (!CHECK_LIB("zlib_a.lib;zlib.lib", "gd", PHP_GD)) { + ERROR("gd not enabled; zlib not enabled"); + } + + // libjpeg lib support + if (CHECK_LIB("libjpeg_a.lib;libjpeg.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("jpeglib.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include")) { + AC_DEFINE("HAVE_LIBJPEG", 1, "JPEG support"); + AC_DEFINE("HAVE_GD_JPG", 1, "JPEG support"); + } + + // libpng16 lib support + if (CHECK_LIB("libpng_a.lib;libpng.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("png.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\libpng16")) { + AC_DEFINE("HAVE_LIBPNG", 1, "PNG support"); + AC_DEFINE("HAVE_GD_PNG", 1, "PNG support"); + } + + // freetype lib support + if (CHECK_LIB("libfreetype_a.lib;libfreetype.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("ft2build.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\freetype2;" + PHP_PHP_BUILD + "\\include\\freetype")) { + AC_DEFINE("HAVE_LIBFREETYPE", 1, "FreeType support"); + AC_DEFINE("HAVE_GD_FREETYPE", 1, "FreeType support"); + } + + // xpm lib support + if (CHECK_LIB("libXpm_a.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("xpm.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\X11")) { + AC_DEFINE("HAVE_LIBXPM", 1, "XPM support"); + AC_DEFINE("HAVE_GD_XPM", 1, "XPM support"); + } + + // iconv lib support + if ((CHECK_LIB("libiconv_a.lib;libiconv.lib", "gd", PHP_GD) || CHECK_LIB("iconv_a.lib;iconv.lib", "gd", PHP_GD)) && + CHECK_HEADER_ADD_INCLUDE("iconv.h", "CFLAGS_GD", PHP_GD)) { + AC_DEFINE("HAVE_LIBICONV", 1, "Iconv support"); + } + + // libwebp lib support + if ((CHECK_LIB("libwebp_a.lib", "gd", PHP_GD) || CHECK_LIB("libwebp.lib", "gd", PHP_GD)) && + CHECK_LIB("libsharpyuv.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("decode.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp") && + CHECK_HEADER_ADD_INCLUDE("encode.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp")) { + AC_DEFINE("HAVE_LIBWEBP", 1, "WebP support"); + AC_DEFINE("HAVE_GD_WEBP", 1, "WebP support"); + } + + // libavif lib support + if (CHECK_LIB("avif_a.lib", "gd", PHP_GD) && + CHECK_LIB("aom_a.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("avif.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\avif")) { + ADD_FLAG("CFLAGS_GD", "/D HAVE_LIBAVIF /D HAVE_GD_AVIF"); + } else if (CHECK_LIB("avif.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("avif.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\avif")) { + ADD_FLAG("CFLAGS_GD", "/D HAVE_LIBAVIF /D HAVE_GD_AVIF"); + } + + CHECK_LIB("User32.lib", "gd", PHP_GD); + CHECK_LIB("Gdi32.lib", "gd", PHP_GD); + + EXTENSION("gd", "gd.c", null, "-Iext/gd/libgd"); + ADD_SOURCES("ext/gd/libgd", "gd.c \ + gdcache.c gdfontg.c gdfontl.c gdfontmb.c gdfonts.c gdfontt.c \ + gdft.c gd_gd2.c gd_gd.c gd_gif_in.c gd_gif_out.c gdhelpers.c gd_io.c gd_io_dp.c \ + gd_io_file.c gd_io_ss.c gd_jpeg.c gdkanji.c gd_png.c gd_ss.c \ + gdtables.c gd_topal.c gd_wbmp.c gdxpm.c wbmp.c gd_xbm.c gd_security.c gd_transform.c \ + gd_filter.c gd_rotate.c gd_color_match.c gd_webp.c gd_avif.c \ + gd_crop.c gd_interpolation.c gd_matrix.c gd_bmp.c gd_tga.c", "gd"); + + AC_DEFINE('HAVE_LIBGD', 1, 'GD support'); + AC_DEFINE('HAVE_GD_BUNDLED', 1, "Bundled GD"); + AC_DEFINE('HAVE_GD_BMP', 1, "BMP support"); + AC_DEFINE('HAVE_GD_TGA', 1, "TGA support"); + ADD_FLAG("CFLAGS_GD", " \ +/D PHP_GD_EXPORTS=1 \ +/D HAVE_GD_GET_INTERPOLATION \ + "); + if (ICC_TOOLSET) { + ADD_FLAG("LDFLAGS_GD", "/nodefaultlib:libcmt"); + } + + PHP_INSTALL_HEADERS("", "ext/gd ext/gd/libgd"); +} diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 8de87d56b..c5a9d10b2 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -31,11 +31,11 @@ // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', // .\bin\spc.ps1 + 'windows-2025', ]; // whether enable thread safe -$zts = false; +$zts = true; $no_strip = false; @@ -50,8 +50,8 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'pdo_odbc', - 'Windows' => 'zlib,phar,mbstring,mbregex,sockets', + 'Linux', 'Darwin' => 'pgsql', + 'Windows' => 'gd,zlib,mbstring,filter', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). @@ -74,7 +74,7 @@ // You can use `common`, `bulk`, `minimal` or `none`. // note: combination is only available for *nix platform. Windows must use `none` combination $base_combination = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'bulk', + 'Linux', 'Darwin' => 'minimal', 'Windows' => 'none', }; @@ -89,7 +89,7 @@ function _getCombination(string $type = 'common'): string 'common' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,gd,gmp,iconv,xml,mbstring,mbregex,' . 'mysqlnd,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,redis,session,simplexml,soap,sockets,' . 'sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip', - 'bulk' => 'apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,event,exif,fileinfo,filter,ftp,gd,gmp,iconv,imagick,' . + 'bulk' => 'apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,event,exif,fileinfo,filter,ftp,gd,gmp,iconv,imagick,imap,' . 'intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,' . 'posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,swoole,sysvmsg,sysvsem,' . 'sysvshm,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib', From 295df194846bf7d6385836e9dd1fae0494fa4c9b Mon Sep 17 00:00:00 2001 From: Nils Silbernagel <6422477+N-Silbernagel@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:37:25 +0100 Subject: [PATCH 472/682] Add shared-extensions, frankenphp and zts to build-unix workflow (#1062) Co-authored-by: crazywhalecc --- .github/workflows/build-unix.yml | 79 +++++++++++++++++++++++++- config/env.ini | 2 + docs/en/guide/action-build.md | 6 +- docs/zh/guide/action-build.md | 4 +- src/SPC/builder/linux/LinuxBuilder.php | 2 +- 5 files changed, 87 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index bf6df9ac4..eee9d0ae6 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -29,6 +29,9 @@ on: description: Extensions to build (comma separated) required: true type: string + shared-extensions: + description: Shared extensions to build (optional, comma separated) + type: string extra-libs: description: Extra libraries to build (optional, comma separated) type: string @@ -42,6 +45,14 @@ on: build-fpm: description: Build fpm binary type: boolean + build-frankenphp: + description: Build frankenphp binary (requires ZTS) + type: boolean + default: false + enable-zts: + description: Enable ZTS + type: boolean + default: false prefer-pre-built: description: Prefer pre-built binaries (reduce build time) type: boolean @@ -73,6 +84,9 @@ on: description: Extensions to build (comma separated) required: true type: string + shared-extensions: + description: Shared extensions to build (optional, comma separated) + type: string extra-libs: description: Extra libraries to build (optional, comma separated) type: string @@ -86,6 +100,14 @@ on: build-fpm: description: Build fpm binary type: boolean + build-frankenphp: + description: Build frankenphp binary (requires ZTS) + type: boolean + default: false + enable-zts: + description: Enable ZTS + type: boolean + default: false prefer-pre-built: description: Prefer pre-built binaries (reduce build time) type: boolean @@ -152,8 +174,19 @@ jobs: RUNS_ON="macos-15" ;; esac - DOWN_CMD="$DOWN_CMD --with-php=${{ inputs.php-version }} --for-extensions=${{ inputs.extensions }} --ignore-cache-sources=php-src" - BUILD_CMD="$BUILD_CMD ${{ inputs.extensions }}" + STATIC_EXTS="${{ inputs.extensions }}" + SHARED_EXTS="${{ inputs['shared-extensions'] }}" + BUILD_FRANKENPHP="${{ inputs['build-frankenphp'] }}" + ENABLE_ZTS="${{ inputs['enable-zts'] }}" + ALL_EXTS="$STATIC_EXTS" + if [ -n "$SHARED_EXTS" ]; then + ALL_EXTS="$ALL_EXTS,$SHARED_EXTS" + fi + DOWN_CMD="$DOWN_CMD --with-php=${{ inputs.php-version }} --for-extensions=$ALL_EXTS --ignore-cache-sources=php-src" + BUILD_CMD="$BUILD_CMD $STATIC_EXTS" + if [ -n "$SHARED_EXTS" ]; then + BUILD_CMD="$BUILD_CMD --build-shared=$SHARED_EXTS" + fi if [ -n "${{ inputs.extra-libs }}" ]; then DOWN_CMD="$DOWN_CMD --for-libs=${{ inputs.extra-libs }}" BUILD_CMD="$BUILD_CMD --with-libs=${{ inputs.extra-libs }}" @@ -177,6 +210,12 @@ jobs: if [ ${{ inputs.build-fpm }} == true ]; then BUILD_CMD="$BUILD_CMD --build-fpm" fi + if [ "$BUILD_FRANKENPHP" = "true" ]; then + BUILD_CMD="$BUILD_CMD --build-frankenphp" + fi + if [ "$ENABLE_ZTS" = "true" ]; then + BUILD_CMD="$BUILD_CMD --enable-zts" + fi echo 'download='"$DOWN_CMD" >> "$GITHUB_OUTPUT" echo 'build='"$BUILD_CMD" >> "$GITHUB_OUTPUT" echo 'run='"$RUNS_ON" >> "$GITHUB_OUTPUT" @@ -199,6 +238,27 @@ jobs: env: phpts: nts + - if: ${{ inputs['build-frankenphp'] == true }} + name: "Install go-xcaddy for FrankenPHP" + run: | + case "${{ inputs.os }}" in + linux-x86_64|linux-aarch64) + ./bin/spc-alpine-docker install-pkg go-xcaddy + ;; + linux-x86_64-glibc|linux-aarch64-glibc) + ./bin/spc-gnu-docker install-pkg go-xcaddy + ;; + macos-x86_64|macos-aarch64) + composer update --no-dev --classmap-authoritative + ./bin/spc doctor --auto-fix + ./bin/spc install-pkg go-xcaddy + ;; + *) + echo "Unsupported OS for go-xcaddy install: ${{ inputs.os }}" + exit 1 + ;; + esac + # Cache downloaded source - id: cache-download uses: actions/cache@v4 @@ -245,7 +305,22 @@ jobs: name: php-fpm-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/php-fpm + # Upload frankenphp executable + - if: ${{ inputs['build-frankenphp'] == true }} + name: "Upload FrankenPHP SAPI" + uses: actions/upload-artifact@v4 + with: + name: php-frankenphp-${{ inputs.php-version }}-${{ inputs.os }} + path: buildroot/bin/frankenphp + # Upload extensions metadata + - if: ${{ inputs['shared-extensions'] != '' }} + name: "Upload shared extensions" + uses: actions/upload-artifact@v4 + with: + name: php-shared-ext-${{ inputs.php-version }}-${{ inputs.os }} + path: | + buildroot/modules/*.so - uses: actions/upload-artifact@v4 name: "Upload License Files" with: diff --git a/config/env.ini b/config/env.ini index a16bc160f..25a20e896 100644 --- a/config/env.ini +++ b/config/env.ini @@ -148,6 +148,8 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable- SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" ; minimum compatible macOS version (LLVM vars, availability not guaranteed) MACOSX_DEPLOYMENT_TARGET=12.0 diff --git a/docs/en/guide/action-build.md b/docs/en/guide/action-build.md index 7d4bba327..b22549569 100644 --- a/docs/en/guide/action-build.md +++ b/docs/en/guide/action-build.md @@ -16,8 +16,10 @@ while also defining the extensions to compile. 1. Fork project. 2. Go to the Actions of the project and select `CI`. -3. Select `Run workflow`, fill in the PHP version you want to compile, the target type, and the list of extensions. (extensions comma separated, e.g. `bcmath,curl,mbstring`) -4. After waiting for about a period of time, enter the corresponding task and get `Artifacts`. +3. Select `Run workflow`, fill in the PHP version you want to compile, the target type, and the list of static extensions. (comma separated, e.g. `bcmath,curl,mbstring`) +4. If you need shared extensions (for example `xdebug`), set `shared-extensions` (comma separated, e.g. `xdebug`). +5. If you need FrankenPHP, enable `build-frankenphp` and also enable `enable-zts`. +6. After waiting for about a period of time, enter the corresponding task and get `Artifacts`. If you enable `debug`, all logs will be output at build time, including compiled logs, for troubleshooting. diff --git a/docs/zh/guide/action-build.md b/docs/zh/guide/action-build.md index 11f382d5e..7adcc456b 100644 --- a/docs/zh/guide/action-build.md +++ b/docs/zh/guide/action-build.md @@ -14,7 +14,9 @@ Action 构建指的是直接使用 GitHub Action 进行编译。 1. Fork 本项目。 2. 进入项目的 Actions,选择 CI 开头的 Workflow(根据你需要的操作系统选择)。 3. 选择 `Run workflow`,填入你要编译的 PHP 版本、目标类型、扩展列表。(扩展列表使用英文逗号分割,例如 `bcmath,curl,mbstring`) -4. 等待大约一段时间后,进入对应的任务中,获取 `Artifacts`。 +4. 如果需要共享扩展(例如 `xdebug`),请设置 `shared-extensions`(使用英文逗号分割,例如 `xdebug`)。 +5. 如果需要 FrankenPHP,请启用 `build-frankenphp`,同时也需要启用 `enable-zts`。 +6. 等待大约一段时间后,进入对应的任务中,获取 `Artifacts`。 如果你选择了 `debug`,则会在构建时输出所有日志,包括编译的日志,以供排查错误。 diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 004c37def..d959aade6 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -162,7 +162,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void 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`.' + 'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc` or use SPC_MUSL_DYNAMIC=true on alpine.' ); } logger()->info('Building shared extensions...'); From d7d41f4d897312aba0382757fd6a64a7e2f9d41a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 21:47:27 +0800 Subject: [PATCH 473/682] Add openssl --- config/pkg/lib/openssl.yml | 3 ++ src/Package/Library/openssl.php | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/config/pkg/lib/openssl.yml b/config/pkg/lib/openssl.yml index 22d065088..161cdcebc 100644 --- a/config/pkg/lib/openssl.yml +++ b/config/pkg/lib/openssl.yml @@ -21,3 +21,6 @@ openssl: static-libs@unix: - libssl.a - libcrypto.a + static-libs@windows: + - libssl.lib + - libcrypto.lib diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php index 541b6145f..a01d01b0b 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -6,13 +6,69 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\Validate; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\System\LinuxUtil; +use StaticPHP\Util\System\WindowsUtil; #[Library('openssl')] class openssl { + #[Validate] + public function validate(): void + { + if (SystemTarget::getTargetOS() === 'Windows') { + global $argv; + $perl_path_native = PKG_ROOT_PATH . '\strawberry-perl-' . arch2gnu(php_uname('m')) . '-win\perl\bin\perl.exe'; + $perl = file_exists($perl_path_native) ? ($perl_path_native) : WindowsUtil::findCommand('perl.exe'); + if ($perl === null) { + throw new EnvironmentException( + 'You need to install perl first!', + "Please run \"{$argv[0]} doctor\" to fix the environment.", + ); + } + ApplicationContext::set('perl', $perl); + } + } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + $perl = ApplicationContext::get('perl'); + $cmd = cmd()->cd($lib->getSourceDir()) + ->exec( + "{$perl} Configure zlib VC-WIN64A " . + 'no-shared ' . + '--prefix=' . quote($lib->getBuildRootPath()) . ' ' . + '--with-zlib-lib=' . quote($lib->getLibDir()) . ' ' . + '--with-zlib-include=' . quote($lib->getIncludeDir()) . ' ' . + '--release ' . + 'no-legacy ' + ); + + // patch zlib + FileSystem::replaceFileStr("{$lib->getSourceDir()}\\Makefile", 'ZLIB1', 'zlibstatic.lib'); + // patch debug: https://stackoverflow.com/questions/18486243/how-do-i-build-openssl-statically-linked-against-windows-runtime + FileSystem::replaceFileStr("{$lib->getSourceDir()}\\Makefile", '/debug', '/incremental:no /opt:icf /dynamicbase /nxcompat /ltcg /nodefaultlib:msvcrt'); + + // build + $cmd->exec("nmake install_dev CNF_LDFLAGS=\"/NODEFAULTLIB:kernel32.lib /NODEFAULTLIB:msvcrt /NODEFAULTLIB:msvcrtd /DEFAULTLIB:libcmt /LIBPATH:{$lib->getLibDir()} zlibstatic.lib\""); + + // copy necessary c files + FileSystem::copy("{$lib->getSourceDir()}\\ms\\applink.c", "{$lib->getIncludeDir()}\\openssl\\applink.c"); + + // patch cmake outputs + FileSystem::replaceFileRegex( + "{$lib->getLibDir()}\\cmake\\OpenSSL\\OpenSSLConfig.cmake", + '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', + 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}" ws2_32.lib gdi32.lib advapi32.lib crypt32.lib user32.lib)' + ); + } + #[BuildFor('Darwin')] public function buildForDarwin(LibraryPackage $pkg): void { From 94c579a4535144f70c66cabb433f9c3f0ad8b9eb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 21:48:46 +0800 Subject: [PATCH 474/682] Add libssh2 --- config/pkg/lib/libssh2.yml | 2 ++ src/Package/Library/libssh2.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/config/pkg/lib/libssh2.yml b/config/pkg/lib/libssh2.yml index 8e1d82754..2916e3a9c 100644 --- a/config/pkg/lib/libssh2.yml +++ b/config/pkg/lib/libssh2.yml @@ -20,3 +20,5 @@ libssh2: - libssh2 static-libs@unix: - libssh2.a + static-libs@windows: + - libssh2.lib diff --git a/src/Package/Library/libssh2.php b/src/Package/Library/libssh2.php index f71d508ac..b41434e09 100644 --- a/src/Package/Library/libssh2.php +++ b/src/Package/Library/libssh2.php @@ -8,10 +8,22 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('libssh2')] class libssh2 { + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DENABLE_ZLIB_COMPRESSION=ON', + '-DBUILD_TESTING=OFF' + ) + ->build(); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void From 19bfb6bc83ac19291b27f79eb7fc1ba658878879 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:29:35 +0800 Subject: [PATCH 475/682] Add zstd --- config/pkg/lib/zstd.yml | 2 ++ src/Package/Library/zstd.php | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/config/pkg/lib/zstd.yml b/config/pkg/lib/zstd.yml index 3d76e270c..875380d1f 100644 --- a/config/pkg/lib/zstd.yml +++ b/config/pkg/lib/zstd.yml @@ -17,3 +17,5 @@ zstd: - libzstd static-libs@unix: - libzstd.a + static-libs@windows: + - zstd_static.lib diff --git a/src/Package/Library/zstd.php b/src/Package/Library/zstd.php index ab538358e..8cbf7aa62 100644 --- a/src/Package/Library/zstd.php +++ b/src/Package/Library/zstd.php @@ -8,10 +8,24 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('zstd')] class zstd { + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $package): void + { + WindowsCMakeExecutor::create($package) + ->setRootDir("{$package->getSourceDir()}/build/cmake") + ->setBuildDir("{$package->getSourceDir()}/build/cmake/build") + ->addConfigureArgs( + '-DZSTD_BUILD_STATIC=ON', + '-DZSTD_BUILD_SHARED=OFF', + ) + ->build(); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void From d1ec473f212a7fb11f8ffe7bf3d78f5ef7abe9e1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:30:15 +0800 Subject: [PATCH 476/682] Allow set current working dir --- src/Package/Library/zstd.php | 2 +- .../Runtime/Executor/WindowsCMakeExecutor.php | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Package/Library/zstd.php b/src/Package/Library/zstd.php index 8cbf7aa62..f12bf3e02 100644 --- a/src/Package/Library/zstd.php +++ b/src/Package/Library/zstd.php @@ -17,7 +17,7 @@ class zstd public function buildWin(LibraryPackage $package): void { WindowsCMakeExecutor::create($package) - ->setRootDir("{$package->getSourceDir()}/build/cmake") + ->setWorkingDir("{$package->getSourceDir()}/build/cmake") ->setBuildDir("{$package->getSourceDir()}/build/cmake/build") ->addConfigureArgs( '-DZSTD_BUILD_STATIC=ON', diff --git a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php index 9e0978196..1f057f126 100644 --- a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php @@ -176,6 +176,12 @@ public function getConfigureArgsString(): string return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs())); } + public function setWorkingDir(string $dir): static + { + $this->cmd = $this->cmd->cd($dir); + return $this; + } + /** * Returns the default CMake args. */ @@ -207,12 +213,12 @@ private function makeCmakeToolchainFile(): string private function initBuildDir(): void { if ($this->build_dir === null) { - $this->build_dir = "{$this->package->getSourceDir()}\\build"; + $this->build_dir = "{$this->package->getSourceRoot()}\\build"; } } private function initCmd(): void { - $this->cmd = cmd()->cd($this->package->getSourceDir()); + $this->cmd = cmd()->cd($this->package->getSourceRoot()); } } From 1213cb578e88c892fdb0c699a6593b4f3b8a8dbb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:30:30 +0800 Subject: [PATCH 477/682] Refactor WindowsUtil to cache Visual Studio search results and add CMake find modules --- src/StaticPHP/Util/System/WindowsUtil.php | 54 +++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index a6df41564..9ed7a7b5f 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -8,8 +8,10 @@ class WindowsUtil { + private static array|false|null $vsCache = null; + /** - * Find windows program using executable name. + * Find Windows program using executable name. * * @param string $name command name (xxx.exe) * @param array $paths search path (default use env path) @@ -39,6 +41,10 @@ public static function findCommand(string $name, array $paths = []): ?string */ public static function findVisualStudio(): array|false { + if (self::$vsCache !== null) { + return self::$vsCache; + } + // call vswhere (need VS and C++ tools installed), output is json $vswhere_exec = PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'vswhere.exe'; $args = [ @@ -49,13 +55,13 @@ public static function findVisualStudio(): array|false $cmd = escapeshellarg($vswhere_exec) . ' ' . implode(' ', $args); $result = f_exec($cmd, $out, $code); if ($code !== 0 || !$result) { - return false; + return self::$vsCache = false; } $json = json_decode(implode("\n", $out), true); if (!is_array($json) || count($json) === 0) { - return false; + return self::$vsCache = false; } - return [ + return self::$vsCache = [ 'version' => $json[0]['installationVersion'], 'major_version' => explode('.', $json[0]['installationVersion'])[0], 'dir' => $json[0]['installationPath'], @@ -89,6 +95,7 @@ public static function makeCmakeToolchainFile(?string $cflags = null, ?string $l $ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'; } $buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH); + $source = str_replace('\\', '/', SOURCE_PATH); $toolchain = << Date: Sat, 21 Mar 2026 00:31:02 +0800 Subject: [PATCH 478/682] Add ngtcp2, remove suggested libs --- config/pkg/lib/ngtcp2.yml | 5 ++--- src/Package/Library/ngtcp2.php | 29 +++++++++++++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/config/pkg/lib/ngtcp2.yml b/config/pkg/lib/ngtcp2.yml index c864739a7..8984ca729 100644 --- a/config/pkg/lib/ngtcp2.yml +++ b/config/pkg/lib/ngtcp2.yml @@ -11,9 +11,6 @@ ngtcp2: license: MIT depends: - openssl - suggests: - - nghttp3 - - brotli headers: - ngtcp2 pkg-configs: @@ -22,3 +19,5 @@ ngtcp2: static-libs@unix: - libngtcp2.a - libngtcp2_crypto_ossl.a + static-libs@windows: + - ngtcp2.lib diff --git a/src/Package/Library/ngtcp2.php b/src/Package/Library/ngtcp2.php index 15821225b..c88b643bf 100644 --- a/src/Package/Library/ngtcp2.php +++ b/src/Package/Library/ngtcp2.php @@ -8,10 +8,27 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('ngtcp2')] class ngtcp2 { + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DENABLE_SHARED_LIB=OFF', + '-DENABLE_STATIC_LIB=ON', + '-DBUILD_STATIC_LIBS=ON', + '-DBUILD_SHARED_LIBS=OFF', + '-DENABLE_STATIC_CRT=ON', + '-DENABLE_LIB_ONLY=ON', + '-DENABLE_OPENSSL=ON', + ) + ->build(); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void @@ -26,18 +43,6 @@ public function build(LibraryPackage $lib): void ]), '--with-openssl=no' ) - ->optionalPackage('nghttp3', ...ac_with_args('libnghttp3', true)) - ->optionalPackage( - 'brotli', - fn (LibraryPackage $brotli) => implode(' ', [ - '--with-brotlidec=yes', - "LIBBROTLIDEC_CFLAGS=\"-I{$brotli->getIncludeDir()}\"", - "LIBBROTLIDEC_LIBS=\"{$brotli->getStaticLibFiles()}\"", - '--with-libbrotlienc=yes', - "LIBBROTLIENC_CFLAGS=\"-I{$brotli->getIncludeDir()}\"", - "LIBBROTLIENC_LIBS=\"{$brotli->getStaticLibFiles()}\"", - ]) - ) ->appendEnv(['PKG_CONFIG' => '$PKG_CONFIG --static']) ->configure('--enable-lib-only') ->make(); From 893f6404691ea0dafd22a2eeaab86eae5106515e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:31:16 +0800 Subject: [PATCH 479/682] Add nghttp3 --- config/pkg/lib/nghttp3.yml | 2 ++ src/Package/Library/nghttp3.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/pkg/lib/nghttp3.yml b/config/pkg/lib/nghttp3.yml index f9adc05b5..272172b99 100644 --- a/config/pkg/lib/nghttp3.yml +++ b/config/pkg/lib/nghttp3.yml @@ -17,3 +17,5 @@ nghttp3: - libnghttp3 static-libs@unix: - libnghttp3.a + static-libs@windows: + - nghttp3.lib diff --git a/src/Package/Library/nghttp3.php b/src/Package/Library/nghttp3.php index 1f686b7b5..4659c5711 100644 --- a/src/Package/Library/nghttp3.php +++ b/src/Package/Library/nghttp3.php @@ -8,10 +8,26 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('nghttp3')] class nghttp3 { + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DENABLE_SHARED_LIB=OFF', + '-DENABLE_STATIC_LIB=ON', + '-DBUILD_STATIC_LIBS=ON', + '-DBUILD_SHARED_LIBS=OFF', + '-DENABLE_STATIC_CRT=ON', + '-DENABLE_LIB_ONLY=ON', + ) + ->build(); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void From 92861669c25add6d27d879c6b245810ff2829415 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:31:30 +0800 Subject: [PATCH 480/682] Add nghttp2 --- config/pkg/lib/nghttp2.yml | 2 ++ src/Package/Library/nghttp2.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/pkg/lib/nghttp2.yml b/config/pkg/lib/nghttp2.yml index 166c33ac5..11521d5a3 100644 --- a/config/pkg/lib/nghttp2.yml +++ b/config/pkg/lib/nghttp2.yml @@ -22,3 +22,5 @@ nghttp2: - libnghttp2 static-libs@unix: - libnghttp2.a + static-libs@windows: + - nghttp2.lib diff --git a/src/Package/Library/nghttp2.php b/src/Package/Library/nghttp2.php index 3a85ada4a..f09659470 100644 --- a/src/Package/Library/nghttp2.php +++ b/src/Package/Library/nghttp2.php @@ -8,10 +8,26 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('nghttp2')] class nghttp2 { + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DENABLE_SHARED_LIB=OFF', + '-DENABLE_STATIC_LIB=ON', + '-DENABLE_STATIC_CRT=ON', + '-DENABLE_LIB_ONLY=ON', + '-DENABLE_DOC=OFF', + '-DBUILD_TESTING=OFF', + ) + ->build(); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void From 7df861696d28028fd9d2d4f00328e4e109e6d114 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:31:45 +0800 Subject: [PATCH 481/682] Add libxml2 --- config/pkg/lib/libxml2.yml | 6 ++++++ src/Package/Library/libxml2.php | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index 7e86b5af5..5ffdc8b27 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -12,7 +12,13 @@ libxml2: - libiconv - zlib - xz + depends@windows: + - zlib + - libiconv-win headers: - libxml2 pkg-configs: - libxml-2.0 + static-libs@windows: + - libxml2s.lib + - libxml2_a.lib diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php index 3f8b3e71f..108b9962f 100644 --- a/src/Package/Library/libxml2.php +++ b/src/Package/Library/libxml2.php @@ -7,12 +7,33 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Util\FileSystem; #[Library('libxml2')] class libxml2 { + #[BuildFor('Windows')] + public function buildForWindows(LibraryPackage $lib, PackageInstaller $installer): void + { + $iconv_win = $installer->getLibraryPackage('libiconv-win'); + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DLIBXML2_WITH_ICONV=ON', + "-DIconv_LIBRARY={$iconv_win->getLibDir()}", + "-DIconv_INCLUDE_DIR={$iconv_win->getIncludeDir()}", + '-DLIBXML2_WITH_ZLIB=ON', + '-DLIBXML2_WITH_PYTHON=OFF', + '-DLIBXML2_WITH_LZMA=OFF', + '-DLIBXML2_WITH_PROGRAMS=OFF', + '-DLIBXML2_WITH_TESTS=OFF', + ) + ->build(); + FileSystem::copy("{$lib->getLibDir()}\\libxml2s.lib", "{$lib->getLibDir()}\\libxml2_a.lib"); + } + #[BuildFor('Linux')] public function buildForLinux(LibraryPackage $lib): void { From f6e00c67ccf3d7f1fbdab4431665c07efdbeaca7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:32:02 +0800 Subject: [PATCH 482/682] Refactor LibraryPackage to skip pkg-config and static-bin checks on Windows --- src/StaticPHP/Package/LibraryPackage.php | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index aa24f057c..769612b9f 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -44,18 +44,20 @@ public function isInstalled(): bool return false; } } - foreach (PackageConfig::get($this->getName(), 'pkg-configs', []) as $pc) { - if (!str_ends_with($pc, '.pc')) { - $pc .= '.pc'; - } - if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}")) { - return false; + if (SystemTarget::getTargetOS() !== 'Windows') { + foreach (PackageConfig::get($this->getName(), 'pkg-configs', []) as $pc) { + if (!str_ends_with($pc, '.pc')) { + $pc .= '.pc'; + } + if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}")) { + return false; + } } - } - foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { - $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; - if (!file_exists($path)) { - return false; + foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { + $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; + if (!file_exists($path)) { + return false; + } } } return true; From deb979416f362a49c989e8397c6c946eed8e52bf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:33:04 +0800 Subject: [PATCH 483/682] Add libiconv-win --- config/pkg/lib/libiconv-win.yml | 13 +++++++++ src/Package/Library/libiconv_win.php | 43 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 config/pkg/lib/libiconv-win.yml create mode 100644 src/Package/Library/libiconv_win.php diff --git a/config/pkg/lib/libiconv-win.yml b/config/pkg/lib/libiconv-win.yml new file mode 100644 index 000000000..103acf2e9 --- /dev/null +++ b/config/pkg/lib/libiconv-win.yml @@ -0,0 +1,13 @@ +libiconv-win: + type: library + artifact: + source: + type: git + rev: master + url: 'https://github.com/static-php/libiconv-win.git' + metadata: + license-files: [source/COPYING] + license: GPL-3.0-or-later + static-libs@windows: + - libiconv.lib + - libiconv_a.lib diff --git a/src/Package/Library/libiconv_win.php b/src/Package/Library/libiconv_win.php new file mode 100644 index 000000000..6cd235e12 --- /dev/null +++ b/src/Package/Library/libiconv_win.php @@ -0,0 +1,43 @@ + '\MSVC17', + '16' => '\MSVC16', + default => throw new EnvironmentException("Current VS version {$ver} is not supported yet!"), + }; + ApplicationContext::set('vs_ver_dir', $vs_ver_dir); + } + + #[BuildFor('Windows')] + public function build(LibraryPackage $lib): void + { + $vs_ver_dir = ApplicationContext::get('vs_ver_dir'); + cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}") + ->exec('msbuild libiconv.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64'); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\x64\\lib\\libiconv.lib", "{$lib->getLibDir()}\\libiconv.lib"); + FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\x64\\lib\\libiconv_a.lib", "{$lib->getLibDir()}\\libiconv_a.lib"); + FileSystem::copy("{$lib->getSourceDir()}\\source\\include\\iconv.h", "{$lib->getIncludeDir()}\\iconv.h"); + } +} From 327bb8bc0f9c30a25c34dd4077349e9b48c08be6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:33:27 +0800 Subject: [PATCH 484/682] Add curl --- config/pkg/target/curl.yml | 9 +++++++++ src/Package/Target/curl.php | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/config/pkg/target/curl.yml b/config/pkg/target/curl.yml index 4daba8c14..78064510c 100644 --- a/config/pkg/target/curl.yml +++ b/config/pkg/target/curl.yml @@ -12,6 +12,10 @@ curl: depends@unix: - openssl - zlib + depends@windows: + - zlib + - libssh2 + - nghttp2 suggests@unix: - libssh2 - brotli @@ -23,6 +27,9 @@ curl: - ldap - idn2 - krb5 + suggests@windows: + - brotli + - zstd frameworks: - CoreFoundation - CoreServices @@ -33,3 +40,5 @@ curl: - curl static-libs@unix: - libcurl.a + static-libs@windows: + - libcurl_a.lib diff --git a/src/Package/Target/curl.php b/src/Package/Target/curl.php index dbfa8f7a1..43a2904b7 100644 --- a/src/Package/Target/curl.php +++ b/src/Package/Target/curl.php @@ -10,6 +10,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; @@ -20,7 +21,9 @@ class curl #[PatchDescription('Remove CMAKE_C_IMPLICIT_LINK_LIBRARIES and fix macOS framework detection')] public function patchBeforeBuild(LibraryPackage $lib): bool { - shell()->cd($lib->getSourceDir())->exec('sed -i.save s@\${CMAKE_C_IMPLICIT_LINK_LIBRARIES}@@ ./CMakeLists.txt'); + if (SystemTarget::getTargetOS() !== 'Windows') { + shell()->cd($lib->getSourceDir())->exec('sed -i.save s@\${CMAKE_C_IMPLICIT_LINK_LIBRARIES}@@ ./CMakeLists.txt'); + } if (SystemTarget::getTargetOS() === 'Darwin') { FileSystem::replaceFileRegex("{$lib->getSourceDir()}/CMakeLists.txt", '/NOT COREFOUNDATION_FRAMEWORK/m', 'FALSE'); FileSystem::replaceFileRegex("{$lib->getSourceDir()}/CMakeLists.txt", '/NOT SYSTEMCONFIGURATION_FRAMEWORK/m', 'FALSE'); @@ -29,6 +32,34 @@ public function patchBeforeBuild(LibraryPackage $lib): bool return true; } + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->optionalPackage('zstd', ...cmake_boolean_args('CURL_ZSTD')) + ->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI')) + ->addConfigureArgs( + '-DBUILD_CURL_EXE=OFF', + '-DZSTD_LIBRARY=zstd_static.lib', + '-DBUILD_TESTING=OFF', + '-DBUILD_EXAMPLES=OFF', + '-DUSE_LIBIDN2=OFF', + '-DCURL_USE_LIBPSL=OFF', + '-DUSE_WINDOWS_SSPI=ON', + '-DCURL_USE_SCHANNEL=ON', + '-DCURL_USE_OPENSSL=OFF', + '-DCURL_ENABLE_SSL=ON', + '-DUSE_NGHTTP2=ON', + '-DSHARE_LIB_OBJECT=OFF', + '-DCURL_USE_LIBSSH2=ON', + '-DENABLE_IPV6=ON', + ) + ->build(); + // move libcurl.lib to libcurl_a.lib + rename("{$lib->getLibDir()}\\libcurl.lib", "{$lib->getLibDir()}\\libcurl_a.lib"); + FileSystem::replaceFileStr("{$lib->getIncludeDir()}\\curl\\curl.h", '#ifdef CURL_STATICLIB', '#if 1'); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void From bd73b4a6dc6daee7cde1a8b23a00fca29d55adb0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 00:34:31 +0800 Subject: [PATCH 485/682] phpstan --- src/Package/Library/libiconv_win.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/libiconv_win.php b/src/Package/Library/libiconv_win.php index 6cd235e12..b6b0531df 100644 --- a/src/Package/Library/libiconv_win.php +++ b/src/Package/Library/libiconv_win.php @@ -23,7 +23,7 @@ public function validate(): void $vs_ver_dir = match ($ver['major_version']) { '17' => '\MSVC17', '16' => '\MSVC16', - default => throw new EnvironmentException("Current VS version {$ver} is not supported yet!"), + default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), }; ApplicationContext::set('vs_ver_dir', $vs_ver_dir); } From 230be2ebe818db2f70853bb668e9b7592b36a07f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 11:12:13 +0800 Subject: [PATCH 486/682] Add libpng --- config/pkg/lib/libpng.yml | 3 +++ src/Package/Library/libpng.php | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/config/pkg/lib/libpng.yml b/config/pkg/lib/libpng.yml index e8831d60c..083cf430a 100644 --- a/config/pkg/lib/libpng.yml +++ b/config/pkg/lib/libpng.yml @@ -14,3 +14,6 @@ libpng: - zlib static-libs@unix: - libpng16.a + static-libs@windows: + - libpng16_static.lib + - libpng_a.lib diff --git a/src/Package/Library/libpng.php b/src/Package/Library/libpng.php index 1d02fdd69..6a1690105 100644 --- a/src/Package/Library/libpng.php +++ b/src/Package/Library/libpng.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('libpng')] class libpng @@ -44,4 +46,21 @@ public function buildUnix(LibraryPackage $lib): void $lib->patchPkgconfPrefix(['libpng16.pc']); $lib->patchLaDependencyPrefix(); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DSKIP_INSTALL_PROGRAM=ON', + '-DSKIP_INSTALL_FILES=ON', + '-DPNG_STATIC=ON', + '-DPNG_SHARED=OFF', + '-DPNG_TESTS=OFF', + ) + ->build(); + + // libpng16_static.lib to libpng_a.lib + FileSystem::copy("{$lib->getLibDir()}\\libpng16_static.lib", "{$lib->getLibDir()}\\libpng_a.lib"); + } } From 5ff973e446920cdfb5f28244a841b23527a27724 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 11:12:24 +0800 Subject: [PATCH 487/682] Add freetype --- config/pkg/lib/freetype.yml | 1 + src/Package/Library/freetype.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/config/pkg/lib/freetype.yml b/config/pkg/lib/freetype.yml index c101a174b..df7dc22a8 100644 --- a/config/pkg/lib/freetype.yml +++ b/config/pkg/lib/freetype.yml @@ -11,6 +11,7 @@ freetype: depends: - zlib suggests: + - libpng - bzip2 - brotli headers@unix: diff --git a/src/Package/Library/freetype.php b/src/Package/Library/freetype.php index 6cb05a90a..8a83eb5e3 100644 --- a/src/Package/Library/freetype.php +++ b/src/Package/Library/freetype.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Util\FileSystem; #[Library('freetype')] @@ -33,4 +34,18 @@ public function buildUnix(LibraryPackage $lib): void $lib->patchPkgconfPrefix(['freetype2.pc']); FileSystem::replaceFileStr("{$lib->getBuildRootPath()}/lib/pkgconfig/freetype2.pc", ' -L/lib ', " -L{$lib->getBuildRootPath()}/lib "); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->optionalPackage('libpng', ...cmake_boolean_args('FT_DISABLE_PNG', true)) + ->optionalPackage('bzip2', ...cmake_boolean_args('FT_DISABLE_BZIP2', true)) + ->optionalPackage('brotli', ...cmake_boolean_args('FT_DISABLE_BROTLI', true)) + ->addConfigureArgs('-DFT_DISABLE_HARFBUZZ=ON') + ->build(); + + // freetype.lib to libfreetype_a.lib + FileSystem::copy("{$lib->getLibDir()}\\freetype.lib", "{$lib->getLibDir()}\\libfreetype_a.lib"); + } } From 520767638f779dd7cfffb38666c6f6dfa9615b67 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 11:20:05 +0800 Subject: [PATCH 488/682] Add gmssl --- src/Package/Library/gmssl.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Package/Library/gmssl.php b/src/Package/Library/gmssl.php index 21b4ea668..7785e55a4 100644 --- a/src/Package/Library/gmssl.php +++ b/src/Package/Library/gmssl.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('gmssl')] class gmssl @@ -18,4 +20,35 @@ public function build(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib)->build(); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + $buildDir = "{$lib->getSourceDir()}\\builddir"; + + // GmSSL requires NMake Makefiles generator on Windows + WindowsCMakeExecutor::create($lib) + ->setBuildDir($buildDir) + ->setCustomDefaultArgs( + '-G "NMake Makefiles"', + '-DWIN32=ON', + '-DBUILD_SHARED_LIBS=OFF', + '-DCMAKE_BUILD_TYPE=Release', + '-DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG"', + '-DCMAKE_CXX_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG"', + '-DCMAKE_INSTALL_PREFIX=' . escapeshellarg($lib->getBuildRootPath()), + '-B ' . escapeshellarg($buildDir), + ) + ->toStep(1) + ->build(); + + // fix cmake_install.cmake install prefix (GmSSL overrides it internally) + $installCmake = "{$buildDir}\\cmake_install.cmake"; + FileSystem::writeFile( + $installCmake, + 'set(CMAKE_INSTALL_PREFIX "' . str_replace('\\', '/', $lib->getBuildRootPath()) . '")' . PHP_EOL . FileSystem::readFile($installCmake) + ); + + cmd()->cd($buildDir)->exec('nmake install XCFLAGS=/MT'); + } } From bf4d227a5544fe5e9a6b6eecb0217d22d946c5b8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 12:42:47 +0800 Subject: [PATCH 489/682] Add error handling for missing vswhere.exe in WindowsUtil --- src/StaticPHP/Util/System/WindowsUtil.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index 9ed7a7b5f..b6d943be4 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -4,6 +4,7 @@ namespace StaticPHP\Util\System; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Util\FileSystem; class WindowsUtil @@ -47,6 +48,10 @@ public static function findVisualStudio(): array|false // call vswhere (need VS and C++ tools installed), output is json $vswhere_exec = PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'vswhere.exe'; + // detect vswhere exists, if not throw error + if (!file_exists($vswhere_exec)) { + throw new EnvironmentException('vswhere.exe not found, please run `doctor` command first'); + } $args = [ '-latest', '-format', 'json', From 75dd01aa918c894e8987d8c8e1fda6fc27c73807 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 12:43:39 +0800 Subject: [PATCH 490/682] Enhance package download logic to handle binary-only packages and improve error messaging for installation failures --- src/StaticPHP/Package/PackageInstaller.php | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 417c4e1b6..f5c03a7de 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -168,10 +168,23 @@ public function run(bool $disable_delay_msg = false): void // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); - $downloader = new ArtifactDownloader( - [...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))], - $this->interactive + // Collect packages that have no build stage for current OS but do have a platform binary. + // These must always download binary (not source), regardless of global prefer-source setting. + $binary_only_packages = array_filter( + $this->packages, + fn ($p) => $p instanceof LibraryPackage + && !$this->isBuildPackage($p) + && !$p->hasStage('build') + && ($p->getArtifact()?->hasPlatformBinary() ?? false) ); + $dl_opts = [ + ...$downloaderOptions, + 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages)), + ]; + if ($binary_only_packages !== []) { + $dl_opts['binary-only'] = implode(',', array_map(fn ($x) => $x->getName(), $binary_only_packages)); + } + $downloader = new ArtifactDownloader($dl_opts, $this->interactive); $downloader->addArtifacts($this->getArtifacts())->download(); } else { logger()->notice('Skipping download (--no-download option enabled)'); @@ -716,10 +729,13 @@ private function validatePackagesBeforeBuild(): void } $is_to_build = $this->isBuildPackage($package); $has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build'); - $should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false); + // Use hasPlatformBinary() here (not shouldUseBinary()) because this runs before download, + // so the binary is not yet on disk. We only need to know if a binary is declared for + // the current platform in the artifact config. + $has_platform_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->hasPlatformBinary() ?? false); // Check if package can neither be built nor installed - if (!$is_to_build && !$should_use_binary && !$has_build_stage) { + if (!$is_to_build && !$has_platform_binary && !$has_build_stage) { throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS: " . SystemTarget::getCurrentPlatformString()); } } From 43d8c9d9d62eb22a290f7226a05fdd3ba4ef26a7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 12:44:17 +0800 Subject: [PATCH 491/682] Add icu (replace old icu-static-win) --- config/pkg/lib/icu.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config/pkg/lib/icu.yml b/config/pkg/lib/icu.yml index 43328228d..e42dcb6c3 100644 --- a/config/pkg/lib/icu.yml +++ b/config/pkg/lib/icu.yml @@ -6,11 +6,20 @@ icu: repo: unicode-org/icu match: icu4c.+-src\.tgz prefer-stable: true + binary: + windows-x86_64: { type: url, url: 'https://dl.static-php.dev/static-php-cli/deps/icu-static-windows-x64/icu-static-windows-x64.zip', extract: hosted } metadata: - license-files: [LICENSE] + license-files: ['@/icu.txt'] license: ICU + headers@windows: + - unicode lang: cpp pkg-configs: - icu-uc - icu-i18n - icu-io + static-libs@windows: + - icudt.lib + - icuin.lib + - icuio.lib + - icuuc.lib From 801d41efeb1d30d3207b0e429fd9f8e388f402ac Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 17:53:49 +0800 Subject: [PATCH 492/682] Add libwebp --- config/pkg/lib/libwebp.yml | 5 +++++ src/Package/Library/libwebp.php | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/config/pkg/lib/libwebp.yml b/config/pkg/lib/libwebp.yml index 62ddddc13..24873b127 100644 --- a/config/pkg/lib/libwebp.yml +++ b/config/pkg/lib/libwebp.yml @@ -14,3 +14,8 @@ libwebp: - libwebpdemux - libwebpmux - libsharpyuv + static-libs@windows: + - libwebp.lib + - libwebpdecoder.lib + - libwebpdemux.lib + - libsharpyuv.lib diff --git a/src/Package/Library/libwebp.php b/src/Package/Library/libwebp.php index 0ee4028d5..30b3826ac 100644 --- a/src/Package/Library/libwebp.php +++ b/src/Package/Library/libwebp.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('libwebp')] class libwebp extends LibraryPackage @@ -41,4 +42,23 @@ public function buildUnix(): void $this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR); $this->patchPkgconfPrefix(['libsharpyuv.pc'], PKGCONF_PATCH_CUSTOM, ['/^includedir=.*$/m', 'includedir=${prefix}/include/webp']); } + + #[BuildFor('Windows')] + public function buildWin(): void + { + WindowsCMakeExecutor::create($this) + ->addConfigureArgs( + '-DWEBP_BUILD_EXTRAS=OFF', + '-DWEBP_BUILD_ANIM_UTILS=OFF', + '-DWEBP_BUILD_CWEBP=OFF', + '-DWEBP_BUILD_DWEBP=OFF', + '-DWEBP_BUILD_GIF2WEBP=OFF', + '-DWEBP_BUILD_IMG2WEBP=OFF', + '-DWEBP_BUILD_VWEBP=OFF', + '-DWEBP_BUILD_WEBPINFO=OFF', + '-DWEBP_BUILD_WEBPMUX=OFF', + '-DWEBP_BUILD_FUZZTEST=OFF', + ) + ->build(); + } } From 36177e4948d441eb947b6cb607e09019f8a276c7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 17:54:07 +0800 Subject: [PATCH 493/682] Add libjpeg --- config/pkg/lib/libjpeg.yml | 4 ++++ src/Package/Library/libjpeg.php | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/config/pkg/lib/libjpeg.yml b/config/pkg/lib/libjpeg.yml index 9171ad388..cca706861 100644 --- a/config/pkg/lib/libjpeg.yml +++ b/config/pkg/lib/libjpeg.yml @@ -7,6 +7,10 @@ libjpeg: metadata: license-files: [LICENSE.md] license: IJG + suggests@windows: + - zlib static-libs@unix: - libjpeg.a - libturbojpeg.a + static-libs@windows: + - libjpeg_a.lib diff --git a/src/Package/Library/libjpeg.php b/src/Package/Library/libjpeg.php index 04f4fd254..6e06bfb70 100644 --- a/src/Package/Library/libjpeg.php +++ b/src/Package/Library/libjpeg.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('libjpeg')] class libjpeg @@ -25,4 +27,20 @@ public function buildUnix(LibraryPackage $lib): void // patch pkgconfig $lib->patchPkgconfPrefix(['libjpeg.pc', 'libturbojpeg.pc']); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DENABLE_SHARED=OFF', + '-DENABLE_STATIC=ON', + '-DBUILD_TESTING=OFF', + '-DWITH_JAVA=OFF', + '-DWITH_CRT_DLL=OFF', + ) + ->optionalPackage('zlib', '-DENABLE_ZLIB_COMPRESSION=ON', '-DENABLE_ZLIB_COMPRESSION=OFF') + ->build(); + FileSystem::copy("{$lib->getLibDir()}\\jpeg-static.lib", "{$lib->getLibDir()}\\libjpeg_a.lib"); + } } From 33f33439d19e44b21320fd930ccbdbb324c4a2b9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 17:54:21 +0800 Subject: [PATCH 494/682] Add libavif --- config/pkg/lib/libavif.yml | 2 ++ src/Package/Library/libavif.php | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/config/pkg/lib/libavif.yml b/config/pkg/lib/libavif.yml index c75b05c45..eaea8542a 100644 --- a/config/pkg/lib/libavif.yml +++ b/config/pkg/lib/libavif.yml @@ -16,3 +16,5 @@ libavif: - libpng static-libs@unix: - libavif.a + static-libs@windows: + - avif.lib diff --git a/src/Package/Library/libavif.php b/src/Package/Library/libavif.php index 6db235e19..b8321375e 100644 --- a/src/Package/Library/libavif.php +++ b/src/Package/Library/libavif.php @@ -6,12 +6,25 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; #[Library('libavif')] class libavif { + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + // workaround for libavif 1.2.0 bug: MSVC does not support empty initializer list + if (SystemTarget::getTargetOS() === 'Windows') { + FileSystem::replaceFileStr($lib->getSourceDir() . '/src/read.c', 'avifFileType ftyp = {};', 'avifFileType ftyp = { 0 };'); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function buildUnix(LibraryPackage $lib): void @@ -27,4 +40,17 @@ public function buildUnix(LibraryPackage $lib): void // patch pkgconfig $lib->patchPkgconfPrefix(['libavif.pc']); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DAVIF_BUILD_APPS=OFF', + '-DAVIF_BUILD_TESTS=OFF', + '-DAVIF_LIBYUV=OFF', + '-DAVIF_ENABLE_GTEST=OFF', + ) + ->build(); + } } From a950f3d716a8c7be04efbc95ca66687257934115 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 17:54:34 +0800 Subject: [PATCH 495/682] Add libaom --- config/pkg/lib/libaom.yml | 2 ++ src/Package/Library/libaom.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/pkg/lib/libaom.yml b/config/pkg/lib/libaom.yml index 6a2dbe3cb..51d14a9b5 100644 --- a/config/pkg/lib/libaom.yml +++ b/config/pkg/lib/libaom.yml @@ -10,3 +10,5 @@ libaom: lang: cpp static-libs@unix: - libaom.a + static-libs@windows: + - aom.lib diff --git a/src/Package/Library/libaom.php b/src/Package/Library/libaom.php index 167ef0766..7e578242b 100644 --- a/src/Package/Library/libaom.php +++ b/src/Package/Library/libaom.php @@ -8,12 +8,28 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ZigToolchain; #[Library('libaom')] class libaom extends LibraryPackage { + #[BuildFor('Windows')] + public function buildWin(): void + { + WindowsCMakeExecutor::create($this) + ->setBuildDir("{$this->getSourceDir()}/builddir") + ->addConfigureArgs( + '-DAOM_TARGET_CPU=generic', + '-DENABLE_TESTS=OFF', + '-DENABLE_EXAMPLES=OFF', + '-DENABLE_TOOLS=OFF', + '-DENABLE_DOCS=OFF', + ) + ->build(); + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function buildUnix(ToolchainInterface $toolchain): void From 9af132d67e9ebb6c6ad89db3ff088577aadf9d53 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 18:03:14 +0800 Subject: [PATCH 496/682] Add missing icu license file --- src/globals/licenses/icu.txt | 568 +++++++++++++++++++++++++++++++++++ 1 file changed, 568 insertions(+) create mode 100644 src/globals/licenses/icu.txt diff --git a/src/globals/licenses/icu.txt b/src/globals/licenses/icu.txt new file mode 100644 index 000000000..5a2eda629 --- /dev/null +++ b/src/globals/licenses/icu.txt @@ -0,0 +1,568 @@ +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 2016-2025 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +SPDX-License-Identifier: Unicode-3.0 + +---------------------------------------------------------------------- + +Third-Party Software Licenses + +This section contains third-party software notices and/or additional +terms for licensed third-party software components included within ICU +libraries. + +---------------------------------------------------------------------- + +ICU License - ICU 1.8.1 to ICU 57.1 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2016 International Business Machines Corporation and others +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies of +the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY +SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in this Software without prior written authorization +of the copyright holder. + +All trademarks and registered trademarks mentioned herein are the +property of their respective owners. + +---------------------------------------------------------------------- + +Chinese/Japanese Word Break Dictionary Data (cjdict.txt) + + # The Google Chrome software developed by Google is licensed under + # the BSD license. Other software included in this distribution is + # provided under other licenses, as set forth below. + # + # The BSD License + # http://opensource.org/licenses/bsd-license.php + # Copyright (C) 2006-2008, Google Inc. + # + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # Redistributions in binary form must reproduce the above + # copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided with + # the distribution. + # Neither the name of Google Inc. nor the names of its + # contributors may be used to endorse or promote products derived from + # this software without specific prior written permission. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + # + # + # The word list in cjdict.txt are generated by combining three word lists + # listed below with further processing for compound word breaking. The + # frequency is generated with an iterative training against Google web + # corpora. + # + # * Libtabe (Chinese) + # - https://sourceforge.net/project/?group_id=1519 + # - Its license terms and conditions are shown below. + # + # * IPADIC (Japanese) + # - http://chasen.aist-nara.ac.jp/chasen/distribution.html + # - Its license terms and conditions are shown below. + # + # ---------COPYING.libtabe ---- BEGIN-------------------- + # + # /* + # * Copyright (c) 1999 TaBE Project. + # * Copyright (c) 1999 Pai-Hsiang Hsiao. + # * All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the TaBE Project nor the names of its + # * contributors may be used to endorse or promote products derived + # * from this software without specific prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # /* + # * Copyright (c) 1999 Computer Systems and Communication Lab, + # * Institute of Information Science, Academia + # * Sinica. All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the Computer Systems and Communication Lab + # * nor the names of its contributors may be used to endorse or + # * promote products derived from this software without specific + # * prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, + # University of Illinois + # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 + # + # ---------------COPYING.libtabe-----END-------------------------------- + # + # + # ---------------COPYING.ipadic-----BEGIN------------------------------- + # + # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science + # and Technology. All Rights Reserved. + # + # Use, reproduction, and distribution of this software is permitted. + # Any copy of this software, whether in its original form or modified, + # must include both the above copyright notice and the following + # paragraphs. + # + # Nara Institute of Science and Technology (NAIST), + # the copyright holders, disclaims all warranties with regard to this + # software, including all implied warranties of merchantability and + # fitness, in no event shall NAIST be liable for + # any special, indirect or consequential damages or any damages + # whatsoever resulting from loss of use, data or profits, whether in an + # action of contract, negligence or other tortuous action, arising out + # of or in connection with the use or performance of this software. + # + # A large portion of the dictionary entries + # originate from ICOT Free Software. The following conditions for ICOT + # Free Software applies to the current dictionary as well. + # + # Each User may also freely distribute the Program, whether in its + # original form or modified, to any third party or parties, PROVIDED + # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + # on, or be attached to, the Program, which is distributed substantially + # in the same form as set out herein and that such intended + # distribution, if actually made, will neither violate or otherwise + # contravene any of the laws and regulations of the countries having + # jurisdiction over the User or the intended distribution itself. + # + # NO WARRANTY + # + # The program was produced on an experimental basis in the course of the + # research and development conducted during the project and is provided + # to users as so produced on an experimental basis. Accordingly, the + # program is provided without any warranty whatsoever, whether express, + # implied, statutory or otherwise. The term "warranty" used herein + # includes, but is not limited to, any warranty of the quality, + # performance, merchantability and fitness for a particular purpose of + # the program and the nonexistence of any infringement or violation of + # any right of any third party. + # + # Each user of the program will agree and understand, and be deemed to + # have agreed and understood, that there is no warranty whatsoever for + # the program and, accordingly, the entire risk arising from or + # otherwise connected with the program is assumed by the user. + # + # Therefore, neither ICOT, the copyright holder, or any other + # organization that participated in or was otherwise related to the + # development of the program and their respective officials, directors, + # officers and other employees shall be held liable for any and all + # damages, including, without limitation, general, special, incidental + # and consequential damages, arising out of or otherwise in connection + # with the use or inability to use the program or any product, material + # or result produced or otherwise obtained by using the program, + # regardless of whether they have been advised of, or otherwise had + # knowledge of, the possibility of such damages at any time during the + # project or thereafter. Each user will be deemed to have agreed to the + # foregoing by his or her commencement of use of the program. The term + # "use" as used herein includes, but is not limited to, the use, + # modification, copying and distribution of the program and the + # production of secondary products from the program. + # + # In the case where the program, whether in its original form or + # modified, was distributed or delivered to or received by a user from + # any person, organization or entity other than ICOT, unless it makes or + # grants independently of ICOT any specific warranty to the user in + # writing, such person, organization or entity, will also be exempted + # from and not be held liable to the user for any such damages as noted + # above as far as the program is concerned. + # + # ---------------COPYING.ipadic-----END---------------------------------- + +---------------------------------------------------------------------- + +Lao Word Break Dictionary Data (laodict.txt) + + # Copyright (C) 2016 and later: Unicode, Inc. and others. + # License & terms of use: http://www.unicode.org/copyright.html + # Copyright (c) 2015 International Business Machines Corporation + # and others. All Rights Reserved. + # + # Project: https://github.com/rober42539/lao-dictionary + # Dictionary: https://github.com/rober42539/lao-dictionary/laodict.txt + # License: https://github.com/rober42539/lao-dictionary/LICENSE.txt + # (copied below) + # + # This file is derived from the above dictionary version of Nov 22, 2020 + # ---------------------------------------------------------------------- + # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # Redistributions of source code must retain the above copyright notice, this + # list of conditions and the following disclaimer. Redistributions in binary + # form must reproduce the above copyright notice, this list of conditions and + # the following disclaimer in the documentation and/or other materials + # provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # OF THE POSSIBILITY OF SUCH DAMAGE. + # -------------------------------------------------------------------------- + +---------------------------------------------------------------------- + +Burmese Word Break Dictionary Data (burmesedict.txt) + + # Copyright (c) 2014 International Business Machines Corporation + # and others. All Rights Reserved. + # + # This list is part of a project hosted at: + # github.com/kanyawtech/myanmar-karen-word-lists + # + # -------------------------------------------------------------------------- + # Copyright (c) 2013, LeRoy Benjamin Sharon + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: Redistributions of source code must retain the above + # copyright notice, this list of conditions and the following + # disclaimer. Redistributions in binary form must reproduce the + # above copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided + # with the distribution. + # + # Neither the name Myanmar Karen Word Lists, nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS + # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + # SUCH DAMAGE. + # -------------------------------------------------------------------------- + +---------------------------------------------------------------------- + +Time Zone Database + + ICU uses the public domain data and code derived from Time Zone +Database for its time zone support. The ownership of the TZ database +is explained in BCP 175: Procedure for Maintaining the Time Zone +Database section 7. + + # 7. Database Ownership + # + # The TZ database itself is not an IETF Contribution or an IETF + # document. Rather it is a pre-existing and regularly updated work + # that is in the public domain, and is intended to remain in the + # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do + # not apply to the TZ Database or contributions that individuals make + # to it. Should any claims be made and substantiated against the TZ + # Database, the organization that is providing the IANA + # Considerations defined in this RFC, under the memorandum of + # understanding with the IETF, currently ICANN, may act in accordance + # with all competent court orders. No ownership claims will be made + # by ICANN or the IETF Trust on the database or the code. Any person + # making a contribution to the database or code waives all rights to + # future claims in that contribution or in the TZ Database. + +---------------------------------------------------------------------- + +Google double-conversion + +Copyright 2006-2011, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------------------------------------------------------------------- + +JSON parsing library (nlohmann/json) + +File: vendor/json/upstream/single_include/nlohmann/json.hpp (only for ICU4C) + +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------------------------------------------------------------- + +File: aclocal.m4 (only for ICU4C) +Section: pkg.m4 - Macros to locate and utilise pkg-config. + + +Copyright © 2004 Scott James Remnant . +Copyright © 2012-2015 Dan Nicholson + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +02111-1307, USA. + +As a special exception to the GNU General Public License, if you +distribute this file as part of a program that contains a +configuration script generated by Autoconf, you may include it under +the same distribution terms that you use for the rest of that +program. + + +(The condition for the exception is fulfilled because +ICU4C includes a configuration script generated by Autoconf, +namely the `configure` script.) + +---------------------------------------------------------------------- + +File: config.guess (only for ICU4C) + + +This file is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see . + +As a special exception to the GNU General Public License, if you +distribute this file as part of a program that contains a +configuration script generated by Autoconf, you may include it under +the same distribution terms that you use for the rest of that +program. This Exception is an additional permission under section 7 +of the GNU General Public License, version 3 ("GPLv3"). + + +(The condition for the exception is fulfilled because +ICU4C includes a configuration script generated by Autoconf, +namely the `configure` script.) + +---------------------------------------------------------------------- + +File: install-sh (only for ICU4C) + + +Copyright 1991 by the Massachusetts Institute of Technology + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of M.I.T. not be used in advertising or +publicity pertaining to distribution of the software without specific, +written prior permission. M.I.T. makes no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +---------------------------------------------------------------------- + +File: sorttable.js (only for ICU4J) + +The MIT Licence, for code from kryogenix.org + +Code downloaded from the Browser Experiments section of kryogenix.org is +licenced under the so-called MIT licence. The licence is below. + +Copyright (c) 1997-date Stuart Langridge + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From adff728999205116f411bb422a6e4353900767ca Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 18:04:15 +0800 Subject: [PATCH 497/682] Add libffi-win --- config/pkg/lib/libffi-win.yml | 12 ++++++++ src/Package/Library/libffi_win.php | 47 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 config/pkg/lib/libffi-win.yml create mode 100644 src/Package/Library/libffi_win.php diff --git a/config/pkg/lib/libffi-win.yml b/config/pkg/lib/libffi-win.yml new file mode 100644 index 000000000..051dfcb22 --- /dev/null +++ b/config/pkg/lib/libffi-win.yml @@ -0,0 +1,12 @@ +libffi-win: + type: library + artifact: + source: + type: git + rev: master + url: 'https://github.com/static-php/libffi-win.git' + metadata: + license-files: [LICENSE] + license: MIT + static-libs@windows: + - libffi.lib diff --git a/src/Package/Library/libffi_win.php b/src/Package/Library/libffi_win.php new file mode 100644 index 000000000..12faaabef --- /dev/null +++ b/src/Package/Library/libffi_win.php @@ -0,0 +1,47 @@ + '\win32\vs17_x64', + '16' => '\win32\vs16_x64', + default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported!"), + }; + ApplicationContext::set('libffi_win_vs_ver_dir', $vs_ver_dir); + } + + #[BuildFor('Windows')] + public function build(LibraryPackage $lib): void + { + $vs_ver_dir = ApplicationContext::get('libffi_win_vs_ver_dir'); + cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}") + ->exec('msbuild libffi-msvc.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64'); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + + FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\x64\\Release\\libffi.lib", "{$lib->getLibDir()}\\libffi.lib"); + FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\x64\\Release\\libffi.pdb", "{$lib->getLibDir()}\\libffi.pdb"); + FileSystem::copy("{$lib->getSourceDir()}\\include\\ffi.h", "{$lib->getIncludeDir()}\\ffi.h"); + FileSystem::replaceFileStr("{$lib->getIncludeDir()}\\ffi.h", '#define LIBFFI_H', "#define LIBFFI_H\n#define FFI_BUILDING"); + FileSystem::copy("{$lib->getSourceDir()}\\src\\x86\\ffitarget.h", "{$lib->getIncludeDir()}\\ffitarget.h"); + FileSystem::copy("{$lib->getSourceDir()}\\fficonfig.h", "{$lib->getIncludeDir()}\\fficonfig.h"); + } +} From e4434643ff9d01cbd35ad38ff223983619747c05 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 18:41:57 +0800 Subject: [PATCH 498/682] Add jom to reduce openssl building time --- config/pkg/target/jom.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config/pkg/target/jom.yml diff --git a/config/pkg/target/jom.yml b/config/pkg/target/jom.yml new file mode 100644 index 000000000..70916bfdd --- /dev/null +++ b/config/pkg/target/jom.yml @@ -0,0 +1,7 @@ +jom: + type: target + artifact: + binary: + windows-x86_64: { type: url, url: 'https://download.qt.io/official_releases/jom/jom.zip', extract: '{pkg_root_path}/jom' } + path@windows: + - '{pkg_root_path}\jom' From 08dca4253dbff19641a5dae5af1edf0362f8c81b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 18:42:34 +0800 Subject: [PATCH 499/682] Add librabbitmq --- config/pkg/lib/librabbitmq.yml | 2 ++ src/Package/Library/librabbitmq.php | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/config/pkg/lib/librabbitmq.yml b/config/pkg/lib/librabbitmq.yml index da4e98562..9928d331d 100644 --- a/config/pkg/lib/librabbitmq.yml +++ b/config/pkg/lib/librabbitmq.yml @@ -12,3 +12,5 @@ librabbitmq: - openssl static-libs@unix: - librabbitmq.a + static-libs@windows: + - rabbitmq.4.lib diff --git a/src/Package/Library/librabbitmq.php b/src/Package/Library/librabbitmq.php index 2350ea5e8..03eda2a74 100644 --- a/src/Package/Library/librabbitmq.php +++ b/src/Package/Library/librabbitmq.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('librabbitmq')] class librabbitmq extends LibraryPackage @@ -18,4 +19,11 @@ public function buildUnix(): void { UnixCMakeExecutor::create($this)->addConfigureArgs('-DBUILD_STATIC_LIBS=ON')->build(); } + + #[BuildFor('Windows')] + public function buildWin(): void + { + WindowsCMakeExecutor::create($this)->build(); + rename("{$this->getLibDir()}\\librabbitmq.4.lib", "{$this->getLibDir()}\\rabbitmq.4.lib"); + } } From 508cfa67e5e47af405f29e722154d0bf943053bc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 21 Mar 2026 18:43:05 +0800 Subject: [PATCH 500/682] Make openssl build faster --- config/pkg/lib/openssl.yml | 3 +++ src/Package/Library/openssl.php | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/config/pkg/lib/openssl.yml b/config/pkg/lib/openssl.yml index 161cdcebc..da02f202b 100644 --- a/config/pkg/lib/openssl.yml +++ b/config/pkg/lib/openssl.yml @@ -16,6 +16,9 @@ openssl: license: OpenSSL depends: - zlib + depends@windows: + - zlib + - jom headers: - openssl static-libs@unix: diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php index a01d01b0b..69297198d 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -10,6 +10,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\EnvironmentException; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageBuilder; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\System\LinuxUtil; @@ -36,7 +37,7 @@ public function validate(): void } #[BuildFor('Windows')] - public function buildWin(LibraryPackage $lib): void + public function buildWin(LibraryPackage $lib, PackageBuilder $builder): void { $perl = ApplicationContext::get('perl'); $cmd = cmd()->cd($lib->getSourceDir()) @@ -47,7 +48,9 @@ public function buildWin(LibraryPackage $lib): void '--with-zlib-lib=' . quote($lib->getLibDir()) . ' ' . '--with-zlib-include=' . quote($lib->getIncludeDir()) . ' ' . '--release ' . - 'no-legacy ' + 'no-legacy ' . + 'no-tests ' . + '/FS' ); // patch zlib @@ -56,7 +59,7 @@ public function buildWin(LibraryPackage $lib): void FileSystem::replaceFileStr("{$lib->getSourceDir()}\\Makefile", '/debug', '/incremental:no /opt:icf /dynamicbase /nxcompat /ltcg /nodefaultlib:msvcrt'); // build - $cmd->exec("nmake install_dev CNF_LDFLAGS=\"/NODEFAULTLIB:kernel32.lib /NODEFAULTLIB:msvcrt /NODEFAULTLIB:msvcrtd /DEFAULTLIB:libcmt /LIBPATH:{$lib->getLibDir()} zlibstatic.lib\""); + $cmd->exec("jom.exe /j{$builder->concurrency} install_dev CNF_LDFLAGS=\"/NODEFAULTLIB:kernel32.lib /NODEFAULTLIB:msvcrt /NODEFAULTLIB:msvcrtd /DEFAULTLIB:libcmt /LIBPATH:{$lib->getLibDir()} zlibstatic.lib\""); // copy necessary c files FileSystem::copy("{$lib->getSourceDir()}\\ms\\applink.c", "{$lib->getIncludeDir()}\\openssl\\applink.c"); From 963e2a084a006090d52f23794fd205f902576f15 Mon Sep 17 00:00:00 2001 From: Hendrik Mennen Date: Sun, 22 Mar 2026 09:07:47 +0100 Subject: [PATCH 501/682] Add Windows builders for libaom and brotli libraries Both libraries are listed in lib.json and used as transitive dependencies (libaom via libavif, brotli via freetype/curl) but had no Windows builder, causing builds to fail with "library [X] is in the lib.json list but not supported to compile". libaom: Uses builddir instead of build to avoid collision with the source tree's build/cmake/ directory. Matches the Unix builder's AOM_TARGET_CPU=generic setting for portability. brotli: Standard CMake build with shared libs and tools disabled. Also adds static-libs-windows entry for libaom in lib.json. --- config/lib.json | 3 ++ src/SPC/builder/windows/library/brotli.php | 36 +++++++++++++++++++ src/SPC/builder/windows/library/libaom.php | 41 ++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/SPC/builder/windows/library/brotli.php create mode 100644 src/SPC/builder/windows/library/libaom.php diff --git a/config/lib.json b/config/lib.json index 58cbd4ba0..2612f6be3 100644 --- a/config/lib.json +++ b/config/lib.json @@ -355,6 +355,9 @@ "static-libs-unix": [ "libaom.a" ], + "static-libs-windows": [ + "aom.lib" + ], "cpp-library": true }, "libargon2": { diff --git a/src/SPC/builder/windows/library/brotli.php b/src/SPC/builder/windows/library/brotli.php new file mode 100644 index 000000000..f22f402ce --- /dev/null +++ b/src/SPC/builder/windows/library/brotli.php @@ -0,0 +1,36 @@ +source_dir . '\build'); + + // start build + cmd()->cd($this->source_dir) + ->execWithWrapper( + $this->builder->makeSimpleWrapper('cmake'), + '-B build ' . + '-A x64 ' . + "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . + '-DCMAKE_BUILD_TYPE=Release ' . + '-DBUILD_SHARED_LIBS=OFF ' . + '-DBROTLI_BUILD_TOOLS=OFF ' . + '-DBROTLI_BUNDLED_MODE=OFF ' . + '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' + ) + ->execWithWrapper( + $this->builder->makeSimpleWrapper('cmake'), + "--build build --config Release --target install -j{$this->builder->concurrency}" + ); + } +} diff --git a/src/SPC/builder/windows/library/libaom.php b/src/SPC/builder/windows/library/libaom.php new file mode 100644 index 000000000..06d53cbc7 --- /dev/null +++ b/src/SPC/builder/windows/library/libaom.php @@ -0,0 +1,41 @@ +source_dir . '\builddir'); + + // start build + cmd()->cd($this->source_dir) + ->execWithWrapper( + $this->builder->makeSimpleWrapper('cmake'), + '-S . -B builddir ' . + '-A x64 ' . + "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . + '-DCMAKE_BUILD_TYPE=Release ' . + '-DBUILD_SHARED_LIBS=OFF ' . + '-DAOM_TARGET_CPU=generic ' . + '-DENABLE_DOCS=OFF ' . + '-DENABLE_EXAMPLES=OFF ' . + '-DENABLE_TESTDATA=OFF ' . + '-DENABLE_TESTS=OFF ' . + '-DENABLE_TOOLS=OFF ' . + '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' + ) + ->execWithWrapper( + $this->builder->makeSimpleWrapper('cmake'), + "--build builddir --config Release --target install -j{$this->builder->concurrency}" + ); + } +} From c03508a84bc7f14af614e9e9c975f53071412505 Mon Sep 17 00:00:00 2001 From: Hendrik Mennen Date: Sun, 22 Mar 2026 13:11:29 +0100 Subject: [PATCH 502/682] Improve zlib Windows library detection for future zlib versions (#1070) Co-authored-by: Hendrik Mennen --- src/SPC/builder/windows/library/zlib.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/windows/library/zlib.php b/src/SPC/builder/windows/library/zlib.php index e9f960233..a5fd346ba 100644 --- a/src/SPC/builder/windows/library/zlib.php +++ b/src/SPC/builder/windows/library/zlib.php @@ -35,6 +35,7 @@ protected function build(): void 'zlibstatic.lib', 'zs.lib', 'libzs.lib', + 'libz.lib', ]; foreach ($detect_list as $item) { if (file_exists(BUILD_LIB_PATH . '\\' . $item)) { From af9726359676099876d3252e5db921fed295bb35 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 22 Mar 2026 20:19:31 +0800 Subject: [PATCH 503/682] Forward-port #1070 --- src/Package/Library/zlib.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Library/zlib.php b/src/Package/Library/zlib.php index f45b942c9..9bc9ef663 100644 --- a/src/Package/Library/zlib.php +++ b/src/Package/Library/zlib.php @@ -32,6 +32,7 @@ public function buildWin(LibraryPackage $lib): void 'zlibstatic.lib', 'zs.lib', 'libzs.lib', + 'libz.lib', ]; foreach ($detect_list as $item) { if (file_exists("{$lib->getLibDir()}\\{$item}")) { From a99b6bebaed8bc6bd53a2913ec9dd0ebcb9dbc78 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Mar 2026 16:08:09 +0800 Subject: [PATCH 504/682] Remove freetype useless lib suggestions --- config/lib.json | 4 +--- src/SPC/builder/unix/library/freetype.php | 4 ++-- src/SPC/builder/windows/library/freetype.php | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/lib.json b/config/lib.json index 2612f6be3..824023687 100644 --- a/config/lib.json +++ b/config/lib.json @@ -143,9 +143,7 @@ "zlib" ], "lib-suggests": [ - "libpng", - "bzip2", - "brotli" + "libpng" ] }, "gettext": { diff --git a/src/SPC/builder/unix/library/freetype.php b/src/SPC/builder/unix/library/freetype.php index a10b7fb28..57c1ac05c 100644 --- a/src/SPC/builder/unix/library/freetype.php +++ b/src/SPC/builder/unix/library/freetype.php @@ -13,8 +13,8 @@ protected function build(): void { $cmake = UnixCMakeExecutor::create($this) ->optionalLib('libpng', ...cmake_boolean_args('FT_DISABLE_PNG', true)) - ->optionalLib('bzip2', ...cmake_boolean_args('FT_DISABLE_BZIP2', true)) - ->optionalLib('brotli', ...cmake_boolean_args('FT_DISABLE_BROTLI', true)) + ->addConfigureArgs('-DFT_DISABLE_BZIP2=ON') + ->addConfigureArgs('-DFT_DISABLE_BROTLI=ON') ->addConfigureArgs('-DFT_DISABLE_HARFBUZZ=ON'); // fix cmake 4.0 compatibility diff --git a/src/SPC/builder/windows/library/freetype.php b/src/SPC/builder/windows/library/freetype.php index ef13c8fe6..8401b4de4 100644 --- a/src/SPC/builder/windows/library/freetype.php +++ b/src/SPC/builder/windows/library/freetype.php @@ -24,6 +24,8 @@ protected function build(): void "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . '-DCMAKE_BUILD_TYPE=Release ' . '-DBUILD_SHARED_LIBS=OFF ' . + '-DFT_DISABLE_BROTLI=TRUE ' . + '-DFT_DISABLE_BZIP2=TRUE ' . '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' ) ->execWithWrapper( From d076df6b04f33adc0d4185e9340ff287416bfdca Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Mar 2026 16:21:24 +0800 Subject: [PATCH 505/682] Bump version number --- src/SPC/ConsoleApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 750c49e4b..79f7d724a 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.3'; + public const string VERSION = '2.8.4'; public function __construct() { From 141c73738042827b73188c902eb7b745472c55ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Mar 2026 16:50:13 +0800 Subject: [PATCH 506/682] Add libsodium --- config/pkg/lib/libsodium.yml | 2 ++ src/Package/Library/libsodium.php | 49 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/config/pkg/lib/libsodium.yml b/config/pkg/lib/libsodium.yml index f5a551b93..4bd41363b 100644 --- a/config/pkg/lib/libsodium.yml +++ b/config/pkg/lib/libsodium.yml @@ -13,3 +13,5 @@ libsodium: - libsodium static-libs@unix: - libsodium.a + static-libs@windows: + - libsodium.lib diff --git a/src/Package/Library/libsodium.php b/src/Package/Library/libsodium.php index 50d706ba4..5280927cc 100644 --- a/src/Package/Library/libsodium.php +++ b/src/Package/Library/libsodium.php @@ -6,12 +6,26 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Exception\BuildFailureException; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; +use StaticPHP\Util\System\WindowsUtil; #[Library('libsodium')] class libsodium { + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + if (SystemTarget::getTargetOS() === 'Windows') { + FileSystem::replaceFileStr("{$lib->getSourceDir()}\\src\\libsodium\\include\\sodium\\export.h", '#ifdef SODIUM_STATIC', '#if 1'); + } + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void @@ -21,4 +35,39 @@ public function build(LibraryPackage $lib): void // Patch pkg-config file $lib->patchPkgconfPrefix(['libsodium.pc'], PKGCONF_PATCH_PREFIX); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + $ver = WindowsUtil::findVisualStudio(); + $vs_ver_dir = match ($ver['major_version']) { + '17' => '\vs2022', + '16' => '\vs2019', + default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), + }; + + cmd()->cd("{$lib->getSourceDir()}\\builds\\msvc{$vs_ver_dir}") + ->exec('msbuild libsodium.sln /t:Rebuild /p:Configuration=StaticRelease /p:Platform=x64 /p:PreprocessorDefinitions="SODIUM_STATIC=1"'); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + + // copy include + FileSystem::copyDir("{$lib->getSourceDir()}\\src\\libsodium\\include\\sodium", "{$lib->getIncludeDir()}\\sodium"); + FileSystem::copy("{$lib->getSourceDir()}\\src\\libsodium\\include\\sodium.h", "{$lib->getIncludeDir()}\\sodium.h"); + // copy lib + $ls = FileSystem::scanDirFiles("{$lib->getSourceDir()}\\bin"); + $find = false; + foreach ($ls as $file) { + if (str_ends_with($file, 'libsodium.lib')) { + FileSystem::copy($file, "{$lib->getLibDir()}\\libsodium.lib"); + $find = true; + } + if (str_ends_with($file, 'libsodium.pdb')) { + FileSystem::copy($file, "{$lib->getLibDir()}\\libsodium.pdb"); + } + } + if (!$find) { + throw new BuildFailureException("Build libsodium success, but cannot find libsodium.lib in {$lib->getSourceDir()}\\bin ."); + } + } } From 6d91f8b2d38c01ecc8881f4c321c936cf72deae2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Mar 2026 17:11:37 +0800 Subject: [PATCH 507/682] Add patch description for Windows static linking in libsodium --- src/Package/Library/libsodium.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Package/Library/libsodium.php b/src/Package/Library/libsodium.php index 5280927cc..0d4c8afef 100644 --- a/src/Package/Library/libsodium.php +++ b/src/Package/Library/libsodium.php @@ -7,6 +7,7 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Exception\BuildFailureException; use StaticPHP\Exception\EnvironmentException; use StaticPHP\Package\LibraryPackage; @@ -19,11 +20,11 @@ class libsodium { #[PatchBeforeBuild] + #[PatchDescription('Replace SODIUM_STATIC define guard with unconditional #if 1 for MSVC static linking')] public function patchBeforeBuild(LibraryPackage $lib): void { - if (SystemTarget::getTargetOS() === 'Windows') { - FileSystem::replaceFileStr("{$lib->getSourceDir()}\\src\\libsodium\\include\\sodium\\export.h", '#ifdef SODIUM_STATIC', '#if 1'); - } + spc_skip_if(SystemTarget::getTargetOS() !== 'Windows', 'This patch is only for Windows builds.'); + FileSystem::replaceFileStr($lib->getSourceDir() . '\src\libsodium\include\sodium\export.h', '#ifdef SODIUM_STATIC', '#if 1'); } #[BuildFor('Linux')] From 41f5948392e18df0c2d9eb053156b4f737bc1eb1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Mar 2026 17:11:47 +0800 Subject: [PATCH 508/682] Add libyaml --- config/pkg/lib/libyaml.yml | 2 ++ src/Package/Library/libyaml.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/config/pkg/lib/libyaml.yml b/config/pkg/lib/libyaml.yml index 0d39e0b1d..8cabd58fb 100644 --- a/config/pkg/lib/libyaml.yml +++ b/config/pkg/lib/libyaml.yml @@ -13,3 +13,5 @@ libyaml: - yaml.h static-libs@unix: - libyaml.a + static-libs@windows: + - yaml.lib diff --git a/src/Package/Library/libyaml.php b/src/Package/Library/libyaml.php index 602c875c8..5334079ba 100644 --- a/src/Package/Library/libyaml.php +++ b/src/Package/Library/libyaml.php @@ -6,16 +6,44 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; #[Library('libyaml')] class libyaml { + #[PatchBeforeBuild] + #[PatchDescription('Copy missing cmake helper files required for MSVC build (not included in libyaml git source)')] + public function patchBeforeBuild(LibraryPackage $lib): void + { + spc_skip_if(SystemTarget::getTargetOS() !== 'Windows', 'This patch is only for Windows builds.'); + // check missing files: cmake\config.h.in and .\YamlConfig.cmake.in + if (!file_exists($lib->getSourceDir() . '\cmake\config.h.in')) { + FileSystem::createDir($lib->getSourceDir() . '\cmake'); + FileSystem::copy(ROOT_DIR . '/src/globals/extra/libyaml_config.h.in', $lib->getSourceDir() . '\cmake\config.h.in'); + } + if (!file_exists($lib->getSourceDir() . '\YamlConfig.cmake.in')) { + FileSystem::copy(ROOT_DIR . '/src/globals/extra/libyaml_yamlConfig.cmake.in', $lib->getSourceDir() . '\YamlConfig.cmake.in'); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function buildUnix(LibraryPackage $lib): void { UnixAutoconfExecutor::create($lib)->configure()->make(); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs('-DBUILD_TESTING=OFF') + ->build(); + } } From 1f42f1a479c10b07ffd6aa77840b04315bef31e0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 10:52:13 +0800 Subject: [PATCH 509/682] Add libzip --- config/pkg/lib/libzip.yml | 7 +++++-- src/Package/Library/libzip.php | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/config/pkg/lib/libzip.yml b/config/pkg/lib/libzip.yml index 2de69d424..3d8a375a0 100644 --- a/config/pkg/lib/libzip.yml +++ b/config/pkg/lib/libzip.yml @@ -8,9 +8,10 @@ libzip: prefer-stable: true metadata: license-files: [LICENSE] - depends@unix: + license: BSD-3-Clause + depends: - zlib - suggests@unix: + suggests: - bzip2 - xz - zstd @@ -20,3 +21,5 @@ libzip: - zipconf.h static-libs@unix: - libzip.a + static-libs@windows: + - libzip_a.lib diff --git a/src/Package/Library/libzip.php b/src/Package/Library/libzip.php index f6ebdfea7..b1d386345 100644 --- a/src/Package/Library/libzip.php +++ b/src/Package/Library/libzip.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('libzip')] class libzip @@ -33,4 +35,28 @@ public function buildUnix(LibraryPackage $lib): void ->build(); $lib->patchPkgconfPrefix(['libzip.pc'], PKGCONF_PATCH_PREFIX); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->optionalPackage('bzip2', ...cmake_boolean_args('ENABLE_BZIP2')) + ->optionalPackage('xz', ...cmake_boolean_args('ENABLE_LZMA')) + ->optionalPackage('openssl', ...cmake_boolean_args('ENABLE_OPENSSL')) + ->optionalPackage('zstd', ...cmake_boolean_args('ENABLE_ZSTD')) + ->addConfigureArgs( + '-DENABLE_GNUTLS=OFF', + '-DENABLE_MBEDTLS=OFF', + '-DBUILD_DOC=OFF', + '-DBUILD_EXAMPLES=OFF', + '-DBUILD_REGRESS=OFF', + '-DBUILD_TOOLS=OFF', + '-DBUILD_OSSFUZZ=OFF', + ) + ->build(); + FileSystem::copy( + $lib->getBuildRootPath() . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'zip.lib', + $lib->getBuildRootPath() . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'libzip_a.lib' + ); + } } From 2a50015c12e8d32d4b39ec157428d164ed8c9e60 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:07:02 +0800 Subject: [PATCH 510/682] Add download options for install-pkg command --- src/StaticPHP/Command/InstallPackageCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 864fd3796..1185e9f93 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -4,6 +4,7 @@ namespace StaticPHP\Command; +use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\DI\ApplicationContext; use StaticPHP\Package\PackageInstaller; use StaticPHP\Registry\PackageLoader; @@ -29,6 +30,7 @@ public function configure(): void return array_filter($packages, fn ($name) => str_starts_with($name, $val)); } ); + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions('dl')); } public function handle(): int From 7c3022b7e3bf947e837d3fc43ccd97277cf9260f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:07:57 +0800 Subject: [PATCH 511/682] Add postgresql (replace postgresql-win) --- config/pkg/lib/postgresql.yml | 12 +++++++++--- src/StaticPHP/Package/PackageInstaller.php | 5 +++++ src/StaticPHP/Util/FileSystem.php | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/pkg/lib/postgresql.yml b/config/pkg/lib/postgresql.yml index 1237b1846..ee78072e4 100644 --- a/config/pkg/lib/postgresql.yml +++ b/config/pkg/lib/postgresql.yml @@ -5,19 +5,25 @@ postgresql: type: ghtagtar repo: postgres/postgres match: REL_18_\d+ + binary: + windows-x86_64: { type: url, url: 'https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip', extract: { lib/libpq.lib: '{build_root_path}/lib/libpq.lib', lib/libpgport.lib: '{build_root_path}/lib/libpgport.lib', lib/libpgcommon.lib: '{build_root_path}/lib/libpgcommon.lib', include/libpq-fe.h: '{build_root_path}/include/libpq-fe.h', include/postgres_ext.h: '{build_root_path}/include/postgres_ext.h', include/pg_config_ext.h: '{build_root_path}/include/pg_config_ext.h', include/libpq/libpq-fs.h: '{build_root_path}/include/libpq/libpq-fs.h' } } metadata: - license-files: [COPYRIGHT] + license-files: ['@/postgresql.txt'] license: PostgreSQL - depends: + depends@unix: - libiconv - libxml2 - openssl - zlib - libedit - suggests: + suggests@unix: - icu - libxslt - ldap - zstd pkg-configs: - libpq + static-libs@windows: + - libpq.lib + - libpgport.lib + - libpgcommon.lib diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index f5c03a7de..16e8f45a9 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -323,6 +323,11 @@ public function isPackageInstalled(Package|string $package_name): bool $artifact = $package->getArtifact(); return $artifact->isBinaryExtracted(); } + // Fallback: if the download cache is missing (e.g. download failed or cache was cleared), + // still check whether the files are physically present in buildroot. + if ($package instanceof LibraryPackage) { + return $package->isInstalled(); + } return false; } diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 144f81eb3..38a614e01 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -405,6 +405,7 @@ public static function isRelativePath(string $path): bool public static function replacePathVariable(string $path): string { $replacement = [ + '{build_root_path}' => BUILD_ROOT_PATH, '{pkg_root_path}' => PKG_ROOT_PATH, '{php_sdk_path}' => getenv('PHP_SDK_PATH') ? getenv('PHP_SDK_PATH') : WORKING_DIR . '/php-sdk-binary-tools', '{working_dir}' => WORKING_DIR, From bf05af7e16ff686c98141c50613705354ba05a8c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:13:22 +0800 Subject: [PATCH 512/682] Add pthreads4w --- config/pkg/lib/pthreads4w.yml | 12 +++++++++++ src/Package/Library/pthreads4w.php | 34 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 config/pkg/lib/pthreads4w.yml create mode 100644 src/Package/Library/pthreads4w.php diff --git a/config/pkg/lib/pthreads4w.yml b/config/pkg/lib/pthreads4w.yml new file mode 100644 index 000000000..06a6245f8 --- /dev/null +++ b/config/pkg/lib/pthreads4w.yml @@ -0,0 +1,12 @@ +pthreads4w: + type: library + artifact: + source: + type: git + rev: master + url: 'https://git.code.sf.net/p/pthreads4w/code' + metadata: + license-files: [LICENSE] + license: Apache-2.0 + static-libs@windows: + - libpthreadVC3.lib diff --git a/src/Package/Library/pthreads4w.php b/src/Package/Library/pthreads4w.php new file mode 100644 index 000000000..69f181f0b --- /dev/null +++ b/src/Package/Library/pthreads4w.php @@ -0,0 +1,34 @@ +cd($lib->getSourceDir()) + ->exec( + 'nmake /E /nologo /f Makefile ' . + 'DESTROOT=' . $lib->getBuildRootPath() . ' ' . + 'XCFLAGS="/MT" ' . // no dll + 'EHFLAGS="/I. /DHAVE_CONFIG_H /Os /Ob2 /D__PTW32_STATIC_LIB /D__PTW32_BUILD_INLINED" ' . + 'pthreadVC3.inlined_static_stamp' + ); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + FileSystem::copy("{$lib->getSourceDir()}\\libpthreadVC3.lib", "{$lib->getLibDir()}\\libpthreadVC3.lib"); + FileSystem::copy("{$lib->getSourceDir()}\\_ptw32.h", "{$lib->getIncludeDir()}\\_ptw32.h"); + FileSystem::copy("{$lib->getSourceDir()}\\pthread.h", "{$lib->getIncludeDir()}\\pthread.h"); + FileSystem::copy("{$lib->getSourceDir()}\\sched.h", "{$lib->getIncludeDir()}\\sched.h"); + FileSystem::copy("{$lib->getSourceDir()}\\semaphore.h", "{$lib->getIncludeDir()}\\semaphore.h"); + } +} From 175567fd1170d3e917551b240a26869a7bc44831 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:13:42 +0800 Subject: [PATCH 513/682] Add missing postgresql license --- src/globals/licenses/postgresql.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/globals/licenses/postgresql.txt diff --git a/src/globals/licenses/postgresql.txt b/src/globals/licenses/postgresql.txt new file mode 100644 index 000000000..2f33cebbe --- /dev/null +++ b/src/globals/licenses/postgresql.txt @@ -0,0 +1,23 @@ +PostgreSQL Database Management System +(also known as Postgres, formerly known as Postgres95) + +Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + +Portions Copyright (c) 1994, The Regents of the University of California + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose, without fee, and without a written agreement +is hereby granted, provided that the above copyright notice and this +paragraph and the following two paragraphs appear in all copies. + +IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING +LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS +DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. From 6c52451c6c085dc8e3cae240bfb2a066fb3ddfda Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:18:06 +0800 Subject: [PATCH 514/682] Add qdbm --- config/pkg/lib/qdbm.yml | 2 ++ src/Package/Library/qdbm.php | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/config/pkg/lib/qdbm.yml b/config/pkg/lib/qdbm.yml index 1b46e3049..86b881d7c 100644 --- a/config/pkg/lib/qdbm.yml +++ b/config/pkg/lib/qdbm.yml @@ -10,3 +10,5 @@ qdbm: license: 'GPL-2.0-only OR LGPL-2.1-only' static-libs@unix: - libqdbm.a + static-libs@windows: + - qdbm_a.lib diff --git a/src/Package/Library/qdbm.php b/src/Package/Library/qdbm.php index 3b5c276c8..358846fb6 100644 --- a/src/Package/Library/qdbm.php +++ b/src/Package/Library/qdbm.php @@ -23,4 +23,15 @@ public function buildUnix(LibraryPackage $lib): void $ac->make(SystemTarget::getTargetOS() === 'Darwin' ? 'mac' : ''); $lib->patchPkgconfPrefix(['qdbm.pc']); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + cmd()->cd($lib->getSourceDir()) + ->exec('nmake /f VCMakefile'); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + FileSystem::copy("{$lib->getSourceDir()}\\qdbm_a.lib", "{$lib->getLibDir()}\\qdbm_a.lib"); + FileSystem::copy("{$lib->getSourceDir()}\\depot.h", "{$lib->getIncludeDir()}\\depot.h"); + } } From fcd0052d12ceac15c146a5659dee944eb297e4fc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:31:31 +0800 Subject: [PATCH 515/682] Add sqlite --- config/pkg/lib/sqlite.yml | 2 ++ src/Package/Library/sqlite.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/config/pkg/lib/sqlite.yml b/config/pkg/lib/sqlite.yml index fb6eecf35..442629d0c 100644 --- a/config/pkg/lib/sqlite.yml +++ b/config/pkg/lib/sqlite.yml @@ -10,3 +10,5 @@ sqlite: - sqlite3ext.h static-libs@unix: - libsqlite3.a + static-libs@windows: + - libsqlite3_a.lib diff --git a/src/Package/Library/sqlite.php b/src/Package/Library/sqlite.php index ae802bfa9..a3d15f9b7 100644 --- a/src/Package/Library/sqlite.php +++ b/src/Package/Library/sqlite.php @@ -6,8 +6,11 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; #[Library('sqlite')] class sqlite @@ -19,4 +22,18 @@ public function buildUnix(LibraryPackage $lib): void UnixAutoconfExecutor::create($lib)->configure()->make(); $lib->patchPkgconfPrefix(['sqlite3.pc']); } + + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + spc_skip_if(SystemTarget::getTargetOS() !== 'Windows', 'This patch is only for Windows builds.'); + FileSystem::copy(ROOT_DIR . '/src/globals/extra/Makefile-sqlite', "{$lib->getSourceDir()}\\Makefile"); + } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + cmd()->cd($lib->getSourceDir()) + ->exec("nmake PREFIX={$lib->getBuildRootPath()} install-static"); + } } From 93c099dd3166aa5d88290bc0535e49062f42069f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 12:31:45 +0800 Subject: [PATCH 516/682] Add xz --- config/pkg/lib/xz.yml | 6 ++++++ src/Package/Library/xz.php | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/pkg/lib/xz.yml b/config/pkg/lib/xz.yml index 7d0af682b..3be1815d8 100644 --- a/config/pkg/lib/xz.yml +++ b/config/pkg/lib/xz.yml @@ -14,7 +14,13 @@ xz: - libiconv headers@unix: - lzma + headers@windows: + - lzma + - lzma.h pkg-configs: - liblzma static-libs@unix: - liblzma.a + static-libs@windows: + - lzma.lib + - liblzma_a.lib diff --git a/src/Package/Library/xz.php b/src/Package/Library/xz.php index 3486d4c17..44a20090f 100644 --- a/src/Package/Library/xz.php +++ b/src/Package/Library/xz.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('xz')] class xz @@ -27,4 +29,14 @@ public function build(LibraryPackage $lib): void $lib->patchPkgconfPrefix(['liblzma.pc']); $lib->patchLaDependencyPrefix(); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib)->build(); + // copy lzma.lib to liblzma_a.lib + FileSystem::copy("{$lib->getLibDir()}\\lzma.lib", "{$lib->getLibDir()}\\liblzma_a.lib"); + // patch lzma.h: make static API always available on Windows + FileSystem::replaceFileStr("{$lib->getIncludeDir()}\\lzma.h", 'defined(LZMA_API_STATIC)', 'defined(_WIN32)'); + } } From b625d80dc02de60ba2ebc3256d751464434c6898 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 13:30:17 +0800 Subject: [PATCH 517/682] Fix tar command for unix --- src/StaticPHP/Runtime/Shell/DefaultShell.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 77dbf94a0..272011e49 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -6,6 +6,7 @@ use StaticPHP\Exception\InterruptException; use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; /** @@ -132,7 +133,8 @@ public function executeTarExtract(string $archive_path, string $target_path, str }; $mute = $this->console_putput ? '' : ' 2>/dev/null'; - $cmd = "\"C:\\Windows\\system32\\tar.exe\" {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; + $tar = SystemTarget::isUnix() ? 'tar' : '"C:\\Windows\\system32\\tar.exe"'; + $cmd = "{$tar} {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; $this->logCommandInfo($cmd); logger()->debug("[TAR EXTRACT] {$cmd}"); @@ -185,9 +187,11 @@ public function execute7zExtract(string $archive_path, string $target_path): boo }; $extname = FileSystem::extname($archive_path); + $tar = SystemTarget::isUnix() ? 'tar' : '"C:\\Windows\\system32\\tar.exe"'; + match ($extname) { 'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'), - 'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | \"C:\\Windows\\system32\\tar.exe\" -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}"), }; From 590a94a723f0b85d5fd31d7271dace42c22f6575 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 14:41:32 +0800 Subject: [PATCH 518/682] Fix cli checks caused php testing fail --- src/globals/patch/php-src-patches/cli_checks_81.patch | 10 +++++----- src/globals/patch/php-src-patches/cli_checks_83.patch | 10 +++++----- src/globals/patch/php-src-patches/cli_checks_84.patch | 10 +++++----- src/globals/patch/php-src-patches/cli_checks_85.patch | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/globals/patch/php-src-patches/cli_checks_81.patch b/src/globals/patch/php-src-patches/cli_checks_81.patch index 92d6cce5a..39ed5828b 100644 --- a/src/globals/patch/php-src-patches/cli_checks_81.patch +++ b/src/globals/patch/php-src-patches/cli_checks_81.patch @@ -20,7 +20,7 @@ index 8f05686367..c155028233 100644 REGISTER_INI_ENTRIES(); - FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; -+ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); @@ -103,7 +103,7 @@ index 4287045511..eab0311d07 100644 return NULL; } - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_in = 0; fd = STDIN_FILENO; if (cli_in) { @@ -112,7 +112,7 @@ index 4287045511..eab0311d07 100644 #endif } else if (!strcasecmp(path, "stdout")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_out = 0; fd = STDOUT_FILENO; if (cli_out++) { @@ -121,7 +121,7 @@ index 4287045511..eab0311d07 100644 #endif } else if (!strcasecmp(path, "stderr")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_err = 0; fd = STDERR_FILENO; if (cli_err++) { @@ -130,7 +130,7 @@ index 4287045511..eab0311d07 100644 int dtablesize; - if (strcmp(sapi_module.name, "cli")) { -+ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { if (options & REPORT_ERRORS) { php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); } diff --git a/src/globals/patch/php-src-patches/cli_checks_83.patch b/src/globals/patch/php-src-patches/cli_checks_83.patch index e94250958..db02843b4 100644 --- a/src/globals/patch/php-src-patches/cli_checks_83.patch +++ b/src/globals/patch/php-src-patches/cli_checks_83.patch @@ -20,7 +20,7 @@ index bbfe07576e..398373d577 100644 REGISTER_INI_ENTRIES(); - FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; -+ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); @@ -112,7 +112,7 @@ index 8926485025..6740163bc5 100644 return NULL; } - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_in = 0; fd = STDIN_FILENO; if (cli_in) { @@ -121,7 +121,7 @@ index 8926485025..6740163bc5 100644 #endif } else if (!strcasecmp(path, "stdout")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_out = 0; fd = STDOUT_FILENO; if (cli_out++) { @@ -130,7 +130,7 @@ index 8926485025..6740163bc5 100644 #endif } else if (!strcasecmp(path, "stderr")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_err = 0; fd = STDERR_FILENO; if (cli_err++) { @@ -139,7 +139,7 @@ index 8926485025..6740163bc5 100644 int dtablesize; - if (strcmp(sapi_module.name, "cli")) { -+ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { if (options & REPORT_ERRORS) { php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); } diff --git a/src/globals/patch/php-src-patches/cli_checks_84.patch b/src/globals/patch/php-src-patches/cli_checks_84.patch index 6b8ac74e1..b137c6ef4 100644 --- a/src/globals/patch/php-src-patches/cli_checks_84.patch +++ b/src/globals/patch/php-src-patches/cli_checks_84.patch @@ -20,7 +20,7 @@ index d797f5f93f..27cb05e3e4 100644 REGISTER_INI_ENTRIES(); - FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; -+ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); @@ -111,7 +111,7 @@ index a5581d9ccc..98455f7b52 100644 return NULL; } - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_in = 0; fd = STDIN_FILENO; if (cli_in) { @@ -120,7 +120,7 @@ index a5581d9ccc..98455f7b52 100644 #endif } else if (!strcasecmp(path, "stdout")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_out = 0; fd = STDOUT_FILENO; if (cli_out++) { @@ -129,7 +129,7 @@ index a5581d9ccc..98455f7b52 100644 #endif } else if (!strcasecmp(path, "stderr")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_err = 0; fd = STDERR_FILENO; if (cli_err++) { @@ -138,7 +138,7 @@ index a5581d9ccc..98455f7b52 100644 int dtablesize; - if (strcmp(sapi_module.name, "cli")) { -+ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { if (options & REPORT_ERRORS) { php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); } diff --git a/src/globals/patch/php-src-patches/cli_checks_85.patch b/src/globals/patch/php-src-patches/cli_checks_85.patch index cfd72550c..b8cf0fa51 100644 --- a/src/globals/patch/php-src-patches/cli_checks_85.patch +++ b/src/globals/patch/php-src-patches/cli_checks_85.patch @@ -20,7 +20,7 @@ index 10fc11f5..eb4d4175 100644 REGISTER_INI_ENTRIES(); - FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; -+ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); @@ -98,7 +98,7 @@ index ea33ba49..083184b8 100644 return NULL; } - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_in = 0; fd = STDIN_FILENO; if (cli_in) { @@ -107,7 +107,7 @@ index ea33ba49..083184b8 100644 #endif } else if (!strcasecmp(path, "stdout")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_out = 0; fd = STDOUT_FILENO; if (cli_out++) { @@ -116,7 +116,7 @@ index ea33ba49..083184b8 100644 #endif } else if (!strcasecmp(path, "stderr")) { - if (!strcmp(sapi_module.name, "cli")) { -+ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { static int cli_err = 0; fd = STDERR_FILENO; if (cli_err++) { @@ -125,7 +125,7 @@ index ea33ba49..083184b8 100644 int dtablesize; - if (strcmp(sapi_module.name, "cli")) { -+ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { if (options & REPORT_ERRORS) { php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); } From 9cd312554494f0b9b10afe1bec2ea5f9f6f122f0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 14:41:46 +0800 Subject: [PATCH 519/682] Patch configure script to include liblber for ldap dependency --- src/Package/Library/postgresql.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 682b79e28..45c48577c 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -73,6 +73,11 @@ public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): FileSystem::resetDir("{$this->getSourceDir()}/build"); + if ($installer->isPackageResolved('ldap')) { + $ldap_libs = clean_spaces(implode(' ', PkgConfigUtil::getLibsArray('ldap'))); + FileSystem::replaceFileStr("{$this->getSourceDir()}/configure", '-lldap', $ldap_libs); + } + // PHP source relies on the non-private encoding functions in libpgcommon.a FileSystem::replaceFileStr( "{$this->getSourceDir()}/src/common/Makefile", From 13a15b1a5ab2f5b32719ab85839ede698aa77fe7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 15:31:15 +0800 Subject: [PATCH 520/682] Use alpha1 as version number --- src/StaticPHP/ConsoleApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index a02b38c7d..2882b5741 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -29,7 +29,7 @@ class ConsoleApplication extends Application { - public const string VERSION = '3.0.0-dev'; + public const string VERSION = '3.0.0-alpha1'; private static array $additional_commands = []; From 2fa07700ef3200e09d9b38a10a855f1af485a8aa Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 18:55:04 +0800 Subject: [PATCH 521/682] Fix zlib extension wrong arg --- config/pkg/ext/builtin-extensions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index b938182c0..ca72e816d 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -342,6 +342,6 @@ ext-zlib: - zlib php-extension: arg-type: custom - arg-type@windows: with + arg-type@windows: enable build-with-php: true build-shared: false From 94dd44e68dc1e7aef31ff45059096a72d2c5eb95 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 24 Mar 2026 18:58:12 +0800 Subject: [PATCH 522/682] Add ext-amqp --- config/pkg/ext/ext-amqp.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml index 1c8023602..6c73bf203 100644 --- a/config/pkg/ext/ext-amqp.yml +++ b/config/pkg/ext/ext-amqp.yml @@ -10,6 +10,7 @@ ext-amqp: depends: - librabbitmq depends@windows: + - librabbitmq - ext-openssl php-extension: arg-type: '--with-amqp@shared_suffix@ --with-librabbitmq-dir=@build_root_path@' From ecf712b2b7c548a02f59f0e50b85359abc252a24 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 26 Mar 2026 12:32:27 +0700 Subject: [PATCH 523/682] hard code protobuf version while we're on v2 --- config/source.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/source.json b/config/source.json index 040197cb1..3f48cd784 100644 --- a/config/source.json +++ b/config/source.json @@ -1054,7 +1054,7 @@ }, "protobuf": { "type": "url", - "url": "https://pecl.php.net/get/protobuf", + "url": "https://pecl.php.net/get/protobuf-5.34.1.tgz", "path": "php-src/ext/protobuf", "filename": "protobuf.tgz", "license": { From 844bb69f0dfc547f55be8d72e025a217dc9314de Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Mar 2026 22:30:39 +0800 Subject: [PATCH 524/682] Fix ext-curl implementation --- config/pkg/ext/builtin-extensions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index ca72e816d..e5a6d28a9 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -16,6 +16,7 @@ ext-curl: depends: - curl depends@windows: + - curl - ext-zlib - ext-openssl php-extension: From daae5f2a7ca71a0322bd6c94c9d03cf4b7d1d4dd Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 30 Mar 2026 00:56:51 +0700 Subject: [PATCH 525/682] libjpeg-turbo mustn't compile zlib symbols on its own --- config/lib.json | 2 +- config/source.json | 1 + src/SPC/builder/unix/library/libjpeg.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/lib.json b/config/lib.json index 824023687..f960ab82f 100644 --- a/config/lib.json +++ b/config/lib.json @@ -494,7 +494,7 @@ "static-libs-windows": [ "libjpeg_a.lib" ], - "lib-suggests-windows": [ + "lib-depends": [ "zlib" ] }, diff --git a/config/source.json b/config/source.json index 3f48cd784..4b3f7b594 100644 --- a/config/source.json +++ b/config/source.json @@ -641,6 +641,7 @@ "libjpeg": { "type": "ghtar", "repo": "libjpeg-turbo/libjpeg-turbo", + "prefer-stable": true, "license": { "type": "file", "path": "LICENSE.md" diff --git a/src/SPC/builder/unix/library/libjpeg.php b/src/SPC/builder/unix/library/libjpeg.php index 862c1e88d..98881b762 100644 --- a/src/SPC/builder/unix/library/libjpeg.php +++ b/src/SPC/builder/unix/library/libjpeg.php @@ -14,6 +14,7 @@ protected function build(): void ->addConfigureArgs( '-DENABLE_STATIC=ON', '-DENABLE_SHARED=OFF', + '-DWITH_SYSTEM_ZLIB=ON' ) ->build(); // patch pkgconfig From 8f7897e13bc7dd9dfe488e723bc12f02bf644ad2 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 30 Mar 2026 01:06:31 +0700 Subject: [PATCH 526/682] test --- src/globals/test-extensions.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index b29e028ff..e31485d55 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -24,11 +24,11 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ // 'macos-15-intel', // bin/spc for x86_64 - // 'macos-15', // bin/spc for arm64 - // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'macos-15', // bin/spc for arm64 + 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 'windows-2025', @@ -42,15 +42,15 @@ // compress with upx $upx = false; -// whether to test frankenphp build, only available for macos and linux -$frankenphp = false; +// whether to test frankenphp build, only available for macOS and linux +$frankenphp = true; // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'pgsql', + 'Linux', 'Darwin' => 'zlib', 'Windows' => 'gd,zlib,mbstring,filter', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => '', + 'Linux', 'Darwin' => 'libjpeg', 'Windows' => '', }; From 5a5f54bdcd446c572ea63b35b6513610f69c40f8 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 30 Mar 2026 01:37:08 +0700 Subject: [PATCH 527/682] brilliant to test php 8.1 --- src/globals/test-extensions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index e31485d55..ccc561052 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,7 +13,7 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', + // '8.1', // '8.2', // '8.3', '8.4', @@ -43,7 +43,7 @@ $upx = false; // whether to test frankenphp build, only available for macOS and linux -$frankenphp = true; +$frankenphp = false; // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; From b96586e4d37664c6e8a96ab51203b8a9c230c96c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 31 Mar 2026 15:10:47 +0800 Subject: [PATCH 528/682] Add curl extension and enhance Windows build process --- src/Package/Extension/curl.php | 26 + src/Package/Library/nghttp2.php | 4 + src/Package/Target/php.php | 31 + src/Package/Target/php/unix.php | 23 +- src/Package/Target/php/windows.php | 1022 +++++++++++++---- src/StaticPHP/Package/PhpExtensionPackage.php | 50 + src/StaticPHP/Runtime/Shell/DefaultShell.php | 4 +- src/StaticPHP/Runtime/Shell/WindowsCmd.php | 126 +- src/StaticPHP/Util/SPCConfigUtil.php | 946 ++++++++------- src/StaticPHP/Util/System/WindowsUtil.php | 22 + 10 files changed, 1492 insertions(+), 762 deletions(-) create mode 100644 src/Package/Extension/curl.php diff --git a/src/Package/Extension/curl.php b/src/Package/Extension/curl.php new file mode 100644 index 000000000..f5c05b570 --- /dev/null +++ b/src/Package/Extension/curl.php @@ -0,0 +1,26 @@ +build(); + + FileSystem::replaceFileStr($lib->getIncludeDir() . '\nghttp2\nghttp2.h', '#ifdef NGHTTP2_STATICLIB', '#if 1'); + } #[BuildFor('Linux')] diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 38e2ad91b..29b6848a9 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -12,6 +12,7 @@ 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\Config\PackageConfig; @@ -29,6 +30,7 @@ use StaticPHP\Toolchain\ToolchainManager; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Input\InputArgument; @@ -339,6 +341,35 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void FileSystem::removeDir(BUILD_MODULES_PATH); } + #[Stage('postInstall')] + public function postInstall(TargetPackage $package, PackageInstaller $installer): void + { + if ($package->getName() === 'frankenphp') { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + return; + } + if ($package->getName() !== 'php') { + return; + } + if (SystemTarget::isUnix()) { + if ($installer->interactive) { + InteractiveTerm::indicateProgress('Running PHP smoke tests'); + } + $package->runStage([$this, 'smokeTestForUnix']); + if ($installer->interactive) { + InteractiveTerm::finish('PHP smoke tests passed'); + } + } elseif (SystemTarget::getTargetOS() === 'Windows') { + if ($installer->interactive) { + InteractiveTerm::indicateProgress('Running PHP smoke tests'); + } + $package->runStage([$this, 'smokeTestForWindows']); + if ($installer->interactive) { + InteractiveTerm::finish('PHP smoke tests passed'); + } + } + } + private function makeStaticExtensionString(PackageInstaller $installer): string { $arg = []; diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index f16d879f6..78eca80d2 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -469,27 +469,6 @@ public function build(TargetPackage $package): void $package->runStage([$this, 'unixBuildSharedExt']); } - #[Stage('postInstall')] - public function postInstall(TargetPackage $package, PackageInstaller $installer): void - { - if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'smokeTestFrankenphpForUnix']); - return; - } - if ($package->getName() !== 'php') { - return; - } - if (SystemTarget::isUnix()) { - if ($installer->interactive) { - InteractiveTerm::indicateProgress('Running PHP smoke tests'); - } - $package->runStage([$this, 'smokeTestForUnix']); - if ($installer->interactive) { - InteractiveTerm::finish('PHP smoke tests passed'); - } - } - } - /** * Patch phpize and php-config if needed */ @@ -662,7 +641,7 @@ protected function seekPhpSrcLogFileOnException(callable $callback, string $sour /** * Generate micro extension test php code. */ - private function generateMicroExtTests(PackageInstaller $installer): string + protected function generateMicroExtTests(PackageInstaller $installer): string { $php = "getResolvedPackages(PhpExtensionPackage::class) as $ext) { diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 74e746e2b..1188a0e49 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -1,239 +1,783 @@ -getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); - } - - #[Stage] - public function buildconfForWindows(TargetPackage $package): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); - V2CompatLayer::emitPatchPoint('before-php-buildconf'); - cmd()->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('php.exe')); - - // 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(); - 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", - '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()}\\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_file = "{$src[0]}\\{$src[1]}"; - $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); - - $builder->deployBinary($src_file, $dst_file); - - // make debug info file path - if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { - FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); - } - } -} +getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); + } + + #[Stage] + public function buildconfForWindows(TargetPackage $package): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); + V2CompatLayer::emitPatchPoint('before-php-buildconf'); + cmd()->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'); + $embed = $installer->isPackageResolved('php-embed'); + $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; + $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; + $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; + $args[] = $embed ? '--enable-embed=yes' : '--enable-embed=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, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); + + // Collect static-libs@windows from all resolved library packages. + // PHP's configure.bat only adds libs declared by enabled extensions via config.w32; + // transitive library-only deps (e.g. zlibstatic.lib needed by libcrypto.lib) are + // not covered. Inject them here so the final link step has all required symbols. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_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'); + } + + #[BeforeStage('php', [self::class, 'makeCgiForWindows'])] + #[PatchDescription('Patch Windows Makefile for CGI target')] + public function patchCGITarget(TargetPackage $package): void + { + // search Makefile code line contains "$(BUILD_DIR)\php-cgi.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-cgi.exe:')) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); + } + $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; + $lines[$line_num + 1] = "\t" . '@"$(LINK)” /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); + + // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() + FileSystem::replaceFileRegex("{$package->getSourceDir()}\\sapi\\cgi\\cgi_main.c", '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); + } + + #[Stage] + public function makeCgiForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php-cgi.exe')); + + // Collect static-libs@windows from all resolved library packages. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; + } + } + + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe"); + + $this->deployWindowsBinary($builder, $package, 'php-cgi'); + } + + #[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']); + } + if ($installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'makeEmbedForWindows']); + } + } + + #[Stage] + public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('micro.sfx')); + + // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) + $makefile = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); + if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; + } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n"; + } + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", $makefile); + + // Collect static-libs@windows from all resolved library packages. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; + } + } + + $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; + + // phar patch for micro + $phar_patched = false; + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + + try { + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } + + $this->deployWindowsBinary($builder, $package, 'php-micro'); + } + + #[BeforeStage('php', [self::class, 'makeEmbedForWindows'])] + #[PatchDescription('Patch Windows Makefile for embed static library target')] + public function patchEmbedTarget(TargetPackage $package): void + { + $makefile_path = "{$package->getSourceDir()}\\Makefile"; + $content = FileSystem::readFile($makefile_path); + + // PHP's configure.bat generates PHP_LDFLAGS with /nodefaultlib:libcmt to avoid CRT + // duplication in a normal /MD build. But our static build compiles everything with /MT, + // so every .obj file has DEFAULTLIB:LIBCMT embedded. Removing /nodefaultlib:libcmt lets + // the linker pick up libcmt.lib. We also exclude the dynamic CRT (/nodefaultlib:msvcrt + // /nodefaultlib:msvcrtd) to keep the DLL dependency-free, consistent with CLI/CGI/micro. + $content = str_replace( + 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:libcmt /def:$(PHPDEF)', + 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /def:$(PHPDEF) /ltcg /ignore:4286', + $content + ); + + // Patch embed lib target to build a REAL static library instead of just an import lib. + // The default embed target only includes embed SAPI objects and links against php8.lib (import lib). + // We need to include PHP core objects (PHP_GLOBAL_OBJS) and static extension objects (STATIC_EXT_OBJS) + // to create a self-contained static library that doesn't require php8.dll at runtime. + $major = intdiv($this->getPHPVersionID(), 10000); + $embed_lib = "php{$major}embed.lib"; + + // Find and replace the embed lib build rule + // Actual Makefile format (note the backslash before $(PHPLIB)): + // $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + // @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + $lines = explode("\r\n", $content); + $new_lines = []; + $i = 0; + while ($i < count($lines)) { + $line = $lines[$i]; + // Check if this is the embed lib target dependency line (contains the lib name and $(BUILD_DIR)\$(PHPLIB)) + if (str_contains($line, "\$(BUILD_DIR)\\{$embed_lib}:") && str_contains($line, '$(BUILD_DIR)\\$(PHPLIB)')) { + // Replace the dependency line + // Original: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + // New: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + $new_deps = "\$(BUILD_DIR)\\{$embed_lib}: \$(DEPS_EMBED) \$(EMBED_GLOBAL_OBJS) \$(PHP_GLOBAL_OBJS) \$(STATIC_EXT_OBJS) \$(ASM_OBJS) \$(BUILD_DIR)\\{$embed_lib}.res \$(BUILD_DIR)\\{$embed_lib}.manifest"; + $new_lines[] = $new_deps; + // Skip the original line (we replaced it) + ++$i; + // Now look for the lib.exe command line (should be the next non-empty line starting with tab) + while ($i < count($lines) && trim($lines[$i]) === '') { + $new_lines[] = $lines[$i]; + ++$i; + } + // Replace the lib.exe command to include PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP + // Original: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + // New: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + if ($i < count($lines) && str_contains($lines[$i], '$(MAKE_LIB)')) { + $cmd_line = $lines[$i]; + // Remove $(BUILD_DIR)\$(PHPLIB) from the command (note the backslash) + $cmd_line = str_replace(' $(BUILD_DIR)\\$(PHPLIB)', '', $cmd_line); + // Add PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP after EMBED_GLOBAL_OBJS_RESP + $cmd_line = str_replace( + '$(EMBED_GLOBAL_OBJS_RESP)', + '$(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS)', + $cmd_line + ); + $new_lines[] = $cmd_line; + ++$i; + } + } else { + $new_lines[] = $line; + ++$i; + } + } + $content = implode("\r\n", $new_lines); + + FileSystem::writeFile($makefile_path, $content); + } + + #[Stage] + public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + $major = intdiv($this->getPHPVersionID(), 10000); + $embed_lib = "php{$major}embed.lib"; + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow($embed_lib)); + + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" '; + } + } + + // Build the embed static library (patched to include PHP core and extension objects) + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}{$embed_lib}"); + + // Deploy: php8embed.lib is now a REAL static library containing all PHP code + $rel_type = 'Release'; // TODO: Debug build support + $ts = $builder->getOption('enable-zts') ? '_TS' : ''; + $build_dir = "{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}"; + + // copy static embed lib to buildroot/lib + $embed_lib_src = "{$build_dir}\\{$embed_lib}"; + if (file_exists($embed_lib_src)) { + FileSystem::copy($embed_lib_src, "{$package->getLibDir()}\\{$embed_lib}"); + $package->setOutput('Static library path for embed SAPI', "{$package->getLibDir()}\\{$embed_lib}"); + } + + // Note: We no longer deploy php8.dll because the embed static library is self-contained. + // All PHP core code, extensions, and embed SAPI are statically linked into php8embed.lib. + + // copy .pdb debug info if --no-strip + $debug_dir = BUILD_ROOT_PATH . '\debug'; + if ($builder->getOption('no-strip', false)) { + $pdb = "{$build_dir}\\php{$major}embed.pdb"; + if (file_exists($pdb)) { + FileSystem::createDir($debug_dir); + FileSystem::copy($pdb, "{$debug_dir}\\php{$major}embed.pdb"); + } + } + + // Install PHP headers for embed SAPI development + $this->installPhpHeadersForWindows($package, $installer); + } + + /** + * Install PHP headers to buildroot/include for embed SAPI development. + * This mirrors the 'make install-headers' behavior on Unix. + */ + private function installPhpHeadersForWindows(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Installing PHP headers for embed SAPI'); + + $source_dir = $package->getSourceDir(); + $include_dir = $package->getIncludeDir(); + $php_include_dir = "{$include_dir}\\php"; + + // Create directory structure + FileSystem::createDir("{$php_include_dir}\\main"); + FileSystem::createDir("{$php_include_dir}\\Zend"); + FileSystem::createDir("{$php_include_dir}\\TSRM"); + FileSystem::createDir("{$php_include_dir}\\sapi\\embed"); + + // Copy main/*.h + foreach (glob("{$source_dir}\\main\\*.h") as $h) { + FileSystem::copy($h, "{$php_include_dir}\\main\\" . basename($h)); + } + + // Copy Zend/*.h + foreach (glob("{$source_dir}\\Zend\\*.h") as $h) { + $target = "{$php_include_dir}\\Zend\\" . basename($h); + FileSystem::copy($h, $target); + // Fix GCC-specific #warning directive not supported by MSVC + if (basename($h) === 'zend_atomic.h') { + FileSystem::replaceFileStr($target, '#warning No atomics support detected. Please open an issue with platform details.', '#pragma message("No atomics support detected. Please open an issue with platform details.")'); + } + } + + // Copy TSRM/*.h + foreach (glob("{$source_dir}\\TSRM\\*.h") as $h) { + FileSystem::copy($h, "{$php_include_dir}\\TSRM\\" . basename($h)); + } + + // Copy embed SAPI header + FileSystem::copy("{$source_dir}\\sapi\\embed\\php_embed.h", "{$php_include_dir}\\sapi\\embed\\php_embed.h"); + + // Copy generated config.h (config.w32.h on Windows) to php_config.h + $rel_type = 'Release'; + $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; + $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; + + // Always copy config.w32.h from source (it's used for both build and headers) + if (file_exists("{$source_dir}\\main\\config.w32.h")) { + FileSystem::copy("{$source_dir}\\main\\config.w32.h", "{$php_include_dir}\\main\\php_config.h"); + } + + // Windows: zend_config.w32.h must be copied as zend_config.h for Zend headers to work + if (file_exists("{$source_dir}\\Zend\\zend_config.w32.h")) { + FileSystem::copy("{$source_dir}\\Zend\\zend_config.w32.h", "{$php_include_dir}\\Zend\\zend_config.h"); + } + + // Copy extension headers for enabled extensions + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + $ext_name = $ext->getExtensionName(); + $ext_dir = "{$source_dir}\\ext\\{$ext_name}"; + if (is_dir($ext_dir)) { + $target_ext_dir = "{$php_include_dir}\\ext\\{$ext_name}"; + FileSystem::createDir($target_ext_dir); + foreach (glob("{$ext_dir}\\*.h") as $h) { + FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); + } + // Also copy any arginfo headers + foreach (glob("{$ext_dir}\\*_arginfo.h") as $h) { + if (!file_exists("{$target_ext_dir}\\" . basename($h))) { + FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); + } + } + } + } + + $package->setOutput('PHP headers path for embed SAPI', $php_include_dir); + } + + #[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(); + 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", + '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()}\\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"); + } + } + } + + #[Stage] + public function smokeTestForWindows(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + $option = match ($no_smoke_test) { + false => false, + null => 'all', + default => parse_comma_list($no_smoke_test), + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + + // remove all .dll from buildroot/bin/ + $dlls = glob(BUILD_BIN_PATH . '\*.dll') ?: []; + foreach ($dlls as $dll) { + @unlink($dll); + } + + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForWindows']); + } + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForWindows']); + } + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForWindows'], ['skipExtTest' => $skipExtTest]); + } + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForWindows'], ['installer' => $installer]); + } + } + + #[Stage] + public function smokeTestCliForWindows(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliWindows(); + } + } + + #[Stage] + public function smokeTestCgiForWindows(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); + $raw_output = implode("\n", $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForWindows(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '\micro.sfx'; + + InteractiveTerm::setMessage('Running php-micro smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '\micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + [$ret, $out] = cmd()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPackage $package): void + { + $test_dir = SOURCE_PATH . '\embed-test'; + FileSystem::createDir($test_dir); + + // Create embed.c test file (Windows version) + $embed_c = <<<'C_CODE' +#include + +int main(int argc, char **argv) { + PHP_EMBED_START_BLOCK(argc, argv) + + zend_file_handle file_handle; + zend_stream_init_filename(&file_handle, "embed.php"); + + if (!php_execute_script(&file_handle)) { + php_printf("Failed to execute PHP script.\n"); + } + + PHP_EMBED_END_BLOCK() + return 0; +} +C_CODE; + FileSystem::writeFile($test_dir . '\embed.c', $embed_c); + + // Create embed.php test file + FileSystem::writeFile($test_dir . '\embed.php', "config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + + // Build the embed test executable using cl.exe + // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall + InteractiveTerm::setMessage('Running php-embed build smoke test'); + + // For Windows, we need to use PHP source directory headers directly + // because Windows PHP doesn't use php_config.h like Unix + $source_dir = $package->getSourceDir(); + $rel_type = 'Release'; + $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; + $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; + + // Build include flags pointing to source dirs (like PHP Windows build does) + // Note: embed.c uses #include , so we need $source_dir itself + $include_flags = sprintf( + '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . + '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', + $build_dir, + $source_dir, + $source_dir, + $source_dir, + $source_dir + ); + + // MSVC cl.exe format: compiler flags must come before /link, linker flags after + // ldflags contains /LIBPATH which must be after /link + $compile_cmd = sprintf( + 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', + $include_flags, + BUILD_ROOT_PATH, + $config['libs'], + 'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS) + ); + + // Log command explicitly (workaround for cmd() not logging complex commands properly) + logger()->debug('Embed smoke test compile command: ' . $compile_cmd); + + [$ret, $out] = cmd()->cd($test_dir)->execWithResult($compile_cmd); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: 'php-embed build smoke test' + ); + } + + // Run the embed test + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . $raw_output, + validation_module: 'php-embed run smoke test' + ); + } + } + + 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_file = "{$src[0]}\\{$src[1]}"; + $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); + + $builder->deployBinary($src_file, $dst_file); + + // copy .pdb debug info file + if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { + FileSystem::createDir($debug_dir); + FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); + } + + // with-upx-pack for cli, cgi and micro + if ($builder->getOption('with-upx-pack', false)) { + if (in_array($sapi, ['php-cli', 'php-cgi', 'php-micro'], true)) { + cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst_file)); + } + } + } +} diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index baaa27531..ca3dab739 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -155,6 +155,43 @@ public function getDistName(): string return $this->extension_config['display-name'] ?? $this->getExtensionName(); } + /** + * Run smoke test for the extension on Unix CLI. + * Override this method in a subclass. + */ + public function runSmokeTestCliWindows(): void + { + if (($this->extension_config['smoke-test'] ?? true) === false) { + return; + } + + $distName = $this->getDistName(); + // empty display-name → no --ri check (e.g. password_argon2) + if ($distName === '') { + return; + } + + [$ret] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n --ri "' . $distName . '"', false); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed compile check: php-cli returned {$ret}", + validation_module: 'Extension ' . $this->getName() . ' sanity check' + ); + } + + $test_file = ROOT_DIR . '/src/globals/ext-tests/' . $this->getExtensionName() . '.php'; + if (file_exists($test_file)) { + $test = self::escapeInlineTestWindows(file_get_contents($test_file)); + [$ret, $out] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "' . trim($test) . '"'); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out), + validation_module: 'Extension ' . $this->getName() . ' function check' + ); + } + } + } + /** * Run smoke test for the extension on Unix CLI. * Override this method in a subclass. @@ -394,4 +431,17 @@ private static function escapeInlineTest(string $code): string $code ); } + + /** + * Escape PHP test file content for inline `-r` usage on Windows cmd. + * Strips console_putput ? '' : ' 2>/dev/null'; - $tar = SystemTarget::isUnix() ? 'tar' : '"C:\\Windows\\system32\\tar.exe"'; + $tar = SystemTarget::isUnix() ? 'tar' : '"C:\Windows\system32\tar.exe"'; $cmd = "{$tar} {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; $this->logCommandInfo($cmd); @@ -187,7 +187,7 @@ public function execute7zExtract(string $archive_path, string $target_path): boo }; $extname = FileSystem::extname($archive_path); - $tar = SystemTarget::isUnix() ? 'tar' : '"C:\\Windows\\system32\\tar.exe"'; + $tar = SystemTarget::isUnix() ? 'tar' : '"C:\Windows\system32\tar.exe"'; match ($extname) { 'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'), diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index e9d7a6c0d..75c62bea9 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -1,62 +1,64 @@ -info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - - $original_command = $cmd; - $this->logCommandInfo($original_command); - $this->last_cmd = $cmd = $this->getExecString($cmd); - // echo $cmd . PHP_EOL; - - $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); - return $this; - } - - public function execWithWrapper(string $wrapper, string $args): WindowsCmd - { - return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); - } - - public function execWithResult(string $cmd, bool $with_log = true): array - { - if ($with_log) { - /* @phpstan-ignore-next-line */ - logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); - } else { - 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, env: $this->env); - $out = explode("\n", $result['output']); - return [$result['code'], $out]; - } - - public function getLastCommand(): string - { - return $this->last_cmd; - } - - private function getExecString(string $cmd): string - { - return $cmd; - } -} +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + + $original_command = $cmd; + $this->logCommandInfo($original_command); + $this->last_cmd = $cmd = $this->getExecString($cmd); + // echo $cmd . PHP_EOL; + + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); + return $this; + } + + public function execWithWrapper(string $wrapper, string $args): WindowsCmd + { + return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); + } + + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + logger()->debug('Running command with result: ' . $cmd); + } + $original_command = $cmd; + $this->logCommandInfo($original_command); + $cmd = $this->getExecString($cmd); + $result = $this->passthru($cmd, $this->console_putput, $original_command, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env); + $out = explode("\n", $result['output']); + return [$result['code'], $out]; + } + + public function getLastCommand(): string + { + return $this->last_cmd; + } + + private function getExecString(string $cmd): string + { + return $cmd; + } +} diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 8b6fe6b37..d31e8201a 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -1,437 +1,509 @@ -no_php = $options['no_php'] ?? false; - $this->libs_only_deps = $options['libs_only_deps'] ?? false; - $this->absolute_libs = $options['absolute_libs'] ?? false; - } - - public function config(array $packages = [], bool $include_suggests = false): array - { - // if have php, make php as all extension's dependency - if (!$this->no_php) { - $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; - } else { - $dep_override = []; - } - $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); - - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved); - $libs = $this->getLibsString($resolved, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - // embed - if ($extra_libs = SystemTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } - - $extra_env = getenv('SPC_EXTRA_LIBS'); - if (is_string($extra_env) && !empty($extra_env)) { - $libs .= " {$extra_env}"; - } - // package frameworks - if (SystemTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($resolved)}"; - } - // C++ - if ($this->hasCpp($resolved)) { - $libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } - - if ($this->libs_only_deps) { - // mimalloc must come first - if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); - } - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), - ]; - } - - // embed - if (!$this->no_php) { - $libs = "-lphp {$libs} -lc"; - } - - $allLibs = getenv('LIBS') . ' ' . $libs; - - // mimalloc must come first - if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); - } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; - } - - /** - * [Helper function] - * Get configuration for a specific extension(s) dependencies. - * - * @param array|PhpExtensionPackage $extension_packages Extension instance or list - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array - { - if (!is_array($extension_packages)) { - $extension_packages = [$extension_packages]; - } - return $this->config( - packages: array_map(fn ($y) => $y->getName(), $extension_packages), - include_suggests: $include_suggests, - ); - } - - /** - * [Helper function] - * Get configuration for a specific library(s) dependencies. - * - * @param array|LibraryPackage $lib Library instance or list - * @param bool $include_suggests Whether to include suggested libraries - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array - { - if (!is_array($lib)) { - $lib = [$lib]; - } - $save_no_php = $this->no_php; - $this->no_php = true; - $save_libs_only_deps = $this->libs_only_deps; - $this->libs_only_deps = true; - $ret = $this->config( - packages: array_map(fn ($y) => $y->getName(), $lib), - include_suggests: $include_suggests, - ); - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - return $ret; - } - - /** - * Get build configuration for a package and its sub-dependencies within a resolved set. - * - * This is useful when you need to statically link something against a specific - * library and all its transitive dependencies. It properly handles optional - * dependencies by only including those that were actually resolved. - * - * @param string $package_name The package to get config for - * @param string[] $resolved_packages The full resolved package list - * @param bool $include_suggests Whether to include resolved suggests - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array - { - // Get sub-dependencies within the resolved set - $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); - - if (empty($sub_deps)) { - return [ - 'cflags' => '', - 'ldflags' => '', - 'libs' => '', - ]; - } - - // Use libs_only_deps mode and no_php for library linking - $save_no_php = $this->no_php; - $save_libs_only_deps = $this->libs_only_deps; - $this->no_php = true; - $this->libs_only_deps = true; - - $ret = $this->configWithResolvedPackages($sub_deps); - - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - - return $ret; - } - - /** - * Get configuration using already-resolved packages (skip dependency resolution). - * - * @param string[] $resolved_packages Already resolved package names in build order - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function configWithResolvedPackages(array $resolved_packages): array - { - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved_packages); - $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - if ($extra_libs = SystemTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } - - $extra_env = getenv('SPC_EXTRA_LIBS'); - if (is_string($extra_env) && !empty($extra_env)) { - $libs .= " {$extra_env}"; - } - - // package frameworks - if (SystemTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($resolved_packages)}"; - } - - // C++ - if ($this->hasCpp($resolved_packages)) { - $libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } - - if ($this->libs_only_deps) { - // mimalloc must come first - if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); - } - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), - ]; - } - - // embed - if (!$this->no_php) { - $libs = "-lphp {$libs} -lc"; - } - - $allLibs = getenv('LIBS') . ' ' . $libs; - - // mimalloc must come first - if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); - } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; - } - - private function hasCpp(array $packages): bool - { - foreach ($packages as $package) { - $lang = PackageConfig::get($package, 'lang', 'c'); - if ($lang === 'cpp') { - return true; - } - } - return false; - } - - private function getIncludesString(array $packages): string - { - $base = BUILD_INCLUDE_PATH; - $includes = ["-I{$base}"]; - - // link with libphp - if (!$this->no_php) { - $includes = [ - ...$includes, - "-I{$base}/php", - "-I{$base}/php/main", - "-I{$base}/php/TSRM", - "-I{$base}/php/Zend", - "-I{$base}/php/ext", - ]; - } - - // parse pkg-configs - foreach ($packages as $package) { - $pc = PackageConfig::get($package, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(SystemTarget::isUnix() ? ':' : ';', $pkg_config_path)); - foreach ($pc as $file) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$file}.pc")) { - $found = true; - break; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); - } - } - $pc_cflags = implode(' ', $pc); - if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { - $arr = explode(' ', $pc_cflags); - $arr = array_unique($arr); - $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); - $pc_cflags = implode(' ', $arr); - $includes[] = $pc_cflags; - } - } - $includes = array_unique($includes); - return implode(' ', $includes); - } - - private function getLdflagsString(): string - { - return '-L' . BUILD_LIB_PATH; - } - - private function getLibsString(array $packages, bool $use_short_libs = true): string - { - $lib_names = []; - $frameworks = []; - - foreach ($packages as $package) { - // parse pkg-configs only for unix systems - if (SystemTarget::isUnix()) { - // add pkg-configs libs - $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(':', $pkg_config_path)); - foreach ($pkg_configs as $pkg_config) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$pkg_config}.pc")) { - $found = true; - break; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); - } - } - $pkg_configs = implode(' ', $pkg_configs); - if ($pkg_configs !== '') { - // static libs with dependencies come in reverse order, so reverse this too - $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); - $lib_names = [...$lib_names, ...$pc_libs]; - } - } - // convert all static-libs to short names - $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); - foreach ($libs as $lib) { - if (FileSystem::isRelativePath($lib)) { - // check file existence - if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { - throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); - } - $lib_names[] = $this->getShortLibName($lib); - } else { - $lib_names[] = $lib; - } - } - // add frameworks for macOS - if (SystemTarget::getTargetOS() === 'Darwin') { - $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); - } - } - - // post-process - $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); - $lib_names = array_reverse(array_unique($lib_names)); - $frameworks = array_unique($frameworks); - - // process frameworks to short_name - if (SystemTarget::getTargetOS() === 'Darwin') { - foreach ($frameworks as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $lib_names)) { - $lib_names[] = $ks; - } - } - } - - if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { - if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { - $lib_names[] = '-lcrypt'; - } - } - if (!$use_short_libs) { - $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); - } - return implode(' ', $lib_names); - } - - private function getShortLibName(string $lib): string - { - if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { - return BUILD_LIB_PATH . '/' . $lib; - } - // get short name - return '-l' . substr($lib, 3, -2); - } - - private function getFullLibName(string $lib): string - { - if (!str_starts_with($lib, '-l')) { - return $lib; - } - $libname = substr($lib, 2); - $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; - if (file_exists($staticLib)) { - return $staticLib; - } - return $lib; - } - - private function getFrameworksString(array $extensions): string - { - $list = []; - foreach ($extensions as $extension) { - foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $list)) { - $list[] = $ks; - } - } - } - return implode(' ', $list); - } -} +no_php = $options['no_php'] ?? false; + $this->libs_only_deps = $options['libs_only_deps'] ?? false; + $this->absolute_libs = $options['absolute_libs'] ?? false; + } + + public function config(array $packages = [], bool $include_suggests = false): array + { + // if have php, make php as all extension's dependency + if (!$this->no_php) { + $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; + } else { + $dep_override = []; + } + $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); + + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved); + $libs = $this->getLibsString($resolved, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + // embed + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved)}"; + } + // C++ + if ($this->hasCpp($resolved)) { + $target_os = SystemTarget::getTargetOS(); + if ($target_os === 'Darwin') { + $libcpp = '-lc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } elseif ($target_os !== 'Windows') { + // Linux and other Unix-like systems use libstdc++ + $libcpp = '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + if (SystemTarget::getTargetOS() === 'Windows') { + // Windows: use php8embed.lib directly (either full path or short name) + $major = intdiv(PHP_VERSION_ID, 10000); + $php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib"; + // Windows system libs required by PHP + // Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib + $libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib"; + } else { + $libs = "-lphp {$libs} -lc"; + } + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + + /** + * [Helper function] + * Get configuration for a specific extension(s) dependencies. + * + * @param array|PhpExtensionPackage $extension_packages Extension instance or list + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array + { + if (!is_array($extension_packages)) { + $extension_packages = [$extension_packages]; + } + return $this->config( + packages: array_map(fn ($y) => $y->getName(), $extension_packages), + include_suggests: $include_suggests, + ); + } + + /** + * [Helper function] + * Get configuration for a specific library(s) dependencies. + * + * @param array|LibraryPackage $lib Library instance or list + * @param bool $include_suggests Whether to include suggested libraries + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array + { + if (!is_array($lib)) { + $lib = [$lib]; + } + $save_no_php = $this->no_php; + $this->no_php = true; + $save_libs_only_deps = $this->libs_only_deps; + $this->libs_only_deps = true; + $ret = $this->config( + packages: array_map(fn ($y) => $y->getName(), $lib), + include_suggests: $include_suggests, + ); + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + return $ret; + } + + /** + * Get build configuration for a package and its sub-dependencies within a resolved set. + * + * This is useful when you need to statically link something against a specific + * library and all its transitive dependencies. It properly handles optional + * dependencies by only including those that were actually resolved. + * + * @param string $package_name The package to get config for + * @param string[] $resolved_packages The full resolved package list + * @param bool $include_suggests Whether to include resolved suggests + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array + { + // Get sub-dependencies within the resolved set + $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); + + if (empty($sub_deps)) { + return [ + 'cflags' => '', + 'ldflags' => '', + 'libs' => '', + ]; + } + + // Use libs_only_deps mode and no_php for library linking + $save_no_php = $this->no_php; + $save_libs_only_deps = $this->libs_only_deps; + $this->no_php = true; + $this->libs_only_deps = true; + + $ret = $this->configWithResolvedPackages($sub_deps); + + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + + return $ret; + } + + /** + * Get configuration using already-resolved packages (skip dependency resolution). + * + * @param string[] $resolved_packages Already resolved package names in build order + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function configWithResolvedPackages(array $resolved_packages): array + { + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved_packages); + $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved_packages)}"; + } + + // C++ + if ($this->hasCpp($resolved_packages)) { + $target_os = SystemTarget::getTargetOS(); + if ($target_os === 'Darwin') { + $libcpp = '-lc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } elseif ($target_os !== 'Windows') { + // Linux and other Unix-like systems use libstdc++ + $libcpp = '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + $libs = "-lphp {$libs} -lc"; + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + + private function hasCpp(array $packages): bool + { + foreach ($packages as $package) { + $lang = PackageConfig::get($package, 'lang', 'c'); + if ($lang === 'cpp') { + return true; + } + } + return false; + } + + private function getIncludesString(array $packages): string + { + $base = BUILD_INCLUDE_PATH; + + // Windows MSVC uses /I flag instead of -I + if (SystemTarget::getTargetOS() === 'Windows') { + $includes = ["/I\"{$base}\""]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "/I\"{$base}\\php\"", + "/I\"{$base}\\php\\main\"", + "/I\"{$base}\\php\\TSRM\"", + "/I\"{$base}\\php\\Zend\"", + "/I\"{$base}\\php\\ext\"", + ]; + } + } else { + $includes = ["-I{$base}"]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "-I{$base}/php", + "-I{$base}/php/main", + "-I{$base}/php/TSRM", + "-I{$base}/php/Zend", + "-I{$base}/php/ext", + ]; + } + } + + // parse pkg-configs (only for Unix) + if (SystemTarget::isUnix()) { + foreach ($packages as $package) { + $pc = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); + foreach ($pc as $file) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$file}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); + } + } + $pc_cflags = implode(' ', $pc); + if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { + $arr = explode(' ', $pc_cflags); + $arr = array_unique($arr); + $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); + $pc_cflags = implode(' ', $arr); + $includes[] = $pc_cflags; + } + } + } + $includes = array_unique($includes); + return implode(' ', $includes); + } + + private function getLdflagsString(): string + { + // Windows MSVC uses /LIBPATH flag instead of -L + if (SystemTarget::getTargetOS() === 'Windows') { + return '/LIBPATH:"' . BUILD_LIB_PATH . '"'; + } + return '-L' . BUILD_LIB_PATH; + } + + private function getLibsString(array $packages, bool $use_short_libs = true): string + { + $lib_names = []; + $frameworks = []; + + foreach ($packages as $package) { + // parse pkg-configs only for unix systems + if (SystemTarget::isUnix()) { + // add pkg-configs libs + $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); + foreach ($pkg_configs as $pkg_config) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$pkg_config}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); + } + } + $pkg_configs = implode(' ', $pkg_configs); + if ($pkg_configs !== '') { + // static libs with dependencies come in reverse order, so reverse this too + $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); + $lib_names = [...$lib_names, ...$pc_libs]; + } + } + // convert all static-libs to short names + $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); + foreach ($libs as $lib) { + if (FileSystem::isRelativePath($lib)) { + // check file existence + if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { + throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); + } + $lib_names[] = $this->getShortLibName($lib); + } else { + $lib_names[] = $lib; + } + } + // add frameworks for macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); + } + } + + // post-process + $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); + $lib_names = array_reverse(array_unique($lib_names)); + $frameworks = array_unique($frameworks); + + // process frameworks to short_name + if (SystemTarget::getTargetOS() === 'Darwin') { + foreach ($frameworks as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $lib_names)) { + $lib_names[] = $ks; + } + } + } + + if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { + if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { + $lib_names[] = '-lcrypt'; + } + } + if (!$use_short_libs) { + $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); + } + return implode(' ', $lib_names); + } + + private function getShortLibName(string $lib): string + { + // Windows: library files are xxx.lib format (not libxxx.a) + if (SystemTarget::getTargetOS() === 'Windows') { + if (!str_ends_with($lib, '.lib')) { + return BUILD_LIB_PATH . '\\' . $lib; + } + // For Windows, return just the library filename (e.g., "libssl.lib") + return $lib; + } + + // Unix: library files are libxxx.a format + if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { + return BUILD_LIB_PATH . '/' . $lib; + } + // get short name (e.g., "libssl.a" -> "-lssl") + return '-l' . substr($lib, 3, -2); + } + + private function getFullLibName(string $lib): string + { + // Windows: libraries don't use -l prefix, return as-is or with full path + if (SystemTarget::getTargetOS() === 'Windows') { + if (str_ends_with($lib, '.lib') && !str_contains($lib, '\\') && !str_contains($lib, '/')) { + // It's a short lib name like "libssl.lib", convert to full path + $fullPath = BUILD_LIB_PATH . '\\' . $lib; + if (file_exists($fullPath)) { + return $fullPath; + } + } + return $lib; + } + + // Unix: convert -lxxx to full path + if (!str_starts_with($lib, '-l')) { + return $lib; + } + $libname = substr($lib, 2); + $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; + if (file_exists($staticLib)) { + return $staticLib; + } + return $lib; + } + + private function getFrameworksString(array $extensions): string + { + $list = []; + foreach ($extensions as $extension) { + foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $list)) { + $list[] = $ks; + } + } + } + return implode(' ', $list); + } +} diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index b6d943be4..150730c0a 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -138,6 +138,28 @@ public static function writeCmakeFindModules(): void FileSystem::writeFile($cmake_find_dir . DIRECTORY_SEPARATOR . 'FindOpenSSL.cmake', <<<'CMAKE' # Custom FindOpenSSL.cmake wrapper for static-php-cli Windows builds. +set(_spc_saved_module_path "${CMAKE_MODULE_PATH}") +list(REMOVE_ITEM CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") + +set(_spc_find_args "") +if(OpenSSL_FIND_VERSION) + list(APPEND _spc_find_args "${OpenSSL_FIND_VERSION}") + if(OpenSSL_FIND_VERSION_EXACT) + list(APPEND _spc_find_args EXACT) + endif() +endif() +if(OpenSSL_FIND_REQUIRED) + list(APPEND _spc_find_args REQUIRED) +endif() +if(OpenSSL_FIND_QUIETLY) + list(APPEND _spc_find_args QUIET) +endif() +find_package(OpenSSL ${_spc_find_args}) +unset(_spc_find_args) + +set(CMAKE_MODULE_PATH "${_spc_saved_module_path}") +unset(_spc_saved_module_path) + if(WIN32 AND (OpenSSL_FOUND OR OPENSSL_FOUND)) list(GET CMAKE_FIND_ROOT_PATH 0 _spc_buildroot) # Normalize to forward slashes — backslash paths cause 'Invalid character From a7184d04115e2dc947fb8659aff5abb03b5a8dfa Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 2 Apr 2026 16:32:43 +0800 Subject: [PATCH 529/682] Fix sqlsrv redundant cflags when building PHP --- src/SPC/builder/extension/sqlsrv.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/SPC/builder/extension/sqlsrv.php b/src/SPC/builder/extension/sqlsrv.php index 04bd52261..3f47134b7 100644 --- a/src/SPC/builder/extension/sqlsrv.php +++ b/src/SPC/builder/extension/sqlsrv.php @@ -33,4 +33,14 @@ public function patchBeforeWindowsConfigure(): bool } return false; } + + public function patchBeforeMake(): bool + { + $makefile = SOURCE_PATH . '\php-src\Makefile'; + $makeContent = file_get_contents($makefile); + $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/W4\b/m', '$1', $makeContent); + $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/WX\b/m', '$1', $makeContent); + file_put_contents($makefile, $makeContent); + return true; + } } From e592488d7a0d1c5b6a57abb1e9053b4ac29086ea Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 2 Apr 2026 16:34:51 +0800 Subject: [PATCH 530/682] Add test --- src/globals/test-extensions.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index ccc561052..34bc74436 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -24,11 +24,11 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ // 'macos-15-intel', // bin/spc for x86_64 - 'macos-15', // bin/spc for arm64 - 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + // 'macos-15', // bin/spc for arm64 + // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 + // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 // 'ubuntu-24.04', // bin/spc for x86_64 - 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 'windows-2025', @@ -51,7 +51,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { 'Linux', 'Darwin' => 'zlib', - 'Windows' => 'gd,zlib,mbstring,filter', + 'Windows' => 'amqp,apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,ds,exif,ffi,fileinfo,filter,ftp,gd,iconv,igbinary,libxml,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pdo,pdo_mysql,pdo_sqlite,pdo_sqlsrv,phar,rar,redis,session,shmop,simdjson,simplexml,soap,sockets,sqlite3,sqlsrv,ssh2,sysvshm,tokenizer,xml,xmlreader,xmlwriter,yac,yaml,zip,zlib', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). From cae668a9472c0e766189ab9ad81ddfc4b3a5a46e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 2 Apr 2026 17:59:15 +0800 Subject: [PATCH 531/682] Remove zstd suggested libs for v2 (implemented on v3) Anyway we don't support zstd windows build before --- config/lib.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/lib.json b/config/lib.json index f960ab82f..a11a33157 100644 --- a/config/lib.json +++ b/config/lib.json @@ -109,8 +109,7 @@ "krb5" ], "lib-suggests-windows": [ - "brotli", - "zstd" + "brotli" ], "frameworks": [ "CoreFoundation", From 08a6bf38a4424318c4352ebdf23e3be909382115 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 2 Apr 2026 18:04:03 +0800 Subject: [PATCH 532/682] Remove zstd suggested libs for v2 (implemented on v3) Anyway we don't support zstd windows build before --- config/lib.json | 1 - 1 file changed, 1 deletion(-) diff --git a/config/lib.json b/config/lib.json index a11a33157..33633ad09 100644 --- a/config/lib.json +++ b/config/lib.json @@ -762,7 +762,6 @@ "xz" ], "lib-suggests-windows": [ - "zstd", "openssl" ] }, From 3ded9881e11bc93d946ff9c60f6336259ec67e73 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 09:55:46 +0800 Subject: [PATCH 533/682] Disable openssl for ngtcp2 temporarily --- src/SPC/builder/windows/library/ngtcp2.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/windows/library/ngtcp2.php b/src/SPC/builder/windows/library/ngtcp2.php index d0f557b71..79895dd6b 100644 --- a/src/SPC/builder/windows/library/ngtcp2.php +++ b/src/SPC/builder/windows/library/ngtcp2.php @@ -29,6 +29,7 @@ protected function build(): void '-DBUILD_SHARED_LIBS=OFF ' . '-DENABLE_STATIC_CRT=ON ' . '-DENABLE_LIB_ONLY=ON ' . + '-DENABLE_OPENSSL=OFF ' . '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' ) ->execWithWrapper( From 852a0437bdec3b3127e9cc04b48144ff3ace4f8b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 11:03:22 +0800 Subject: [PATCH 534/682] Disable openssl for ngtcp2 temporarily --- src/Package/Library/ngtcp2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/ngtcp2.php b/src/Package/Library/ngtcp2.php index c88b643bf..0a5ca8f5a 100644 --- a/src/Package/Library/ngtcp2.php +++ b/src/Package/Library/ngtcp2.php @@ -24,7 +24,7 @@ public function buildWin(LibraryPackage $lib): void '-DBUILD_SHARED_LIBS=OFF', '-DENABLE_STATIC_CRT=ON', '-DENABLE_LIB_ONLY=ON', - '-DENABLE_OPENSSL=ON', + '-DENABLE_OPENSSL=OFF', ) ->build(); } From c339b900f892025fd3838c662678eab5580fd9fd Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 11:33:55 +0800 Subject: [PATCH 535/682] Disable simd for libjpeg --- src/SPC/builder/windows/library/libjpeg.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/windows/library/libjpeg.php b/src/SPC/builder/windows/library/libjpeg.php index d7ac718cd..0e5a69994 100644 --- a/src/SPC/builder/windows/library/libjpeg.php +++ b/src/SPC/builder/windows/library/libjpeg.php @@ -28,6 +28,7 @@ protected function build(): void '-DENABLE_STATIC=ON ' . '-DBUILD_TESTING=OFF ' . '-DWITH_JAVA=OFF ' . + '-DWITH_SIMD=OFF ' . '-DWITH_CRT_DLL=OFF ' . "-DENABLE_ZLIB_COMPRESSION={$zlib} " . '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' From e5ad72214c82e5116e56527a0c054cb6a0354c06 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 14:18:13 +0800 Subject: [PATCH 536/682] Add brotli to curl (just workaround for transitive deps) --- config/ext.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/ext.json b/config/ext.json index 79d3831ec..16a71c212 100644 --- a/config/ext.json +++ b/config/ext.json @@ -63,7 +63,8 @@ ], "ext-depends-windows": [ "zlib", - "openssl" + "openssl", + "brotli" ] }, "dba": { From 1a476d0e802fc9b4001dd4e1023739fb0e59b2a0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 15:34:56 +0800 Subject: [PATCH 537/682] Fix xcopy command in FileSystem.php by removing the 'v' flag --- src/SPC/store/FileSystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 1d0815ce2..d2d44b51f 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -152,7 +152,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'); + f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/y/i'); break; case 'Linux': case 'Darwin': From fb8f8d4ef89bce1c6569cac951e8f8754cb29239 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 15:49:02 +0800 Subject: [PATCH 538/682] Correct openssl test script condition --- src/globals/ext-tests/openssl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/ext-tests/openssl.php b/src/globals/ext-tests/openssl.php index c687d0fc7..34228988f 100644 --- a/src/globals/ext-tests/openssl.php +++ b/src/globals/ext-tests/openssl.php @@ -31,6 +31,6 @@ } assert($valid); } -if (PHP_VERSION_ID >= 80500 && defined('OPENSSL_VERSION_NUMBER') && OPENSSL_VERSION_NUMBER >= 0x30200000) { +if (PHP_VERSION_ID >= 80500 && !PHP_ZTS && defined('OPENSSL_VERSION_NUMBER') && OPENSSL_VERSION_NUMBER >= 0x30200000) { assert(function_exists('openssl_password_hash')); } From 51b8a0cab56e83f2ee784eea81d03eeabf0d0f97 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 16:26:20 +0800 Subject: [PATCH 539/682] Fix upx packing for win --- src/Package/Target/php/windows.php | 1559 +++++++++++----------- src/StaticPHP/Package/PackageBuilder.php | 2 +- 2 files changed, 777 insertions(+), 784 deletions(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 1188a0e49..bcb4a7213 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -1,783 +1,776 @@ -getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); - } - - #[Stage] - public function buildconfForWindows(TargetPackage $package): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); - V2CompatLayer::emitPatchPoint('before-php-buildconf'); - cmd()->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'); - $embed = $installer->isPackageResolved('php-embed'); - $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; - $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; - $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; - $args[] = $embed ? '--enable-embed=yes' : '--enable-embed=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, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); - - // Collect static-libs@windows from all resolved library packages. - // PHP's configure.bat only adds libs declared by enabled extensions via config.w32; - // transitive library-only deps (e.g. zlibstatic.lib needed by libcrypto.lib) are - // not covered. Inject them here so the final link step has all required symbols. - $resolved_libs = []; - foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { - foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { - if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { - $resolved_libs[] = $lib_file; - } - } - } - $resolved_libs = array_unique($resolved_libs); - - // extra lib - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_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'); - } - - #[BeforeStage('php', [self::class, 'makeCgiForWindows'])] - #[PatchDescription('Patch Windows Makefile for CGI target')] - public function patchCGITarget(TargetPackage $package): void - { - // search Makefile code line contains "$(BUILD_DIR)\php-cgi.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-cgi.exe:')) { - $found = $line_num; - break; - } - ++$line_num; - } - if ($found === false) { - throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); - } - $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; - $lines[$line_num + 1] = "\t" . '@"$(LINK)” /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; - FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); - - // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() - FileSystem::replaceFileRegex("{$package->getSourceDir()}\\sapi\\cgi\\cgi_main.c", '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); - } - - #[Stage] - public function makeCgiForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php-cgi.exe')); - - // Collect static-libs@windows from all resolved library packages. - $resolved_libs = []; - foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { - foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { - if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { - $resolved_libs[] = $lib_file; - } - } - } - $resolved_libs = array_unique($resolved_libs); - - // extra lib - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; - } - } - - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe"); - - $this->deployWindowsBinary($builder, $package, 'php-cgi'); - } - - #[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']); - } - if ($installer->isPackageResolved('php-embed')) { - $package->runStage([$this, 'makeEmbedForWindows']); - } - } - - #[Stage] - public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('micro.sfx')); - - // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) - $makefile = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); - if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { - $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; - } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { - $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n"; - } - FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", $makefile); - - // Collect static-libs@windows from all resolved library packages. - $resolved_libs = []; - foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { - foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { - if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { - $resolved_libs[] = $lib_file; - } - } - } - $resolved_libs = array_unique($resolved_libs); - - // extra lib - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; - } - } - - $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; - - // phar patch for micro - $phar_patched = false; - if ($installer->isPackageResolved('ext-phar')) { - $phar_patched = true; - SourcePatcher::patchMicroPhar(self::getPHPVersionID()); - } - - try { - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); - } finally { - if ($phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - - $this->deployWindowsBinary($builder, $package, 'php-micro'); - } - - #[BeforeStage('php', [self::class, 'makeEmbedForWindows'])] - #[PatchDescription('Patch Windows Makefile for embed static library target')] - public function patchEmbedTarget(TargetPackage $package): void - { - $makefile_path = "{$package->getSourceDir()}\\Makefile"; - $content = FileSystem::readFile($makefile_path); - - // PHP's configure.bat generates PHP_LDFLAGS with /nodefaultlib:libcmt to avoid CRT - // duplication in a normal /MD build. But our static build compiles everything with /MT, - // so every .obj file has DEFAULTLIB:LIBCMT embedded. Removing /nodefaultlib:libcmt lets - // the linker pick up libcmt.lib. We also exclude the dynamic CRT (/nodefaultlib:msvcrt - // /nodefaultlib:msvcrtd) to keep the DLL dependency-free, consistent with CLI/CGI/micro. - $content = str_replace( - 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:libcmt /def:$(PHPDEF)', - 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /def:$(PHPDEF) /ltcg /ignore:4286', - $content - ); - - // Patch embed lib target to build a REAL static library instead of just an import lib. - // The default embed target only includes embed SAPI objects and links against php8.lib (import lib). - // We need to include PHP core objects (PHP_GLOBAL_OBJS) and static extension objects (STATIC_EXT_OBJS) - // to create a self-contained static library that doesn't require php8.dll at runtime. - $major = intdiv($this->getPHPVersionID(), 10000); - $embed_lib = "php{$major}embed.lib"; - - // Find and replace the embed lib build rule - // Actual Makefile format (note the backslash before $(PHPLIB)): - // $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest - // @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res - $lines = explode("\r\n", $content); - $new_lines = []; - $i = 0; - while ($i < count($lines)) { - $line = $lines[$i]; - // Check if this is the embed lib target dependency line (contains the lib name and $(BUILD_DIR)\$(PHPLIB)) - if (str_contains($line, "\$(BUILD_DIR)\\{$embed_lib}:") && str_contains($line, '$(BUILD_DIR)\\$(PHPLIB)')) { - // Replace the dependency line - // Original: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest - // New: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest - $new_deps = "\$(BUILD_DIR)\\{$embed_lib}: \$(DEPS_EMBED) \$(EMBED_GLOBAL_OBJS) \$(PHP_GLOBAL_OBJS) \$(STATIC_EXT_OBJS) \$(ASM_OBJS) \$(BUILD_DIR)\\{$embed_lib}.res \$(BUILD_DIR)\\{$embed_lib}.manifest"; - $new_lines[] = $new_deps; - // Skip the original line (we replaced it) - ++$i; - // Now look for the lib.exe command line (should be the next non-empty line starting with tab) - while ($i < count($lines) && trim($lines[$i]) === '') { - $new_lines[] = $lines[$i]; - ++$i; - } - // Replace the lib.exe command to include PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP - // Original: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res - // New: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res - if ($i < count($lines) && str_contains($lines[$i], '$(MAKE_LIB)')) { - $cmd_line = $lines[$i]; - // Remove $(BUILD_DIR)\$(PHPLIB) from the command (note the backslash) - $cmd_line = str_replace(' $(BUILD_DIR)\\$(PHPLIB)', '', $cmd_line); - // Add PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP after EMBED_GLOBAL_OBJS_RESP - $cmd_line = str_replace( - '$(EMBED_GLOBAL_OBJS_RESP)', - '$(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS)', - $cmd_line - ); - $new_lines[] = $cmd_line; - ++$i; - } - } else { - $new_lines[] = $line; - ++$i; - } - } - $content = implode("\r\n", $new_lines); - - FileSystem::writeFile($makefile_path, $content); - } - - #[Stage] - public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void - { - $major = intdiv($this->getPHPVersionID(), 10000); - $embed_lib = "php{$major}embed.lib"; - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow($embed_lib)); - - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" '; - } - } - - // Build the embed static library (patched to include PHP core and extension objects) - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}{$embed_lib}"); - - // Deploy: php8embed.lib is now a REAL static library containing all PHP code - $rel_type = 'Release'; // TODO: Debug build support - $ts = $builder->getOption('enable-zts') ? '_TS' : ''; - $build_dir = "{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}"; - - // copy static embed lib to buildroot/lib - $embed_lib_src = "{$build_dir}\\{$embed_lib}"; - if (file_exists($embed_lib_src)) { - FileSystem::copy($embed_lib_src, "{$package->getLibDir()}\\{$embed_lib}"); - $package->setOutput('Static library path for embed SAPI', "{$package->getLibDir()}\\{$embed_lib}"); - } - - // Note: We no longer deploy php8.dll because the embed static library is self-contained. - // All PHP core code, extensions, and embed SAPI are statically linked into php8embed.lib. - - // copy .pdb debug info if --no-strip - $debug_dir = BUILD_ROOT_PATH . '\debug'; - if ($builder->getOption('no-strip', false)) { - $pdb = "{$build_dir}\\php{$major}embed.pdb"; - if (file_exists($pdb)) { - FileSystem::createDir($debug_dir); - FileSystem::copy($pdb, "{$debug_dir}\\php{$major}embed.pdb"); - } - } - - // Install PHP headers for embed SAPI development - $this->installPhpHeadersForWindows($package, $installer); - } - - /** - * Install PHP headers to buildroot/include for embed SAPI development. - * This mirrors the 'make install-headers' behavior on Unix. - */ - private function installPhpHeadersForWindows(TargetPackage $package, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Installing PHP headers for embed SAPI'); - - $source_dir = $package->getSourceDir(); - $include_dir = $package->getIncludeDir(); - $php_include_dir = "{$include_dir}\\php"; - - // Create directory structure - FileSystem::createDir("{$php_include_dir}\\main"); - FileSystem::createDir("{$php_include_dir}\\Zend"); - FileSystem::createDir("{$php_include_dir}\\TSRM"); - FileSystem::createDir("{$php_include_dir}\\sapi\\embed"); - - // Copy main/*.h - foreach (glob("{$source_dir}\\main\\*.h") as $h) { - FileSystem::copy($h, "{$php_include_dir}\\main\\" . basename($h)); - } - - // Copy Zend/*.h - foreach (glob("{$source_dir}\\Zend\\*.h") as $h) { - $target = "{$php_include_dir}\\Zend\\" . basename($h); - FileSystem::copy($h, $target); - // Fix GCC-specific #warning directive not supported by MSVC - if (basename($h) === 'zend_atomic.h') { - FileSystem::replaceFileStr($target, '#warning No atomics support detected. Please open an issue with platform details.', '#pragma message("No atomics support detected. Please open an issue with platform details.")'); - } - } - - // Copy TSRM/*.h - foreach (glob("{$source_dir}\\TSRM\\*.h") as $h) { - FileSystem::copy($h, "{$php_include_dir}\\TSRM\\" . basename($h)); - } - - // Copy embed SAPI header - FileSystem::copy("{$source_dir}\\sapi\\embed\\php_embed.h", "{$php_include_dir}\\sapi\\embed\\php_embed.h"); - - // Copy generated config.h (config.w32.h on Windows) to php_config.h - $rel_type = 'Release'; - $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; - $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; - - // Always copy config.w32.h from source (it's used for both build and headers) - if (file_exists("{$source_dir}\\main\\config.w32.h")) { - FileSystem::copy("{$source_dir}\\main\\config.w32.h", "{$php_include_dir}\\main\\php_config.h"); - } - - // Windows: zend_config.w32.h must be copied as zend_config.h for Zend headers to work - if (file_exists("{$source_dir}\\Zend\\zend_config.w32.h")) { - FileSystem::copy("{$source_dir}\\Zend\\zend_config.w32.h", "{$php_include_dir}\\Zend\\zend_config.h"); - } - - // Copy extension headers for enabled extensions - foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { - $ext_name = $ext->getExtensionName(); - $ext_dir = "{$source_dir}\\ext\\{$ext_name}"; - if (is_dir($ext_dir)) { - $target_ext_dir = "{$php_include_dir}\\ext\\{$ext_name}"; - FileSystem::createDir($target_ext_dir); - foreach (glob("{$ext_dir}\\*.h") as $h) { - FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); - } - // Also copy any arginfo headers - foreach (glob("{$ext_dir}\\*_arginfo.h") as $h) { - if (!file_exists("{$target_ext_dir}\\" . basename($h))) { - FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); - } - } - } - } - - $package->setOutput('PHP headers path for embed SAPI', $php_include_dir); - } - - #[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(); - 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", - '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()}\\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"); - } - } - } - - #[Stage] - public function smokeTestForWindows(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void - { - // analyse --no-smoke-test option - $no_smoke_test = $builder->getOption('no-smoke-test'); - $option = match ($no_smoke_test) { - false => false, - null => 'all', - default => parse_comma_list($no_smoke_test), - }; - $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed']; - // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts - if ($builder->getOption('without-micro-ext-test', false)) { - $valid_tests = array_diff($valid_tests, ['micro-exts']); - } - if (is_array($option)) { - foreach ($option as $test) { - if (!in_array($test, $valid_tests, true)) { - throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); - } - $valid_tests = array_diff($valid_tests, [$test]); - } - } elseif ($option === 'all') { - $valid_tests = []; - } - - // remove all .dll from buildroot/bin/ - $dlls = glob(BUILD_BIN_PATH . '\*.dll') ?: []; - foreach ($dlls as $dll) { - @unlink($dll); - } - - if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { - $package->runStage([$this, 'smokeTestCliForWindows']); - } - if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { - $package->runStage([$this, 'smokeTestCgiForWindows']); - } - if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { - $skipExtTest = !in_array('micro-exts', $valid_tests, true); - $package->runStage([$this, 'smokeTestMicroForWindows'], ['skipExtTest' => $skipExtTest]); - } - if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { - $package->runStage([$this, 'smokeTestEmbedForWindows'], ['installer' => $installer]); - } - } - - #[Stage] - public function smokeTestCliForWindows(PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Running basic php-cli smoke test'); - [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); - $raw_output = implode('', $output); - if ($ret !== 0 || trim($raw_output) !== 'hello') { - throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); - } - - $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); - foreach ($exts as $ext) { - InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); - $ext->runSmokeTestCliWindows(); - } - } - - #[Stage] - public function smokeTestCgiForWindows(): void - { - InteractiveTerm::setMessage('Running basic php-cgi smoke test'); - FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); - [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); - $raw_output = implode("\n", $output); - if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { - throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); - } - } - - #[Stage] - public function smokeTestMicroForWindows(PackageInstaller $installer, bool $skipExtTest = false): void - { - $micro_sfx = BUILD_BIN_PATH . '\micro.sfx'; - - InteractiveTerm::setMessage('Running php-micro smoke test'); - $content = $skipExtTest - ? 'generateMicroExtTests($installer); - $test_file = SOURCE_PATH . '\micro_ext_test.exe'; - if (file_exists($test_file)) { - @unlink($test_file); - } - file_put_contents($test_file, file_get_contents($micro_sfx) . $content); - [$ret, $out] = cmd()->execWithResult($test_file); - $raw_out = trim(implode('', $out)); - if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { - throw new ValidationException( - "micro_ext_test failed. code: {$ret}, output: {$raw_out}", - validation_module: 'phpmicro sanity check item [micro_ext_test]' - ); - } - } - - #[Stage] - public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPackage $package): void - { - $test_dir = SOURCE_PATH . '\embed-test'; - FileSystem::createDir($test_dir); - - // Create embed.c test file (Windows version) - $embed_c = <<<'C_CODE' -#include - -int main(int argc, char **argv) { - PHP_EMBED_START_BLOCK(argc, argv) - - zend_file_handle file_handle; - zend_stream_init_filename(&file_handle, "embed.php"); - - if (!php_execute_script(&file_handle)) { - php_printf("Failed to execute PHP script.\n"); - } - - PHP_EMBED_END_BLOCK() - return 0; -} -C_CODE; - FileSystem::writeFile($test_dir . '\embed.c', $embed_c); - - // Create embed.php test file - FileSystem::writeFile($test_dir . '\embed.php', "config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); - - // Build the embed test executable using cl.exe - // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall - InteractiveTerm::setMessage('Running php-embed build smoke test'); - - // For Windows, we need to use PHP source directory headers directly - // because Windows PHP doesn't use php_config.h like Unix - $source_dir = $package->getSourceDir(); - $rel_type = 'Release'; - $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; - $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; - - // Build include flags pointing to source dirs (like PHP Windows build does) - // Note: embed.c uses #include , so we need $source_dir itself - $include_flags = sprintf( - '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . - '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', - $build_dir, - $source_dir, - $source_dir, - $source_dir, - $source_dir - ); - - // MSVC cl.exe format: compiler flags must come before /link, linker flags after - // ldflags contains /LIBPATH which must be after /link - $compile_cmd = sprintf( - 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', - $include_flags, - BUILD_ROOT_PATH, - $config['libs'], - 'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS) - ); - - // Log command explicitly (workaround for cmd() not logging complex commands properly) - logger()->debug('Embed smoke test compile command: ' . $compile_cmd); - - [$ret, $out] = cmd()->cd($test_dir)->execWithResult($compile_cmd); - if ($ret !== 0) { - throw new ValidationException( - 'embed failed to build. Error message: ' . implode("\n", $out), - validation_module: 'php-embed build smoke test' - ); - } - - // Run the embed test - InteractiveTerm::setMessage('Running php-embed run smoke test'); - [$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe'); - $raw_output = implode('', $output); - if ($ret !== 0 || trim($raw_output) !== 'hello') { - throw new ValidationException( - 'embed failed to run. Error message: ' . $raw_output, - validation_module: 'php-embed run smoke test' - ); - } - } - - 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_file = "{$src[0]}\\{$src[1]}"; - $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); - - $builder->deployBinary($src_file, $dst_file); - - // copy .pdb debug info file - if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { - FileSystem::createDir($debug_dir); - FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); - } - - // with-upx-pack for cli, cgi and micro - if ($builder->getOption('with-upx-pack', false)) { - if (in_array($sapi, ['php-cli', 'php-cgi', 'php-micro'], true)) { - cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst_file)); - } - } - } -} +getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); + } + + #[Stage] + public function buildconfForWindows(TargetPackage $package): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); + V2CompatLayer::emitPatchPoint('before-php-buildconf'); + cmd()->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'); + $embed = $installer->isPackageResolved('php-embed'); + $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; + $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; + $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; + $args[] = $embed ? '--enable-embed=yes' : '--enable-embed=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, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); + + // Collect static-libs@windows from all resolved library packages. + // PHP's configure.bat only adds libs declared by enabled extensions via config.w32; + // transitive library-only deps (e.g. zlibstatic.lib needed by libcrypto.lib) are + // not covered. Inject them here so the final link step has all required symbols. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_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'); + } + + #[BeforeStage('php', [self::class, 'makeCgiForWindows'])] + #[PatchDescription('Patch Windows Makefile for CGI target')] + public function patchCGITarget(TargetPackage $package): void + { + // search Makefile code line contains "$(BUILD_DIR)\php-cgi.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-cgi.exe:')) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); + } + $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; + $lines[$line_num + 1] = "\t" . '@"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); + + // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() + FileSystem::replaceFileRegex("{$package->getSourceDir()}\\sapi\\cgi\\cgi_main.c", '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); + } + + #[Stage] + public function makeCgiForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php-cgi.exe')); + + // Collect static-libs@windows from all resolved library packages. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; + } + } + + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe"); + + $this->deployWindowsBinary($builder, $package, 'php-cgi'); + } + + #[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']); + } + if ($installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'makeEmbedForWindows']); + } + } + + #[Stage] + public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('micro.sfx')); + + // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) + $makefile = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); + if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; + } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n"; + } + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", $makefile); + + // Collect static-libs@windows from all resolved library packages. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; + } + } + + $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; + + // phar patch for micro + $phar_patched = false; + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + + try { + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } + + $this->deployWindowsBinary($builder, $package, 'php-micro'); + } + + #[BeforeStage('php', [self::class, 'makeEmbedForWindows'])] + #[PatchDescription('Patch Windows Makefile for embed static library target')] + public function patchEmbedTarget(TargetPackage $package): void + { + $makefile_path = "{$package->getSourceDir()}\\Makefile"; + $content = FileSystem::readFile($makefile_path); + + // PHP's configure.bat generates PHP_LDFLAGS with /nodefaultlib:libcmt to avoid CRT + // duplication in a normal /MD build. But our static build compiles everything with /MT, + // so every .obj file has DEFAULTLIB:LIBCMT embedded. Removing /nodefaultlib:libcmt lets + // the linker pick up libcmt.lib. We also exclude the dynamic CRT (/nodefaultlib:msvcrt + // /nodefaultlib:msvcrtd) to keep the DLL dependency-free, consistent with CLI/CGI/micro. + $content = str_replace( + 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:libcmt /def:$(PHPDEF)', + 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /def:$(PHPDEF) /ltcg /ignore:4286', + $content + ); + + // Patch embed lib target to build a REAL static library instead of just an import lib. + // The default embed target only includes embed SAPI objects and links against php8.lib (import lib). + // We need to include PHP core objects (PHP_GLOBAL_OBJS) and static extension objects (STATIC_EXT_OBJS) + // to create a self-contained static library that doesn't require php8.dll at runtime. + $major = intdiv($this->getPHPVersionID(), 10000); + $embed_lib = "php{$major}embed.lib"; + + // Find and replace the embed lib build rule + // Actual Makefile format (note the backslash before $(PHPLIB)): + // $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + // @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + $lines = explode("\r\n", $content); + $new_lines = []; + $i = 0; + while ($i < count($lines)) { + $line = $lines[$i]; + // Check if this is the embed lib target dependency line (contains the lib name and $(BUILD_DIR)\$(PHPLIB)) + if (str_contains($line, "\$(BUILD_DIR)\\{$embed_lib}:") && str_contains($line, '$(BUILD_DIR)\$(PHPLIB)')) { + // Replace the dependency line + // Original: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + // New: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + $new_deps = "\$(BUILD_DIR)\\{$embed_lib}: \$(DEPS_EMBED) \$(EMBED_GLOBAL_OBJS) \$(PHP_GLOBAL_OBJS) \$(STATIC_EXT_OBJS) \$(ASM_OBJS) \$(BUILD_DIR)\\{$embed_lib}.res \$(BUILD_DIR)\\{$embed_lib}.manifest"; + $new_lines[] = $new_deps; + // Skip the original line (we replaced it) + ++$i; + // Now look for the lib.exe command line (should be the next non-empty line starting with tab) + while ($i < count($lines) && trim($lines[$i]) === '') { + $new_lines[] = $lines[$i]; + ++$i; + } + // Replace the lib.exe command to include PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP + // Original: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + // New: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + if ($i < count($lines) && str_contains($lines[$i], '$(MAKE_LIB)')) { + $cmd_line = $lines[$i]; + // Remove $(BUILD_DIR)\$(PHPLIB) from the command (note the backslash) + $cmd_line = str_replace(' $(BUILD_DIR)\$(PHPLIB)', '', $cmd_line); + // Add PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP after EMBED_GLOBAL_OBJS_RESP + $cmd_line = str_replace( + '$(EMBED_GLOBAL_OBJS_RESP)', + '$(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS)', + $cmd_line + ); + $new_lines[] = $cmd_line; + ++$i; + } + } else { + $new_lines[] = $line; + ++$i; + } + } + $content = implode("\r\n", $new_lines); + + FileSystem::writeFile($makefile_path, $content); + } + + #[Stage] + public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + $major = intdiv($this->getPHPVersionID(), 10000); + $embed_lib = "php{$major}embed.lib"; + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow($embed_lib)); + + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" '; + } + } + + // Build the embed static library (patched to include PHP core and extension objects) + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}{$embed_lib}"); + + // Deploy: php8embed.lib is now a REAL static library containing all PHP code + $rel_type = 'Release'; // TODO: Debug build support + $ts = $builder->getOption('enable-zts') ? '_TS' : ''; + $build_dir = "{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}"; + + // copy static embed lib to buildroot/lib + $embed_lib_src = "{$build_dir}\\{$embed_lib}"; + if (file_exists($embed_lib_src)) { + FileSystem::copy($embed_lib_src, "{$package->getLibDir()}\\{$embed_lib}"); + $package->setOutput('Static library path for embed SAPI', "{$package->getLibDir()}\\{$embed_lib}"); + } + + // Note: We no longer deploy php8.dll because the embed static library is self-contained. + // All PHP core code, extensions, and embed SAPI are statically linked into php8embed.lib. + + // copy .pdb debug info if --no-strip + $debug_dir = BUILD_ROOT_PATH . '\debug'; + if ($builder->getOption('no-strip', false)) { + $pdb = "{$build_dir}\\php{$major}embed.pdb"; + if (file_exists($pdb)) { + FileSystem::createDir($debug_dir); + FileSystem::copy($pdb, "{$debug_dir}\\php{$major}embed.pdb"); + } + } + + // Install PHP headers for embed SAPI development + $this->installPhpHeadersForWindows($package, $installer); + } + + #[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(); + 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", + '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()}\\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"); + } + } + } + + #[Stage] + public function smokeTestForWindows(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + $option = match ($no_smoke_test) { + false => false, + null => 'all', + default => parse_comma_list($no_smoke_test), + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + + // remove all .dll from buildroot/bin/ + $dlls = glob(BUILD_BIN_PATH . '\*.dll') ?: []; + foreach ($dlls as $dll) { + @unlink($dll); + } + + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForWindows']); + } + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForWindows']); + } + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForWindows'], ['skipExtTest' => $skipExtTest]); + } + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForWindows'], ['installer' => $installer]); + } + } + + #[Stage] + public function smokeTestCliForWindows(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliWindows(); + } + } + + #[Stage] + public function smokeTestCgiForWindows(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); + $raw_output = implode("\n", $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForWindows(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '\micro.sfx'; + + InteractiveTerm::setMessage('Running php-micro smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '\micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + [$ret, $out] = cmd()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPackage $package): void + { + $test_dir = SOURCE_PATH . '\embed-test'; + FileSystem::createDir($test_dir); + + // Create embed.c test file (Windows version) + $embed_c = <<<'C_CODE' +#include + +int main(int argc, char **argv) { + PHP_EMBED_START_BLOCK(argc, argv) + + zend_file_handle file_handle; + zend_stream_init_filename(&file_handle, "embed.php"); + + if (!php_execute_script(&file_handle)) { + php_printf("Failed to execute PHP script.\n"); + } + + PHP_EMBED_END_BLOCK() + return 0; +} +C_CODE; + FileSystem::writeFile($test_dir . '\embed.c', $embed_c); + + // Create embed.php test file + FileSystem::writeFile($test_dir . '\embed.php', "config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + + // Build the embed test executable using cl.exe + // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall + InteractiveTerm::setMessage('Running php-embed build smoke test'); + + // For Windows, we need to use PHP source directory headers directly + // because Windows PHP doesn't use php_config.h like Unix + $source_dir = $package->getSourceDir(); + $rel_type = 'Release'; + $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; + $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; + + // Build include flags pointing to source dirs (like PHP Windows build does) + // Note: embed.c uses #include , so we need $source_dir itself + $include_flags = sprintf( + '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . + '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', + $build_dir, + $source_dir, + $source_dir, + $source_dir, + $source_dir + ); + + // MSVC cl.exe format: compiler flags must come before /link, linker flags after + // ldflags contains /LIBPATH which must be after /link + $compile_cmd = sprintf( + 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', + $include_flags, + BUILD_ROOT_PATH, + $config['libs'], + 'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS) + ); + + // Log command explicitly (workaround for cmd() not logging complex commands properly) + logger()->debug('Embed smoke test compile command: ' . $compile_cmd); + + [$ret, $out] = cmd()->cd($test_dir)->execWithResult($compile_cmd); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: 'php-embed build smoke test' + ); + } + + // Run the embed test + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . $raw_output, + validation_module: 'php-embed run smoke test' + ); + } + } + + 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_file = "{$src[0]}\\{$src[1]}"; + $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); + + $builder->deployBinary($src_file, $dst_file); + + // copy .pdb debug info file + if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { + FileSystem::createDir($debug_dir); + FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); + } + } + + /** + * Install PHP headers to buildroot/include for embed SAPI development. + * This mirrors the 'make install-headers' behavior on Unix. + */ + private function installPhpHeadersForWindows(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Installing PHP headers for embed SAPI'); + + $source_dir = $package->getSourceDir(); + $include_dir = $package->getIncludeDir(); + $php_include_dir = "{$include_dir}\\php"; + + // Create directory structure + FileSystem::createDir("{$php_include_dir}\\main"); + FileSystem::createDir("{$php_include_dir}\\Zend"); + FileSystem::createDir("{$php_include_dir}\\TSRM"); + FileSystem::createDir("{$php_include_dir}\\sapi\\embed"); + + // Copy main/*.h + foreach (glob("{$source_dir}\\main\\*.h") as $h) { + FileSystem::copy($h, "{$php_include_dir}\\main\\" . basename($h)); + } + + // Copy Zend/*.h + foreach (glob("{$source_dir}\\Zend\\*.h") as $h) { + $target = "{$php_include_dir}\\Zend\\" . basename($h); + FileSystem::copy($h, $target); + // Fix GCC-specific #warning directive not supported by MSVC + if (basename($h) === 'zend_atomic.h') { + FileSystem::replaceFileStr($target, '#warning No atomics support detected. Please open an issue with platform details.', '#pragma message("No atomics support detected. Please open an issue with platform details.")'); + } + } + + // Copy TSRM/*.h + foreach (glob("{$source_dir}\\TSRM\\*.h") as $h) { + FileSystem::copy($h, "{$php_include_dir}\\TSRM\\" . basename($h)); + } + + // Copy embed SAPI header + FileSystem::copy("{$source_dir}\\sapi\\embed\\php_embed.h", "{$php_include_dir}\\sapi\\embed\\php_embed.h"); + + // Copy generated config.h (config.w32.h on Windows) to php_config.h + $rel_type = 'Release'; + $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; + $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; + + // Always copy config.w32.h from source (it's used for both build and headers) + if (file_exists("{$source_dir}\\main\\config.w32.h")) { + FileSystem::copy("{$source_dir}\\main\\config.w32.h", "{$php_include_dir}\\main\\php_config.h"); + } + + // Windows: zend_config.w32.h must be copied as zend_config.h for Zend headers to work + if (file_exists("{$source_dir}\\Zend\\zend_config.w32.h")) { + FileSystem::copy("{$source_dir}\\Zend\\zend_config.w32.h", "{$php_include_dir}\\Zend\\zend_config.h"); + } + + // Copy extension headers for enabled extensions + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + $ext_name = $ext->getExtensionName(); + $ext_dir = "{$source_dir}\\ext\\{$ext_name}"; + if (is_dir($ext_dir)) { + $target_ext_dir = "{$php_include_dir}\\ext\\{$ext_name}"; + FileSystem::createDir($target_ext_dir); + foreach (glob("{$ext_dir}\\*.h") as $h) { + FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); + } + // Also copy any arginfo headers + foreach (glob("{$ext_dir}\\*_arginfo.h") as $h) { + if (!file_exists("{$target_ext_dir}\\" . basename($h))) { + FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); + } + } + } + } + + $package->setOutput('PHP headers path for embed SAPI', $php_include_dir); + } +} diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index e50798adc..5138582fb 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -145,7 +145,7 @@ public function deployBinary(string $src, string $dst, bool $executable = true): 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)); + cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst)); } return $dst; From c671cfd13b0f9e5dc78a7bfdcda98ae5be7ade1d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 16:33:57 +0800 Subject: [PATCH 540/682] Add cli, cgi, micro output --- src/Package/Target/php/windows.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index bcb4a7213..586b702b0 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -689,6 +689,13 @@ protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $p $builder->deployBinary($src_file, $dst_file); + $output_label = match ($sapi) { + 'php-cli' => 'Binary path for cli SAPI', + 'php-cgi' => 'Binary path for cgi SAPI', + 'php-micro' => 'Binary path for micro SAPI', + }; + $package->setOutput($output_label, $dst_file); + // copy .pdb debug info file if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { FileSystem::createDir($debug_dir); From e5e6e26f676aa591530cca991e856ad66b3d155f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 3 Apr 2026 17:42:30 +0800 Subject: [PATCH 541/682] Add cli, cgi, micro output --- src/Package/Target/php/windows.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 586b702b0..e81de3b65 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -693,8 +693,11 @@ protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $p 'php-cli' => 'Binary path for cli SAPI', 'php-cgi' => 'Binary path for cgi SAPI', 'php-micro' => 'Binary path for micro SAPI', + default => null, }; - $package->setOutput($output_label, $dst_file); + if ($output_label) { + $package->setOutput($output_label, $dst_file); + } // copy .pdb debug info file if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { From cd3eb3d41d2a0f342bc4f0bbbb9bed00fcedab9a Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 3 Apr 2026 19:53:23 +0800 Subject: [PATCH 542/682] Update src/globals/ext-tests/openssl.php --- src/globals/ext-tests/openssl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/ext-tests/openssl.php b/src/globals/ext-tests/openssl.php index 34228988f..117231604 100644 --- a/src/globals/ext-tests/openssl.php +++ b/src/globals/ext-tests/openssl.php @@ -31,6 +31,6 @@ } assert($valid); } -if (PHP_VERSION_ID >= 80500 && !PHP_ZTS && defined('OPENSSL_VERSION_NUMBER') && OPENSSL_VERSION_NUMBER >= 0x30200000) { +if (PHP_VERSION_ID >= 80500 && (!PHP_ZTS || PHP_OS_FAMILY !== 'Windows') && defined('OPENSSL_VERSION_NUMBER') && OPENSSL_VERSION_NUMBER >= 0x30200000) { assert(function_exists('openssl_password_hash')); } From 0b2b1d51e12f7a590281c2f0b08dba51ba2a6a71 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 4 Apr 2026 18:39:28 +0800 Subject: [PATCH 543/682] Fix file paths for SQLSRV --- src/SPC/builder/extension/sqlsrv.php | 2 +- src/globals/test-extensions.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SPC/builder/extension/sqlsrv.php b/src/SPC/builder/extension/sqlsrv.php index 3f47134b7..fa55b9324 100644 --- a/src/SPC/builder/extension/sqlsrv.php +++ b/src/SPC/builder/extension/sqlsrv.php @@ -36,7 +36,7 @@ public function patchBeforeWindowsConfigure(): bool public function patchBeforeMake(): bool { - $makefile = SOURCE_PATH . '\php-src\Makefile'; + $makefile = SOURCE_PATH . '/php-src/Makefile'; $makeContent = file_get_contents($makefile); $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/W4\b/m', '$1', $makeContent); $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/WX\b/m', '$1', $makeContent); diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 34bc74436..9f7b90b6d 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -27,11 +27,11 @@ // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', + // 'windows-2025', ]; // whether enable thread safe @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'zlib', + 'Linux', 'Darwin' => 'sqlsrv,pdo_sqlsrv', 'Windows' => 'amqp,apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,ds,exif,ffi,fileinfo,filter,ftp,gd,iconv,igbinary,libxml,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pdo,pdo_mysql,pdo_sqlite,pdo_sqlsrv,phar,rar,redis,session,shmop,simdjson,simplexml,soap,sockets,sqlite3,sqlsrv,ssh2,sysvshm,tokenizer,xml,xmlreader,xmlwriter,yac,yaml,zip,zlib', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'libjpeg', + 'Linux', 'Darwin' => '', 'Windows' => '', }; From ddb9e3e7e4a7d736d8d3c6842d9b538140c69b40 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 4 Apr 2026 18:18:22 +0700 Subject: [PATCH 544/682] add framework coreservices to watcher library --- config/lib.json | 3 +++ src/globals/test-extensions.php | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/config/lib.json b/config/lib.json index 33633ad09..4792a9329 100644 --- a/config/lib.json +++ b/config/lib.json @@ -998,6 +998,9 @@ ], "headers": [ "wtr/watcher-c.h" + ], + "frameworks": [ + "CoreServices" ] }, "xz": { diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 9f7b90b6d..7ae79aca2 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -16,18 +16,18 @@ // '8.1', // '8.2', // '8.3', - '8.4', + // '8.4', '8.5', // 'git', ]; // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - // 'macos-15-intel', // bin/spc for x86_64 - // 'macos-15', // bin/spc for arm64 + 'macos-15-intel', // bin/spc for x86_64 + 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - 'ubuntu-24.04', // bin/spc for x86_64 + // 'ubuntu-24.04', // bin/spc for x86_64 // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 @@ -43,7 +43,7 @@ $upx = false; // whether to test frankenphp build, only available for macOS and linux -$frankenphp = false; +$frankenphp = true; // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; From 991da260baded89901a865a22a5144af31347e09 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Apr 2026 13:06:25 +0800 Subject: [PATCH 545/682] Enhance CMake configuration with dynamic linker flags for target packages --- .../Runtime/Executor/UnixCMakeExecutor.php | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 9442d30c2..2269c3231 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -10,10 +10,13 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\TargetPackage; use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\PkgConfigUtil; +use StaticPHP\Util\SPCConfigUtil; use ZM\Logger\ConsoleColor; /** @@ -214,7 +217,7 @@ public function getConfigureArgsString(): string */ private function getDefaultCMakeArgs(): array { - return $this->custom_default_args ?? [ + $args = $this->custom_default_args ?? [ '-DCMAKE_BUILD_TYPE=Release', "-DCMAKE_INSTALL_PREFIX={$this->package->getBuildRootPath()}", '-DCMAKE_INSTALL_BINDIR=bin', @@ -224,6 +227,20 @@ private function getDefaultCMakeArgs(): array '-DBUILD_SHARED_LIBS=OFF', "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", ]; + + // EXE linker flags: base system libs + framework flags for target packages + $exeLinkerFlags = SystemTarget::getRuntimeLibs(); + if ($this->package instanceof TargetPackage) { + $resolvedNames = array_keys($this->installer->getResolvedPackages()); + $resolvedNames[] = $this->package->getName(); + $fwFlags = SPCConfigUtil::getFrameworksString($resolvedNames); + if ($fwFlags !== '') { + $exeLinkerFlags .= " {$fwFlags}"; + } + } + $args[] = "-DCMAKE_EXE_LINKER_FLAGS=\"{$exeLinkerFlags}\""; + + return $args; } /** @@ -274,13 +291,13 @@ private function makeCmakeToolchainFile(): string SET(CMAKE_INSTALL_PREFIX "{$root}") SET(CMAKE_INSTALL_LIBDIR "lib") -set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}") +set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}" CACHE FILEPATH "pkg-config executable" FORCE) set(PKG_CONFIG_ARGN "--static" CACHE STRING "Extra arguments for pkg-config" FORCE) +set(ENV{PKG_CONFIG_PATH} "{$root}/lib/pkgconfig") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) -set(CMAKE_EXE_LINKER_FLAGS "-ldl -lpthread -lm -lutil") CMAKE; // Whoops, linux may need CMAKE_AR sometimes if (PHP_OS_FAMILY === 'Linux') { From f8d24e2b3a87eb4b1d538eb08f69ea84ebea9850 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Apr 2026 13:13:45 +0800 Subject: [PATCH 546/682] Refactor package resolution to filter only available build artifacts --- src/Package/Extension/curl.php | 52 +- src/Package/Library/nghttp2.php | 1 - src/Package/Target/php/unix.php | 4 +- src/Package/Target/php/windows.php | 1566 ++++++++++---------- src/StaticPHP/Package/PackageInstaller.php | 26 + src/StaticPHP/Runtime/Shell/WindowsCmd.php | 128 +- src/StaticPHP/Util/SPCConfigUtil.php | 1018 ++++++------- 7 files changed, 1410 insertions(+), 1385 deletions(-) diff --git a/src/Package/Extension/curl.php b/src/Package/Extension/curl.php index f5c05b570..eef43a566 100644 --- a/src/Package/Extension/curl.php +++ b/src/Package/Extension/curl.php @@ -1,26 +1,26 @@ -build(); FileSystem::replaceFileStr($lib->getIncludeDir() . '\nghttp2\nghttp2.h', '#ifdef NGHTTP2_STATICLIB', '#if 1'); - } #[BuildFor('Linux')] diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 78eca80d2..2b81402df 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -577,7 +577,7 @@ public function smokeTestEmbedForUnix(PackageInstaller $installer, ToolchainInte copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); - $config = new SPCConfigUtil()->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $config = new SPCConfigUtil()->config($installer->getAvailableResolvedPackageNames()); $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; if ($toolchain->isStatic()) { $lens .= ' -static'; @@ -735,7 +735,7 @@ private function processLibphpSoFile(string $libphpSo, PackageInstaller $install */ private function makeVars(PackageInstaller $installer): array { - $config = new SPCConfigUtil(['libs_only_deps' => true])->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $config = new SPCConfigUtil(['libs_only_deps' => true])->config($installer->getAvailableResolvedPackageNames()); $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 1188a0e49..4d90a59fd 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -1,783 +1,783 @@ -getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); - } - - #[Stage] - public function buildconfForWindows(TargetPackage $package): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); - V2CompatLayer::emitPatchPoint('before-php-buildconf'); - cmd()->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'); - $embed = $installer->isPackageResolved('php-embed'); - $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; - $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; - $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; - $args[] = $embed ? '--enable-embed=yes' : '--enable-embed=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, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); - - // Collect static-libs@windows from all resolved library packages. - // PHP's configure.bat only adds libs declared by enabled extensions via config.w32; - // transitive library-only deps (e.g. zlibstatic.lib needed by libcrypto.lib) are - // not covered. Inject them here so the final link step has all required symbols. - $resolved_libs = []; - foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { - foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { - if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { - $resolved_libs[] = $lib_file; - } - } - } - $resolved_libs = array_unique($resolved_libs); - - // extra lib - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_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'); - } - - #[BeforeStage('php', [self::class, 'makeCgiForWindows'])] - #[PatchDescription('Patch Windows Makefile for CGI target')] - public function patchCGITarget(TargetPackage $package): void - { - // search Makefile code line contains "$(BUILD_DIR)\php-cgi.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-cgi.exe:')) { - $found = $line_num; - break; - } - ++$line_num; - } - if ($found === false) { - throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); - } - $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; - $lines[$line_num + 1] = "\t" . '@"$(LINK)” /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; - FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); - - // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() - FileSystem::replaceFileRegex("{$package->getSourceDir()}\\sapi\\cgi\\cgi_main.c", '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); - } - - #[Stage] - public function makeCgiForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php-cgi.exe')); - - // Collect static-libs@windows from all resolved library packages. - $resolved_libs = []; - foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { - foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { - if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { - $resolved_libs[] = $lib_file; - } - } - } - $resolved_libs = array_unique($resolved_libs); - - // extra lib - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; - } - } - - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe"); - - $this->deployWindowsBinary($builder, $package, 'php-cgi'); - } - - #[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']); - } - if ($installer->isPackageResolved('php-embed')) { - $package->runStage([$this, 'makeEmbedForWindows']); - } - } - - #[Stage] - public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('micro.sfx')); - - // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) - $makefile = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); - if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { - $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; - } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { - $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n"; - } - FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", $makefile); - - // Collect static-libs@windows from all resolved library packages. - $resolved_libs = []; - foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { - foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { - if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { - $resolved_libs[] = $lib_file; - } - } - } - $resolved_libs = array_unique($resolved_libs); - - // extra lib - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; - } - } - - $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; - - // phar patch for micro - $phar_patched = false; - if ($installer->isPackageResolved('ext-phar')) { - $phar_patched = true; - SourcePatcher::patchMicroPhar(self::getPHPVersionID()); - } - - try { - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); - } finally { - if ($phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - - $this->deployWindowsBinary($builder, $package, 'php-micro'); - } - - #[BeforeStage('php', [self::class, 'makeEmbedForWindows'])] - #[PatchDescription('Patch Windows Makefile for embed static library target')] - public function patchEmbedTarget(TargetPackage $package): void - { - $makefile_path = "{$package->getSourceDir()}\\Makefile"; - $content = FileSystem::readFile($makefile_path); - - // PHP's configure.bat generates PHP_LDFLAGS with /nodefaultlib:libcmt to avoid CRT - // duplication in a normal /MD build. But our static build compiles everything with /MT, - // so every .obj file has DEFAULTLIB:LIBCMT embedded. Removing /nodefaultlib:libcmt lets - // the linker pick up libcmt.lib. We also exclude the dynamic CRT (/nodefaultlib:msvcrt - // /nodefaultlib:msvcrtd) to keep the DLL dependency-free, consistent with CLI/CGI/micro. - $content = str_replace( - 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:libcmt /def:$(PHPDEF)', - 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /def:$(PHPDEF) /ltcg /ignore:4286', - $content - ); - - // Patch embed lib target to build a REAL static library instead of just an import lib. - // The default embed target only includes embed SAPI objects and links against php8.lib (import lib). - // We need to include PHP core objects (PHP_GLOBAL_OBJS) and static extension objects (STATIC_EXT_OBJS) - // to create a self-contained static library that doesn't require php8.dll at runtime. - $major = intdiv($this->getPHPVersionID(), 10000); - $embed_lib = "php{$major}embed.lib"; - - // Find and replace the embed lib build rule - // Actual Makefile format (note the backslash before $(PHPLIB)): - // $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest - // @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res - $lines = explode("\r\n", $content); - $new_lines = []; - $i = 0; - while ($i < count($lines)) { - $line = $lines[$i]; - // Check if this is the embed lib target dependency line (contains the lib name and $(BUILD_DIR)\$(PHPLIB)) - if (str_contains($line, "\$(BUILD_DIR)\\{$embed_lib}:") && str_contains($line, '$(BUILD_DIR)\\$(PHPLIB)')) { - // Replace the dependency line - // Original: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest - // New: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest - $new_deps = "\$(BUILD_DIR)\\{$embed_lib}: \$(DEPS_EMBED) \$(EMBED_GLOBAL_OBJS) \$(PHP_GLOBAL_OBJS) \$(STATIC_EXT_OBJS) \$(ASM_OBJS) \$(BUILD_DIR)\\{$embed_lib}.res \$(BUILD_DIR)\\{$embed_lib}.manifest"; - $new_lines[] = $new_deps; - // Skip the original line (we replaced it) - ++$i; - // Now look for the lib.exe command line (should be the next non-empty line starting with tab) - while ($i < count($lines) && trim($lines[$i]) === '') { - $new_lines[] = $lines[$i]; - ++$i; - } - // Replace the lib.exe command to include PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP - // Original: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res - // New: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res - if ($i < count($lines) && str_contains($lines[$i], '$(MAKE_LIB)')) { - $cmd_line = $lines[$i]; - // Remove $(BUILD_DIR)\$(PHPLIB) from the command (note the backslash) - $cmd_line = str_replace(' $(BUILD_DIR)\\$(PHPLIB)', '', $cmd_line); - // Add PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP after EMBED_GLOBAL_OBJS_RESP - $cmd_line = str_replace( - '$(EMBED_GLOBAL_OBJS_RESP)', - '$(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS)', - $cmd_line - ); - $new_lines[] = $cmd_line; - ++$i; - } - } else { - $new_lines[] = $line; - ++$i; - } - } - $content = implode("\r\n", $new_lines); - - FileSystem::writeFile($makefile_path, $content); - } - - #[Stage] - public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void - { - $major = intdiv($this->getPHPVersionID(), 10000); - $embed_lib = "php{$major}embed.lib"; - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow($embed_lib)); - - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" '; - } - } - - // Build the embed static library (patched to include PHP core and extension objects) - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}{$embed_lib}"); - - // Deploy: php8embed.lib is now a REAL static library containing all PHP code - $rel_type = 'Release'; // TODO: Debug build support - $ts = $builder->getOption('enable-zts') ? '_TS' : ''; - $build_dir = "{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}"; - - // copy static embed lib to buildroot/lib - $embed_lib_src = "{$build_dir}\\{$embed_lib}"; - if (file_exists($embed_lib_src)) { - FileSystem::copy($embed_lib_src, "{$package->getLibDir()}\\{$embed_lib}"); - $package->setOutput('Static library path for embed SAPI', "{$package->getLibDir()}\\{$embed_lib}"); - } - - // Note: We no longer deploy php8.dll because the embed static library is self-contained. - // All PHP core code, extensions, and embed SAPI are statically linked into php8embed.lib. - - // copy .pdb debug info if --no-strip - $debug_dir = BUILD_ROOT_PATH . '\debug'; - if ($builder->getOption('no-strip', false)) { - $pdb = "{$build_dir}\\php{$major}embed.pdb"; - if (file_exists($pdb)) { - FileSystem::createDir($debug_dir); - FileSystem::copy($pdb, "{$debug_dir}\\php{$major}embed.pdb"); - } - } - - // Install PHP headers for embed SAPI development - $this->installPhpHeadersForWindows($package, $installer); - } - - /** - * Install PHP headers to buildroot/include for embed SAPI development. - * This mirrors the 'make install-headers' behavior on Unix. - */ - private function installPhpHeadersForWindows(TargetPackage $package, PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Installing PHP headers for embed SAPI'); - - $source_dir = $package->getSourceDir(); - $include_dir = $package->getIncludeDir(); - $php_include_dir = "{$include_dir}\\php"; - - // Create directory structure - FileSystem::createDir("{$php_include_dir}\\main"); - FileSystem::createDir("{$php_include_dir}\\Zend"); - FileSystem::createDir("{$php_include_dir}\\TSRM"); - FileSystem::createDir("{$php_include_dir}\\sapi\\embed"); - - // Copy main/*.h - foreach (glob("{$source_dir}\\main\\*.h") as $h) { - FileSystem::copy($h, "{$php_include_dir}\\main\\" . basename($h)); - } - - // Copy Zend/*.h - foreach (glob("{$source_dir}\\Zend\\*.h") as $h) { - $target = "{$php_include_dir}\\Zend\\" . basename($h); - FileSystem::copy($h, $target); - // Fix GCC-specific #warning directive not supported by MSVC - if (basename($h) === 'zend_atomic.h') { - FileSystem::replaceFileStr($target, '#warning No atomics support detected. Please open an issue with platform details.', '#pragma message("No atomics support detected. Please open an issue with platform details.")'); - } - } - - // Copy TSRM/*.h - foreach (glob("{$source_dir}\\TSRM\\*.h") as $h) { - FileSystem::copy($h, "{$php_include_dir}\\TSRM\\" . basename($h)); - } - - // Copy embed SAPI header - FileSystem::copy("{$source_dir}\\sapi\\embed\\php_embed.h", "{$php_include_dir}\\sapi\\embed\\php_embed.h"); - - // Copy generated config.h (config.w32.h on Windows) to php_config.h - $rel_type = 'Release'; - $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; - $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; - - // Always copy config.w32.h from source (it's used for both build and headers) - if (file_exists("{$source_dir}\\main\\config.w32.h")) { - FileSystem::copy("{$source_dir}\\main\\config.w32.h", "{$php_include_dir}\\main\\php_config.h"); - } - - // Windows: zend_config.w32.h must be copied as zend_config.h for Zend headers to work - if (file_exists("{$source_dir}\\Zend\\zend_config.w32.h")) { - FileSystem::copy("{$source_dir}\\Zend\\zend_config.w32.h", "{$php_include_dir}\\Zend\\zend_config.h"); - } - - // Copy extension headers for enabled extensions - foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { - $ext_name = $ext->getExtensionName(); - $ext_dir = "{$source_dir}\\ext\\{$ext_name}"; - if (is_dir($ext_dir)) { - $target_ext_dir = "{$php_include_dir}\\ext\\{$ext_name}"; - FileSystem::createDir($target_ext_dir); - foreach (glob("{$ext_dir}\\*.h") as $h) { - FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); - } - // Also copy any arginfo headers - foreach (glob("{$ext_dir}\\*_arginfo.h") as $h) { - if (!file_exists("{$target_ext_dir}\\" . basename($h))) { - FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); - } - } - } - } - - $package->setOutput('PHP headers path for embed SAPI', $php_include_dir); - } - - #[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(); - 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", - '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()}\\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"); - } - } - } - - #[Stage] - public function smokeTestForWindows(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void - { - // analyse --no-smoke-test option - $no_smoke_test = $builder->getOption('no-smoke-test'); - $option = match ($no_smoke_test) { - false => false, - null => 'all', - default => parse_comma_list($no_smoke_test), - }; - $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed']; - // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts - if ($builder->getOption('without-micro-ext-test', false)) { - $valid_tests = array_diff($valid_tests, ['micro-exts']); - } - if (is_array($option)) { - foreach ($option as $test) { - if (!in_array($test, $valid_tests, true)) { - throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); - } - $valid_tests = array_diff($valid_tests, [$test]); - } - } elseif ($option === 'all') { - $valid_tests = []; - } - - // remove all .dll from buildroot/bin/ - $dlls = glob(BUILD_BIN_PATH . '\*.dll') ?: []; - foreach ($dlls as $dll) { - @unlink($dll); - } - - if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { - $package->runStage([$this, 'smokeTestCliForWindows']); - } - if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { - $package->runStage([$this, 'smokeTestCgiForWindows']); - } - if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { - $skipExtTest = !in_array('micro-exts', $valid_tests, true); - $package->runStage([$this, 'smokeTestMicroForWindows'], ['skipExtTest' => $skipExtTest]); - } - if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { - $package->runStage([$this, 'smokeTestEmbedForWindows'], ['installer' => $installer]); - } - } - - #[Stage] - public function smokeTestCliForWindows(PackageInstaller $installer): void - { - InteractiveTerm::setMessage('Running basic php-cli smoke test'); - [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); - $raw_output = implode('', $output); - if ($ret !== 0 || trim($raw_output) !== 'hello') { - throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); - } - - $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); - foreach ($exts as $ext) { - InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); - $ext->runSmokeTestCliWindows(); - } - } - - #[Stage] - public function smokeTestCgiForWindows(): void - { - InteractiveTerm::setMessage('Running basic php-cgi smoke test'); - FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); - [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); - $raw_output = implode("\n", $output); - if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { - throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); - } - } - - #[Stage] - public function smokeTestMicroForWindows(PackageInstaller $installer, bool $skipExtTest = false): void - { - $micro_sfx = BUILD_BIN_PATH . '\micro.sfx'; - - InteractiveTerm::setMessage('Running php-micro smoke test'); - $content = $skipExtTest - ? 'generateMicroExtTests($installer); - $test_file = SOURCE_PATH . '\micro_ext_test.exe'; - if (file_exists($test_file)) { - @unlink($test_file); - } - file_put_contents($test_file, file_get_contents($micro_sfx) . $content); - [$ret, $out] = cmd()->execWithResult($test_file); - $raw_out = trim(implode('', $out)); - if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { - throw new ValidationException( - "micro_ext_test failed. code: {$ret}, output: {$raw_out}", - validation_module: 'phpmicro sanity check item [micro_ext_test]' - ); - } - } - - #[Stage] - public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPackage $package): void - { - $test_dir = SOURCE_PATH . '\embed-test'; - FileSystem::createDir($test_dir); - - // Create embed.c test file (Windows version) - $embed_c = <<<'C_CODE' -#include - -int main(int argc, char **argv) { - PHP_EMBED_START_BLOCK(argc, argv) - - zend_file_handle file_handle; - zend_stream_init_filename(&file_handle, "embed.php"); - - if (!php_execute_script(&file_handle)) { - php_printf("Failed to execute PHP script.\n"); - } - - PHP_EMBED_END_BLOCK() - return 0; -} -C_CODE; - FileSystem::writeFile($test_dir . '\embed.c', $embed_c); - - // Create embed.php test file - FileSystem::writeFile($test_dir . '\embed.php', "config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); - - // Build the embed test executable using cl.exe - // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall - InteractiveTerm::setMessage('Running php-embed build smoke test'); - - // For Windows, we need to use PHP source directory headers directly - // because Windows PHP doesn't use php_config.h like Unix - $source_dir = $package->getSourceDir(); - $rel_type = 'Release'; - $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; - $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; - - // Build include flags pointing to source dirs (like PHP Windows build does) - // Note: embed.c uses #include , so we need $source_dir itself - $include_flags = sprintf( - '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . - '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', - $build_dir, - $source_dir, - $source_dir, - $source_dir, - $source_dir - ); - - // MSVC cl.exe format: compiler flags must come before /link, linker flags after - // ldflags contains /LIBPATH which must be after /link - $compile_cmd = sprintf( - 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', - $include_flags, - BUILD_ROOT_PATH, - $config['libs'], - 'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS) - ); - - // Log command explicitly (workaround for cmd() not logging complex commands properly) - logger()->debug('Embed smoke test compile command: ' . $compile_cmd); - - [$ret, $out] = cmd()->cd($test_dir)->execWithResult($compile_cmd); - if ($ret !== 0) { - throw new ValidationException( - 'embed failed to build. Error message: ' . implode("\n", $out), - validation_module: 'php-embed build smoke test' - ); - } - - // Run the embed test - InteractiveTerm::setMessage('Running php-embed run smoke test'); - [$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe'); - $raw_output = implode('', $output); - if ($ret !== 0 || trim($raw_output) !== 'hello') { - throw new ValidationException( - 'embed failed to run. Error message: ' . $raw_output, - validation_module: 'php-embed run smoke test' - ); - } - } - - 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_file = "{$src[0]}\\{$src[1]}"; - $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); - - $builder->deployBinary($src_file, $dst_file); - - // copy .pdb debug info file - if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { - FileSystem::createDir($debug_dir); - FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); - } - - // with-upx-pack for cli, cgi and micro - if ($builder->getOption('with-upx-pack', false)) { - if (in_array($sapi, ['php-cli', 'php-cgi', 'php-micro'], true)) { - cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst_file)); - } - } - } -} +getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); + } + + #[Stage] + public function buildconfForWindows(TargetPackage $package): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); + V2CompatLayer::emitPatchPoint('before-php-buildconf'); + cmd()->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'); + $embed = $installer->isPackageResolved('php-embed'); + $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; + $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; + $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; + $args[] = $embed ? '--enable-embed=yes' : '--enable-embed=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, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); + + // Collect static-libs@windows from all resolved library packages. + // PHP's configure.bat only adds libs declared by enabled extensions via config.w32; + // transitive library-only deps (e.g. zlibstatic.lib needed by libcrypto.lib) are + // not covered. Inject them here so the final link step has all required symbols. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_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'); + } + + #[BeforeStage('php', [self::class, 'makeCgiForWindows'])] + #[PatchDescription('Patch Windows Makefile for CGI target')] + public function patchCGITarget(TargetPackage $package): void + { + // search Makefile code line contains "$(BUILD_DIR)\php-cgi.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-cgi.exe:')) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); + } + $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; + $lines[$line_num + 1] = "\t" . '@"$(LINK)” /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); + + // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() + FileSystem::replaceFileRegex("{$package->getSourceDir()}\\sapi\\cgi\\cgi_main.c", '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); + } + + #[Stage] + public function makeCgiForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php-cgi.exe')); + + // Collect static-libs@windows from all resolved library packages. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; + } + } + + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe"); + + $this->deployWindowsBinary($builder, $package, 'php-cgi'); + } + + #[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']); + } + if ($installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'makeEmbedForWindows']); + } + } + + #[Stage] + public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('micro.sfx')); + + // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) + $makefile = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); + if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; + } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n"; + } + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", $makefile); + + // Collect static-libs@windows from all resolved library packages. + $resolved_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $resolved_libs[] = $lib_file; + } + } + } + $resolved_libs = array_unique($resolved_libs); + + // extra lib + $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; + } + } + + $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; + + // phar patch for micro + $phar_patched = false; + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + + try { + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } + + $this->deployWindowsBinary($builder, $package, 'php-micro'); + } + + #[BeforeStage('php', [self::class, 'makeEmbedForWindows'])] + #[PatchDescription('Patch Windows Makefile for embed static library target')] + public function patchEmbedTarget(TargetPackage $package): void + { + $makefile_path = "{$package->getSourceDir()}\\Makefile"; + $content = FileSystem::readFile($makefile_path); + + // PHP's configure.bat generates PHP_LDFLAGS with /nodefaultlib:libcmt to avoid CRT + // duplication in a normal /MD build. But our static build compiles everything with /MT, + // so every .obj file has DEFAULTLIB:LIBCMT embedded. Removing /nodefaultlib:libcmt lets + // the linker pick up libcmt.lib. We also exclude the dynamic CRT (/nodefaultlib:msvcrt + // /nodefaultlib:msvcrtd) to keep the DLL dependency-free, consistent with CLI/CGI/micro. + $content = str_replace( + 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:libcmt /def:$(PHPDEF)', + 'PHP_LDFLAGS=$(DLL_LDFLAGS) /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /def:$(PHPDEF) /ltcg /ignore:4286', + $content + ); + + // Patch embed lib target to build a REAL static library instead of just an import lib. + // The default embed target only includes embed SAPI objects and links against php8.lib (import lib). + // We need to include PHP core objects (PHP_GLOBAL_OBJS) and static extension objects (STATIC_EXT_OBJS) + // to create a self-contained static library that doesn't require php8.dll at runtime. + $major = intdiv($this->getPHPVersionID(), 10000); + $embed_lib = "php{$major}embed.lib"; + + // Find and replace the embed lib build rule + // Actual Makefile format (note the backslash before $(PHPLIB)): + // $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + // @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + $lines = explode("\r\n", $content); + $new_lines = []; + $i = 0; + while ($i < count($lines)) { + $line = $lines[$i]; + // Check if this is the embed lib target dependency line (contains the lib name and $(BUILD_DIR)\$(PHPLIB)) + if (str_contains($line, "\$(BUILD_DIR)\\{$embed_lib}:") && str_contains($line, '$(BUILD_DIR)\$(PHPLIB)')) { + // Replace the dependency line + // Original: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + // New: $(BUILD_DIR)\php8embed.lib: $(DEPS_EMBED) $(EMBED_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php8embed.lib.res $(BUILD_DIR)\php8embed.lib.manifest + $new_deps = "\$(BUILD_DIR)\\{$embed_lib}: \$(DEPS_EMBED) \$(EMBED_GLOBAL_OBJS) \$(PHP_GLOBAL_OBJS) \$(STATIC_EXT_OBJS) \$(ASM_OBJS) \$(BUILD_DIR)\\{$embed_lib}.res \$(BUILD_DIR)\\{$embed_lib}.manifest"; + $new_lines[] = $new_deps; + // Skip the original line (we replaced it) + ++$i; + // Now look for the lib.exe command line (should be the next non-empty line starting with tab) + while ($i < count($lines) && trim($lines[$i]) === '') { + $new_lines[] = $lines[$i]; + ++$i; + } + // Replace the lib.exe command to include PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP + // Original: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + // New: @$(MAKE_LIB) /nologo /out:$(BUILD_DIR)\php8embed.lib $(ARFLAGS) $(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS) $(ARFLAGS_EMBED) $(LIBS_EMBED) $(BUILD_DIR)\php8embed.lib.res + if ($i < count($lines) && str_contains($lines[$i], '$(MAKE_LIB)')) { + $cmd_line = $lines[$i]; + // Remove $(BUILD_DIR)\$(PHPLIB) from the command (note the backslash) + $cmd_line = str_replace(' $(BUILD_DIR)\$(PHPLIB)', '', $cmd_line); + // Add PHP_GLOBAL_OBJS_RESP and STATIC_EXT_OBJS_RESP after EMBED_GLOBAL_OBJS_RESP + $cmd_line = str_replace( + '$(EMBED_GLOBAL_OBJS_RESP)', + '$(EMBED_GLOBAL_OBJS_RESP) $(PHP_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(ASM_OBJS) $(STATIC_EXT_LIBS)', + $cmd_line + ); + $new_lines[] = $cmd_line; + ++$i; + } + } else { + $new_lines[] = $line; + ++$i; + } + } + $content = implode("\r\n", $new_lines); + + FileSystem::writeFile($makefile_path, $content); + } + + #[Stage] + public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void + { + $major = intdiv($this->getPHPVersionID(), 10000); + $embed_lib = "php{$major}embed.lib"; + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow($embed_lib)); + + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" '; + } + } + + // Build the embed static library (patched to include PHP core and extension objects) + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}{$embed_lib}"); + + // Deploy: php8embed.lib is now a REAL static library containing all PHP code + $rel_type = 'Release'; // TODO: Debug build support + $ts = $builder->getOption('enable-zts') ? '_TS' : ''; + $build_dir = "{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}"; + + // copy static embed lib to buildroot/lib + $embed_lib_src = "{$build_dir}\\{$embed_lib}"; + if (file_exists($embed_lib_src)) { + FileSystem::copy($embed_lib_src, "{$package->getLibDir()}\\{$embed_lib}"); + $package->setOutput('Static library path for embed SAPI', "{$package->getLibDir()}\\{$embed_lib}"); + } + + // Note: We no longer deploy php8.dll because the embed static library is self-contained. + // All PHP core code, extensions, and embed SAPI are statically linked into php8embed.lib. + + // copy .pdb debug info if --no-strip + $debug_dir = BUILD_ROOT_PATH . '\debug'; + if ($builder->getOption('no-strip', false)) { + $pdb = "{$build_dir}\\php{$major}embed.pdb"; + if (file_exists($pdb)) { + FileSystem::createDir($debug_dir); + FileSystem::copy($pdb, "{$debug_dir}\\php{$major}embed.pdb"); + } + } + + // Install PHP headers for embed SAPI development + $this->installPhpHeadersForWindows($package, $installer); + } + + #[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(); + 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", + '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()}\\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"); + } + } + } + + #[Stage] + public function smokeTestForWindows(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + $option = match ($no_smoke_test) { + false => false, + null => 'all', + default => parse_comma_list($no_smoke_test), + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + + // remove all .dll from buildroot/bin/ + $dlls = glob(BUILD_BIN_PATH . '\*.dll') ?: []; + foreach ($dlls as $dll) { + @unlink($dll); + } + + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForWindows']); + } + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForWindows']); + } + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForWindows'], ['skipExtTest' => $skipExtTest]); + } + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForWindows'], ['installer' => $installer]); + } + } + + #[Stage] + public function smokeTestCliForWindows(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliWindows(); + } + } + + #[Stage] + public function smokeTestCgiForWindows(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); + $raw_output = implode("\n", $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForWindows(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '\micro.sfx'; + + InteractiveTerm::setMessage('Running php-micro smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '\micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + [$ret, $out] = cmd()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPackage $package): void + { + $test_dir = SOURCE_PATH . '\embed-test'; + FileSystem::createDir($test_dir); + + // Create embed.c test file (Windows version) + $embed_c = <<<'C_CODE' +#include + +int main(int argc, char **argv) { + PHP_EMBED_START_BLOCK(argc, argv) + + zend_file_handle file_handle; + zend_stream_init_filename(&file_handle, "embed.php"); + + if (!php_execute_script(&file_handle)) { + php_printf("Failed to execute PHP script.\n"); + } + + PHP_EMBED_END_BLOCK() + return 0; +} +C_CODE; + FileSystem::writeFile($test_dir . '\embed.c', $embed_c); + + // Create embed.php test file + FileSystem::writeFile($test_dir . '\embed.php', "config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + + // Build the embed test executable using cl.exe + // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall + InteractiveTerm::setMessage('Running php-embed build smoke test'); + + // For Windows, we need to use PHP source directory headers directly + // because Windows PHP doesn't use php_config.h like Unix + $source_dir = $package->getSourceDir(); + $rel_type = 'Release'; + $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; + $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; + + // Build include flags pointing to source dirs (like PHP Windows build does) + // Note: embed.c uses #include , so we need $source_dir itself + $include_flags = sprintf( + '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . + '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', + $build_dir, + $source_dir, + $source_dir, + $source_dir, + $source_dir + ); + + // MSVC cl.exe format: compiler flags must come before /link, linker flags after + // ldflags contains /LIBPATH which must be after /link + $compile_cmd = sprintf( + 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', + $include_flags, + BUILD_ROOT_PATH, + $config['libs'], + 'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS) + ); + + // Log command explicitly (workaround for cmd() not logging complex commands properly) + logger()->debug('Embed smoke test compile command: ' . $compile_cmd); + + [$ret, $out] = cmd()->cd($test_dir)->execWithResult($compile_cmd); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: 'php-embed build smoke test' + ); + } + + // Run the embed test + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . $raw_output, + validation_module: 'php-embed run smoke test' + ); + } + } + + 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_file = "{$src[0]}\\{$src[1]}"; + $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); + + $builder->deployBinary($src_file, $dst_file); + + // copy .pdb debug info file + if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { + FileSystem::createDir($debug_dir); + FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); + } + + // with-upx-pack for cli, cgi and micro + if ($builder->getOption('with-upx-pack', false)) { + if (in_array($sapi, ['php-cli', 'php-cgi', 'php-micro'], true)) { + cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst_file)); + } + } + } + + /** + * Install PHP headers to buildroot/include for embed SAPI development. + * This mirrors the 'make install-headers' behavior on Unix. + */ + private function installPhpHeadersForWindows(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Installing PHP headers for embed SAPI'); + + $source_dir = $package->getSourceDir(); + $include_dir = $package->getIncludeDir(); + $php_include_dir = "{$include_dir}\\php"; + + // Create directory structure + FileSystem::createDir("{$php_include_dir}\\main"); + FileSystem::createDir("{$php_include_dir}\\Zend"); + FileSystem::createDir("{$php_include_dir}\\TSRM"); + FileSystem::createDir("{$php_include_dir}\\sapi\\embed"); + + // Copy main/*.h + foreach (glob("{$source_dir}\\main\\*.h") as $h) { + FileSystem::copy($h, "{$php_include_dir}\\main\\" . basename($h)); + } + + // Copy Zend/*.h + foreach (glob("{$source_dir}\\Zend\\*.h") as $h) { + $target = "{$php_include_dir}\\Zend\\" . basename($h); + FileSystem::copy($h, $target); + // Fix GCC-specific #warning directive not supported by MSVC + if (basename($h) === 'zend_atomic.h') { + FileSystem::replaceFileStr($target, '#warning No atomics support detected. Please open an issue with platform details.', '#pragma message("No atomics support detected. Please open an issue with platform details.")'); + } + } + + // Copy TSRM/*.h + foreach (glob("{$source_dir}\\TSRM\\*.h") as $h) { + FileSystem::copy($h, "{$php_include_dir}\\TSRM\\" . basename($h)); + } + + // Copy embed SAPI header + FileSystem::copy("{$source_dir}\\sapi\\embed\\php_embed.h", "{$php_include_dir}\\sapi\\embed\\php_embed.h"); + + // Copy generated config.h (config.w32.h on Windows) to php_config.h + $rel_type = 'Release'; + $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; + $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; + + // Always copy config.w32.h from source (it's used for both build and headers) + if (file_exists("{$source_dir}\\main\\config.w32.h")) { + FileSystem::copy("{$source_dir}\\main\\config.w32.h", "{$php_include_dir}\\main\\php_config.h"); + } + + // Windows: zend_config.w32.h must be copied as zend_config.h for Zend headers to work + if (file_exists("{$source_dir}\\Zend\\zend_config.w32.h")) { + FileSystem::copy("{$source_dir}\\Zend\\zend_config.w32.h", "{$php_include_dir}\\Zend\\zend_config.h"); + } + + // Copy extension headers for enabled extensions + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + $ext_name = $ext->getExtensionName(); + $ext_dir = "{$source_dir}\\ext\\{$ext_name}"; + if (is_dir($ext_dir)) { + $target_ext_dir = "{$php_include_dir}\\ext\\{$ext_name}"; + FileSystem::createDir($target_ext_dir); + foreach (glob("{$ext_dir}\\*.h") as $h) { + FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); + } + // Also copy any arginfo headers + foreach (glob("{$ext_dir}\\*_arginfo.h") as $h) { + if (!file_exists("{$target_ext_dir}\\" . basename($h))) { + FileSystem::copy($h, "{$target_ext_dir}\\" . basename($h)); + } + } + } + } + + $package->setOutput('PHP headers path for embed SAPI', $php_include_dir); + } +} diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 16e8f45a9..a15ec4737 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -301,6 +301,32 @@ public function isPackageResolved(string $package_name): bool return isset($this->packages[$package_name]); } + /** + * Get resolved package names filtered to only packages whose build artifacts are available. + * This excludes library packages that haven't been built/installed yet, which naturally + * prevents SPCConfigUtil from checking static-lib files of libraries that come after + * the current target in the build order (e.g. 'watcher' for frankenphp isn't built + * when 'php' is being compiled). + * + * @return string[] Available resolved package names + */ + public function getAvailableResolvedPackageNames(): array + { + return array_values(array_filter( + array_keys($this->packages), + function (string $name): bool { + $pkg = $this->packages[$name] ?? null; + // Exclude library packages whose build artifacts don't exist yet. + // Extensions and targets are not filtered — extensions are compiled into PHP + // and don't have standalone build artifacts. + if ($pkg instanceof LibraryPackage && $pkg->getType() === 'library' && !$pkg->isInstalled()) { + return false; + } + return true; + } + )); + } + public function isPackageInstalled(Package|string $package_name): bool { if (empty($this->packages)) { diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index 75c62bea9..ad07f93bd 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -1,64 +1,64 @@ -info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - - $original_command = $cmd; - $this->logCommandInfo($original_command); - $this->last_cmd = $cmd = $this->getExecString($cmd); - // echo $cmd . PHP_EOL; - - $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); - return $this; - } - - public function execWithWrapper(string $wrapper, string $args): WindowsCmd - { - return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); - } - - public function execWithResult(string $cmd, bool $with_log = true): array - { - if ($with_log) { - /* @phpstan-ignore-next-line */ - logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); - } else { - logger()->debug('Running command with result: ' . $cmd); - } - $original_command = $cmd; - $this->logCommandInfo($original_command); - $cmd = $this->getExecString($cmd); - $result = $this->passthru($cmd, $this->console_putput, $original_command, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env); - $out = explode("\n", $result['output']); - return [$result['code'], $out]; - } - - public function getLastCommand(): string - { - return $this->last_cmd; - } - - private function getExecString(string $cmd): string - { - return $cmd; - } -} +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + + $original_command = $cmd; + $this->logCommandInfo($original_command); + $this->last_cmd = $cmd = $this->getExecString($cmd); + // echo $cmd . PHP_EOL; + + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); + return $this; + } + + public function execWithWrapper(string $wrapper, string $args): WindowsCmd + { + return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); + } + + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + logger()->debug('Running command with result: ' . $cmd); + } + $original_command = $cmd; + $this->logCommandInfo($original_command); + $cmd = $this->getExecString($cmd); + $result = $this->passthru($cmd, $this->console_putput, $original_command, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env); + $out = explode("\n", $result['output']); + return [$result['code'], $out]; + } + + public function getLastCommand(): string + { + return $this->last_cmd; + } + + private function getExecString(string $cmd): string + { + return $cmd; + } +} diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index d31e8201a..ece95f392 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -1,509 +1,509 @@ -no_php = $options['no_php'] ?? false; - $this->libs_only_deps = $options['libs_only_deps'] ?? false; - $this->absolute_libs = $options['absolute_libs'] ?? false; - } - - public function config(array $packages = [], bool $include_suggests = false): array - { - // if have php, make php as all extension's dependency - if (!$this->no_php) { - $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; - } else { - $dep_override = []; - } - $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); - - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved); - $libs = $this->getLibsString($resolved, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - // embed - if ($extra_libs = SystemTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } - - $extra_env = getenv('SPC_EXTRA_LIBS'); - if (is_string($extra_env) && !empty($extra_env)) { - $libs .= " {$extra_env}"; - } - // package frameworks - if (SystemTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($resolved)}"; - } - // C++ - if ($this->hasCpp($resolved)) { - $target_os = SystemTarget::getTargetOS(); - if ($target_os === 'Darwin') { - $libcpp = '-lc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } elseif ($target_os !== 'Windows') { - // Linux and other Unix-like systems use libstdc++ - $libcpp = '-lstdc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } - // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed - } - - if ($this->libs_only_deps) { - // mimalloc must come first - if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); - } - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), - ]; - } - - // embed - if (!$this->no_php) { - if (SystemTarget::getTargetOS() === 'Windows') { - // Windows: use php8embed.lib directly (either full path or short name) - $major = intdiv(PHP_VERSION_ID, 10000); - $php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib"; - // Windows system libs required by PHP - // Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib - $libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib"; - } else { - $libs = "-lphp {$libs} -lc"; - } - } - - $allLibs = getenv('LIBS') . ' ' . $libs; - - // mimalloc must come first - if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); - } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; - } - - /** - * [Helper function] - * Get configuration for a specific extension(s) dependencies. - * - * @param array|PhpExtensionPackage $extension_packages Extension instance or list - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array - { - if (!is_array($extension_packages)) { - $extension_packages = [$extension_packages]; - } - return $this->config( - packages: array_map(fn ($y) => $y->getName(), $extension_packages), - include_suggests: $include_suggests, - ); - } - - /** - * [Helper function] - * Get configuration for a specific library(s) dependencies. - * - * @param array|LibraryPackage $lib Library instance or list - * @param bool $include_suggests Whether to include suggested libraries - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array - { - if (!is_array($lib)) { - $lib = [$lib]; - } - $save_no_php = $this->no_php; - $this->no_php = true; - $save_libs_only_deps = $this->libs_only_deps; - $this->libs_only_deps = true; - $ret = $this->config( - packages: array_map(fn ($y) => $y->getName(), $lib), - include_suggests: $include_suggests, - ); - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - return $ret; - } - - /** - * Get build configuration for a package and its sub-dependencies within a resolved set. - * - * This is useful when you need to statically link something against a specific - * library and all its transitive dependencies. It properly handles optional - * dependencies by only including those that were actually resolved. - * - * @param string $package_name The package to get config for - * @param string[] $resolved_packages The full resolved package list - * @param bool $include_suggests Whether to include resolved suggests - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array - { - // Get sub-dependencies within the resolved set - $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); - - if (empty($sub_deps)) { - return [ - 'cflags' => '', - 'ldflags' => '', - 'libs' => '', - ]; - } - - // Use libs_only_deps mode and no_php for library linking - $save_no_php = $this->no_php; - $save_libs_only_deps = $this->libs_only_deps; - $this->no_php = true; - $this->libs_only_deps = true; - - $ret = $this->configWithResolvedPackages($sub_deps); - - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - - return $ret; - } - - /** - * Get configuration using already-resolved packages (skip dependency resolution). - * - * @param string[] $resolved_packages Already resolved package names in build order - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function configWithResolvedPackages(array $resolved_packages): array - { - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved_packages); - $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - if ($extra_libs = SystemTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } - - $extra_env = getenv('SPC_EXTRA_LIBS'); - if (is_string($extra_env) && !empty($extra_env)) { - $libs .= " {$extra_env}"; - } - - // package frameworks - if (SystemTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($resolved_packages)}"; - } - - // C++ - if ($this->hasCpp($resolved_packages)) { - $target_os = SystemTarget::getTargetOS(); - if ($target_os === 'Darwin') { - $libcpp = '-lc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } elseif ($target_os !== 'Windows') { - // Linux and other Unix-like systems use libstdc++ - $libcpp = '-lstdc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } - // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed - } - - if ($this->libs_only_deps) { - // mimalloc must come first - if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); - } - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), - ]; - } - - // embed - if (!$this->no_php) { - $libs = "-lphp {$libs} -lc"; - } - - $allLibs = getenv('LIBS') . ' ' . $libs; - - // mimalloc must come first - if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); - } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; - } - - private function hasCpp(array $packages): bool - { - foreach ($packages as $package) { - $lang = PackageConfig::get($package, 'lang', 'c'); - if ($lang === 'cpp') { - return true; - } - } - return false; - } - - private function getIncludesString(array $packages): string - { - $base = BUILD_INCLUDE_PATH; - - // Windows MSVC uses /I flag instead of -I - if (SystemTarget::getTargetOS() === 'Windows') { - $includes = ["/I\"{$base}\""]; - - // link with libphp - if (!$this->no_php) { - $includes = [ - ...$includes, - "/I\"{$base}\\php\"", - "/I\"{$base}\\php\\main\"", - "/I\"{$base}\\php\\TSRM\"", - "/I\"{$base}\\php\\Zend\"", - "/I\"{$base}\\php\\ext\"", - ]; - } - } else { - $includes = ["-I{$base}"]; - - // link with libphp - if (!$this->no_php) { - $includes = [ - ...$includes, - "-I{$base}/php", - "-I{$base}/php/main", - "-I{$base}/php/TSRM", - "-I{$base}/php/Zend", - "-I{$base}/php/ext", - ]; - } - } - - // parse pkg-configs (only for Unix) - if (SystemTarget::isUnix()) { - foreach ($packages as $package) { - $pc = PackageConfig::get($package, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(':', $pkg_config_path)); - foreach ($pc as $file) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$file}.pc")) { - $found = true; - break; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); - } - } - $pc_cflags = implode(' ', $pc); - if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { - $arr = explode(' ', $pc_cflags); - $arr = array_unique($arr); - $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); - $pc_cflags = implode(' ', $arr); - $includes[] = $pc_cflags; - } - } - } - $includes = array_unique($includes); - return implode(' ', $includes); - } - - private function getLdflagsString(): string - { - // Windows MSVC uses /LIBPATH flag instead of -L - if (SystemTarget::getTargetOS() === 'Windows') { - return '/LIBPATH:"' . BUILD_LIB_PATH . '"'; - } - return '-L' . BUILD_LIB_PATH; - } - - private function getLibsString(array $packages, bool $use_short_libs = true): string - { - $lib_names = []; - $frameworks = []; - - foreach ($packages as $package) { - // parse pkg-configs only for unix systems - if (SystemTarget::isUnix()) { - // add pkg-configs libs - $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(':', $pkg_config_path)); - foreach ($pkg_configs as $pkg_config) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$pkg_config}.pc")) { - $found = true; - break; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); - } - } - $pkg_configs = implode(' ', $pkg_configs); - if ($pkg_configs !== '') { - // static libs with dependencies come in reverse order, so reverse this too - $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); - $lib_names = [...$lib_names, ...$pc_libs]; - } - } - // convert all static-libs to short names - $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); - foreach ($libs as $lib) { - if (FileSystem::isRelativePath($lib)) { - // check file existence - if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { - throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); - } - $lib_names[] = $this->getShortLibName($lib); - } else { - $lib_names[] = $lib; - } - } - // add frameworks for macOS - if (SystemTarget::getTargetOS() === 'Darwin') { - $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); - } - } - - // post-process - $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); - $lib_names = array_reverse(array_unique($lib_names)); - $frameworks = array_unique($frameworks); - - // process frameworks to short_name - if (SystemTarget::getTargetOS() === 'Darwin') { - foreach ($frameworks as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $lib_names)) { - $lib_names[] = $ks; - } - } - } - - if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { - if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { - $lib_names[] = '-lcrypt'; - } - } - if (!$use_short_libs) { - $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); - } - return implode(' ', $lib_names); - } - - private function getShortLibName(string $lib): string - { - // Windows: library files are xxx.lib format (not libxxx.a) - if (SystemTarget::getTargetOS() === 'Windows') { - if (!str_ends_with($lib, '.lib')) { - return BUILD_LIB_PATH . '\\' . $lib; - } - // For Windows, return just the library filename (e.g., "libssl.lib") - return $lib; - } - - // Unix: library files are libxxx.a format - if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { - return BUILD_LIB_PATH . '/' . $lib; - } - // get short name (e.g., "libssl.a" -> "-lssl") - return '-l' . substr($lib, 3, -2); - } - - private function getFullLibName(string $lib): string - { - // Windows: libraries don't use -l prefix, return as-is or with full path - if (SystemTarget::getTargetOS() === 'Windows') { - if (str_ends_with($lib, '.lib') && !str_contains($lib, '\\') && !str_contains($lib, '/')) { - // It's a short lib name like "libssl.lib", convert to full path - $fullPath = BUILD_LIB_PATH . '\\' . $lib; - if (file_exists($fullPath)) { - return $fullPath; - } - } - return $lib; - } - - // Unix: convert -lxxx to full path - if (!str_starts_with($lib, '-l')) { - return $lib; - } - $libname = substr($lib, 2); - $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; - if (file_exists($staticLib)) { - return $staticLib; - } - return $lib; - } - - private function getFrameworksString(array $extensions): string - { - $list = []; - foreach ($extensions as $extension) { - foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $list)) { - $list[] = $ks; - } - } - } - return implode(' ', $list); - } -} +no_php = $options['no_php'] ?? false; + $this->libs_only_deps = $options['libs_only_deps'] ?? false; + $this->absolute_libs = $options['absolute_libs'] ?? false; + } + + public function config(array $packages = [], bool $include_suggests = false): array + { + // if have php, make php as all extension's dependency + if (!$this->no_php) { + $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; + } else { + $dep_override = []; + } + $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); + + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved); + $libs = $this->getLibsString($resolved, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + // embed + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved)}"; + } + // C++ + if ($this->hasCpp($resolved)) { + $target_os = SystemTarget::getTargetOS(); + if ($target_os === 'Darwin') { + $libcpp = '-lc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } elseif ($target_os !== 'Windows') { + // Linux and other Unix-like systems use libstdc++ + $libcpp = '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + if (SystemTarget::getTargetOS() === 'Windows') { + // Windows: use php8embed.lib directly (either full path or short name) + $major = intdiv(PHP_VERSION_ID, 10000); + $php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib"; + // Windows system libs required by PHP + // Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib + $libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib"; + } else { + $libs = "-lphp {$libs} -lc"; + } + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + + /** + * [Helper function] + * Get configuration for a specific extension(s) dependencies. + * + * @param array|PhpExtensionPackage $extension_packages Extension instance or list + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array + { + if (!is_array($extension_packages)) { + $extension_packages = [$extension_packages]; + } + return $this->config( + packages: array_map(fn ($y) => $y->getName(), $extension_packages), + include_suggests: $include_suggests, + ); + } + + /** + * [Helper function] + * Get configuration for a specific library(s) dependencies. + * + * @param array|LibraryPackage $lib Library instance or list + * @param bool $include_suggests Whether to include suggested libraries + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array + { + if (!is_array($lib)) { + $lib = [$lib]; + } + $save_no_php = $this->no_php; + $this->no_php = true; + $save_libs_only_deps = $this->libs_only_deps; + $this->libs_only_deps = true; + $ret = $this->config( + packages: array_map(fn ($y) => $y->getName(), $lib), + include_suggests: $include_suggests, + ); + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + return $ret; + } + + /** + * Get build configuration for a package and its sub-dependencies within a resolved set. + * + * This is useful when you need to statically link something against a specific + * library and all its transitive dependencies. It properly handles optional + * dependencies by only including those that were actually resolved. + * + * @param string $package_name The package to get config for + * @param string[] $resolved_packages The full resolved package list + * @param bool $include_suggests Whether to include resolved suggests + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array + { + // Get sub-dependencies within the resolved set + $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); + + if (empty($sub_deps)) { + return [ + 'cflags' => '', + 'ldflags' => '', + 'libs' => '', + ]; + } + + // Use libs_only_deps mode and no_php for library linking + $save_no_php = $this->no_php; + $save_libs_only_deps = $this->libs_only_deps; + $this->no_php = true; + $this->libs_only_deps = true; + + $ret = $this->configWithResolvedPackages($sub_deps); + + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + + return $ret; + } + + /** + * Get configuration using already-resolved packages (skip dependency resolution). + * + * @param string[] $resolved_packages Already resolved package names in build order + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function configWithResolvedPackages(array $resolved_packages): array + { + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved_packages); + $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved_packages)}"; + } + + // C++ + if ($this->hasCpp($resolved_packages)) { + $target_os = SystemTarget::getTargetOS(); + if ($target_os === 'Darwin') { + $libcpp = '-lc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } elseif ($target_os !== 'Windows') { + // Linux and other Unix-like systems use libstdc++ + $libcpp = '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + $libs = "-lphp {$libs} -lc"; + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + + private function hasCpp(array $packages): bool + { + foreach ($packages as $package) { + $lang = PackageConfig::get($package, 'lang', 'c'); + if ($lang === 'cpp') { + return true; + } + } + return false; + } + + private function getIncludesString(array $packages): string + { + $base = BUILD_INCLUDE_PATH; + + // Windows MSVC uses /I flag instead of -I + if (SystemTarget::getTargetOS() === 'Windows') { + $includes = ["/I\"{$base}\""]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "/I\"{$base}\\php\"", + "/I\"{$base}\\php\\main\"", + "/I\"{$base}\\php\\TSRM\"", + "/I\"{$base}\\php\\Zend\"", + "/I\"{$base}\\php\\ext\"", + ]; + } + } else { + $includes = ["-I{$base}"]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "-I{$base}/php", + "-I{$base}/php/main", + "-I{$base}/php/TSRM", + "-I{$base}/php/Zend", + "-I{$base}/php/ext", + ]; + } + } + + // parse pkg-configs (only for Unix) + if (SystemTarget::isUnix()) { + foreach ($packages as $package) { + $pc = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); + foreach ($pc as $file) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$file}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); + } + } + $pc_cflags = implode(' ', $pc); + if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { + $arr = explode(' ', $pc_cflags); + $arr = array_unique($arr); + $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); + $pc_cflags = implode(' ', $arr); + $includes[] = $pc_cflags; + } + } + } + $includes = array_unique($includes); + return implode(' ', $includes); + } + + private function getLdflagsString(): string + { + // Windows MSVC uses /LIBPATH flag instead of -L + if (SystemTarget::getTargetOS() === 'Windows') { + return '/LIBPATH:"' . BUILD_LIB_PATH . '"'; + } + return '-L' . BUILD_LIB_PATH; + } + + private function getLibsString(array $packages, bool $use_short_libs = true): string + { + $lib_names = []; + $frameworks = []; + + foreach ($packages as $package) { + // parse pkg-configs only for unix systems + if (SystemTarget::isUnix()) { + // add pkg-configs libs + $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); + foreach ($pkg_configs as $pkg_config) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$pkg_config}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); + } + } + $pkg_configs = implode(' ', $pkg_configs); + if ($pkg_configs !== '') { + // static libs with dependencies come in reverse order, so reverse this too + $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); + $lib_names = [...$lib_names, ...$pc_libs]; + } + } + // convert all static-libs to short names + $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); + foreach ($libs as $lib) { + if (FileSystem::isRelativePath($lib)) { + // check file existence + if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { + throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); + } + $lib_names[] = $this->getShortLibName($lib); + } else { + $lib_names[] = $lib; + } + } + // add frameworks for macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); + } + } + + // post-process + $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); + $lib_names = array_reverse(array_unique($lib_names)); + $frameworks = array_unique($frameworks); + + // process frameworks to short_name + if (SystemTarget::getTargetOS() === 'Darwin') { + foreach ($frameworks as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $lib_names)) { + $lib_names[] = $ks; + } + } + } + + if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { + if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { + $lib_names[] = '-lcrypt'; + } + } + if (!$use_short_libs) { + $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); + } + return implode(' ', $lib_names); + } + + private function getShortLibName(string $lib): string + { + // Windows: library files are xxx.lib format (not libxxx.a) + if (SystemTarget::getTargetOS() === 'Windows') { + if (!str_ends_with($lib, '.lib')) { + return BUILD_LIB_PATH . '\\' . $lib; + } + // For Windows, return just the library filename (e.g., "libssl.lib") + return $lib; + } + + // Unix: library files are libxxx.a format + if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { + return BUILD_LIB_PATH . '/' . $lib; + } + // get short name (e.g., "libssl.a" -> "-lssl") + return '-l' . substr($lib, 3, -2); + } + + private function getFullLibName(string $lib): string + { + // Windows: libraries don't use -l prefix, return as-is or with full path + if (SystemTarget::getTargetOS() === 'Windows') { + if (str_ends_with($lib, '.lib') && !str_contains($lib, '\\') && !str_contains($lib, '/')) { + // It's a short lib name like "libssl.lib", convert to full path + $fullPath = BUILD_LIB_PATH . '\\' . $lib; + if (file_exists($fullPath)) { + return $fullPath; + } + } + return $lib; + } + + // Unix: convert -lxxx to full path + if (!str_starts_with($lib, '-l')) { + return $lib; + } + $libname = substr($lib, 2); + $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; + if (file_exists($staticLib)) { + return $staticLib; + } + return $lib; + } + + private function getFrameworksString(array $extensions): string + { + $list = []; + foreach ($extensions as $extension) { + foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $list)) { + $list[] = $ks; + } + } + } + return implode(' ', $list); + } +} From 85b9f5e05576cc8774322271ff95d37d61924a7b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Apr 2026 13:15:25 +0800 Subject: [PATCH 547/682] Refactor package resolution to filter only available build artifacts --- .../Runtime/Executor/UnixCMakeExecutor.php | 2 +- src/StaticPHP/Util/SPCConfigUtil.php | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 2269c3231..73209fff0 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -233,7 +233,7 @@ private function getDefaultCMakeArgs(): array if ($this->package instanceof TargetPackage) { $resolvedNames = array_keys($this->installer->getResolvedPackages()); $resolvedNames[] = $this->package->getName(); - $fwFlags = SPCConfigUtil::getFrameworksString($resolvedNames); + $fwFlags = new SPCConfigUtil()->getFrameworksString($resolvedNames); if ($fwFlags !== '') { $exeLinkerFlags .= " {$fwFlags}"; } diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index ece95f392..63b0e90ef 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -285,6 +285,20 @@ public function configWithResolvedPackages(array $resolved_packages): array ]; } + public function getFrameworksString(array $extensions): string + { + $list = []; + foreach ($extensions as $extension) { + foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $list)) { + $list[] = $ks; + } + } + } + return implode(' ', $list); + } + private function hasCpp(array $packages): bool { foreach ($packages as $package) { @@ -492,18 +506,4 @@ private function getFullLibName(string $lib): string } return $lib; } - - private function getFrameworksString(array $extensions): string - { - $list = []; - foreach ($extensions as $extension) { - foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $list)) { - $list[] = $ks; - } - } - } - return implode(' ', $list); - } } From 8fa27ae59c8a073969ef6c7aa4c430c4a8058043 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 7 Apr 2026 17:04:08 +0800 Subject: [PATCH 548/682] Enhance spc-debug script to support Xdebug profiling mode --- bin/spc-debug | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bin/spc-debug b/bin/spc-debug index d5a18c837..2c5360037 100755 --- a/bin/spc-debug +++ b/bin/spc-debug @@ -1,4 +1,12 @@ #!/usr/bin/env bash +# Use SPC_XDEBUG=profile to enable Xdebug profiling mode, which will generate profiling files in /tmp. +# Otherwise, it will enable Xdebug debugging mode, which allows you to connect a debugger to port 9003. +if [ "$SPC_XDEBUG" = "profile" ]; then + XDEBUG_PREFIX="-d xdebug.mode=profile -d xdebug.start_with_request=yes -d xdebug.output_dir=/tmp -d xdebug.output_name=spc-profile.%t.%p.%r" +else + XDEBUG_PREFIX="-d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.client_port=9003 -d xdebug.start_with_request=yes" +fi + # This script runs the 'spc' command with Xdebug enabled for debugging purposes. -php -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.client_port=9003 -d xdebug.start_with_request=yes "$(dirname "$0")/../bin/spc" "$@" +php $XDEBUG_PREFIX "$(dirname "$0")/../bin/spc" "$@" From 8e91e028069f63eab439e7e811ff411d8806c936 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 7 Apr 2026 17:09:05 +0800 Subject: [PATCH 549/682] Add suggestion for ext-yaml to improve YAML config file parsing --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 0d4fde4e8..e6c2354c0 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,9 @@ "optimize-autoloader": true, "sort-packages": true }, + "suggest": { + "ext-yaml": "Speeds up YAML config file parsing" + }, "funding": [ { "type": "other", From baa21d6e946271a9ea36921a8fa1f40e02ce8604 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 7 Apr 2026 17:10:33 +0800 Subject: [PATCH 550/682] Implement caching for config file parsing to improve performance --- .gitignore | 3 ++ src/StaticPHP/Config/ArtifactConfig.php | 33 +++++++++--- src/StaticPHP/Config/ConfigCache.php | 67 +++++++++++++++++++++++++ src/StaticPHP/Config/PackageConfig.php | 20 +++++--- src/StaticPHP/Registry/Registry.php | 2 +- 5 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 src/StaticPHP/Config/ConfigCache.php diff --git a/.gitignore b/.gitignore index 21bae186b..810af82ce 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ spc.exe # dumped files from StaticPHP v3 /dump-*.json + +# config parse cache +/.spc.cache.php diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index e3e59fb55..db0af2884 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -42,13 +42,22 @@ public static function loadFromFile(string $file, string $registry_name): string if ($content === false) { throw new WrongUsageException("Failed to read artifact config file: {$file}"); } - $data = match (pathinfo($file, PATHINFO_EXTENSION)) { - 'json' => json_decode($content, true), - 'yml', 'yaml' => Yaml::parse($content), - default => throw new WrongUsageException("Unsupported artifact config file format: {$file}"), - }; - if (!is_array($data)) { - throw new WrongUsageException("Invalid JSON format in artifact config file: {$file}"); + // use cache to skip redundant parsing + $data = ConfigCache::get($content); + if ($data !== null) { + logger()->debug("Config cache hit: {$file}"); + } else { + $data = match (pathinfo($file, PATHINFO_EXTENSION)) { + 'json' => json_decode($content, true), + 'yml', 'yaml' => extension_loaded('yaml') ? yaml_parse($content) : Yaml::parse($content), + default => throw new WrongUsageException("Unsupported artifact config file format: {$file}"), + }; + if (!is_array($data)) { + throw new WrongUsageException("Invalid JSON format in artifact config file: {$file}"); + } + if (is_array($data)) { + ConfigCache::set($content, $data); + } } ConfigValidator::validateAndLintArtifacts(basename($file), $data); foreach ($data as $artifact_name => $config) { @@ -68,6 +77,16 @@ public static function getAll(): array return self::$artifact_configs; } + /** + * Restore artifact configs from cache without re-parsing YAML files. + * + * @internal used by Registry cache layer only + */ + public static function _restoreFromCache(array $configs): void + { + self::$artifact_configs = array_merge(self::$artifact_configs, $configs); + } + /** * Get the configuration for a specific artifact by name. * diff --git a/src/StaticPHP/Config/ConfigCache.php b/src/StaticPHP/Config/ConfigCache.php new file mode 100644 index 000000000..a81879572 --- /dev/null +++ b/src/StaticPHP/Config/ConfigCache.php @@ -0,0 +1,67 @@ +/.spc.cache.php (plain PHP, var_export'd array). + * Written once on shutdown when any new entry was added. + */ +class ConfigCache +{ + private static ?array $cache = null; + + private static bool $dirty = false; + + /** + * Return the cached parsed result for $content, or null on miss. + */ + public static function get(string $content): ?array + { + self::load(); + return self::$cache[$content] ?? null; + } + + /** + * Store a parsed result. Will be persisted to disk on shutdown. + */ + public static function set(string $content, array $data): void + { + self::load(); + self::$cache[$content] = $data; + self::$dirty = true; + } + + /** + * Write cache to disk if anything changed. Called automatically on shutdown. + */ + public static function flush(): void + { + if (!self::$dirty) { + return; + } + file_put_contents(self::cachePath(), ' json_decode($content, true), - 'yml', 'yaml' => Yaml::parse($content), - default => throw new WrongUsageException("Unsupported package config file format: {$file}"), - }; + // judge extension — use cache to skip redundant parsing + $data = ConfigCache::get($content); + if ($data !== null) { + logger()->debug("Config cache hit: {$file}"); + } else { + $data = match (pathinfo($file, PATHINFO_EXTENSION)) { + 'json' => json_decode($content, true), + 'yml', 'yaml' => extension_loaded('yaml') ? yaml_parse($content) : Yaml::parse($content), + default => throw new WrongUsageException("Unsupported package config file format: {$file}"), + }; + if (is_array($data)) { + ConfigCache::set($content, $data); + } + } ConfigValidator::validateAndLintPackages(basename($file), $data); foreach ($data as $pkg_name => $config) { self::$package_configs[$pkg_name] = $config; diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 4075b70b4..8f206c300 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -64,7 +64,7 @@ public static function loadRegistry(string $registry_file, bool $auto_require = } $data = match (pathinfo($registry_file, PATHINFO_EXTENSION)) { 'json' => json_decode($yaml, true), - 'yaml', 'yml' => Yaml::parse($yaml), + 'yaml', 'yml' => extension_loaded('yaml') ? yaml_parse($yaml) : Yaml::parse($yaml), default => throw new RegistryException("Unsupported registry file format: {$registry_file}"), }; if (!is_array($data)) { From 30c9a3f7a34c693c583dab3c6b5121b40c16b5ce Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 7 Apr 2026 17:11:27 +0800 Subject: [PATCH 551/682] phpstan --- src/StaticPHP/Config/ArtifactConfig.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index db0af2884..8462091d1 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -55,9 +55,7 @@ public static function loadFromFile(string $file, string $registry_name): string if (!is_array($data)) { throw new WrongUsageException("Invalid JSON format in artifact config file: {$file}"); } - if (is_array($data)) { - ConfigCache::set($content, $data); - } + ConfigCache::set($content, $data); } ConfigValidator::validateAndLintArtifacts(basename($file), $data); foreach ($data as $artifact_name => $config) { From 76fc5abfe7ac9d2596822af706cba8b35976b9c6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 10:04:07 +0800 Subject: [PATCH 552/682] Apply v2 patch for xlswriter --- config/pkg/ext/ext-xlswriter.yml | 1 + src/Package/Extension/xlswriter.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/config/pkg/ext/ext-xlswriter.yml b/config/pkg/ext/ext-xlswriter.yml index 24d2fa3ce..c13751af0 100644 --- a/config/pkg/ext/ext-xlswriter.yml +++ b/config/pkg/ext/ext-xlswriter.yml @@ -16,3 +16,4 @@ ext-xlswriter: support: BSD: wip arg-type: custom + arg-type@windows: '--with-xlswriter' diff --git a/src/Package/Extension/xlswriter.php b/src/Package/Extension/xlswriter.php index b2f25716e..ae11f307e 100644 --- a/src/Package/Extension/xlswriter.php +++ b/src/Package/Extension/xlswriter.php @@ -4,10 +4,14 @@ namespace Package\Extension; +use Package\Target\php; +use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\SourcePatcher; #[Extension('xlswriter')] class xlswriter extends PhpExtensionPackage @@ -22,4 +26,17 @@ public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): } return $arg; } + + #[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-xlswriter')] + #[PatchDescription('Fix Windows build: apply win32 patch and add UTF-8 BOM to theme.c')] + public function patchBeforeMakeForWindows(): void + { + // fix windows build with openssl extension duplicate symbol bug + SourcePatcher::patchFile('spc_fix_xlswriter_win32.patch', $this->getSourceDir()); + $content = file_get_contents($this->getSourceDir() . '/library/libxlsxwriter/src/theme.c'); + $bom = pack('CCC', 0xEF, 0xBB, 0xBF); + if (!str_starts_with($content, $bom)) { + file_put_contents($this->getSourceDir() . '/library/libxlsxwriter/src/theme.c', $bom . $content); + } + } } From 921870eaea98c4268ffe98d40969fb45bd4143ee Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 10:08:24 +0800 Subject: [PATCH 553/682] Update composer.lock --- composer.lock | 379 +++++++++++++++----------------------------------- 1 file changed, 111 insertions(+), 268 deletions(-) diff --git a/composer.lock b/composer.lock index eded86efb..69cc2e278 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f30595c9c60e55083112410cd1ffb203", + "content-hash": "1d5518bdf7730190aead0e953abff538", "packages": [ { "name": "laravel/prompts", - "version": "v0.3.12", + "version": "v0.3.15", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8" + "reference": "4bb8107ec97651fd3f17f897d6489dbc4d8fb999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/4861ded9003b7f8a158176a0b7666f74ee761be8", - "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8", + "url": "https://api.github.com/repos/laravel/prompts/zipball/4bb8107ec97651fd3f17f897d6489dbc4d8fb999", + "reference": "4bb8107ec97651fd3f17f897d6489dbc4d8fb999", "shasum": "" }, "require": { @@ -61,22 +61,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.12" + "source": "https://github.com/laravel/prompts/tree/v0.3.15" }, - "time": "2026-02-03T06:57:26+00:00" + "time": "2026-03-17T13:45:17+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.9", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", - "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", "shasum": "" }, "require": { @@ -124,168 +124,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-02-03T06:55:34+00:00" - }, - { - "name": "nette/php-generator", - "version": "v4.2.1", - "source": { - "type": "git", - "url": "https://github.com/nette/php-generator.git", - "reference": "52aff4d9b12f20ca9f3e31a559b646d2fd21dd61" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/52aff4d9b12f20ca9f3e31a559b646d2fd21dd61", - "reference": "52aff4d9b12f20ca9f3e31a559b646d2fd21dd61", - "shasum": "" - }, - "require": { - "nette/utils": "^4.0.6", - "php": "8.1 - 8.5" - }, - "require-dev": { - "jetbrains/phpstorm-attributes": "^1.2", - "nette/tester": "^2.6", - "nikic/php-parser": "^5.0", - "phpstan/phpstan": "^2.0@stable", - "tracy/tracy": "^2.8" - }, - "suggest": { - "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-4": { - "Nette\\": "src" - }, - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" - ], - "authors": [ - { - "name": "David Grudl", - "homepage": "https://davidgrudl.com" - }, - { - "name": "Nette Community", - "homepage": "https://nette.org/contributors" - } - ], - "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.", - "homepage": "https://nette.org", - "keywords": [ - "code", - "nette", - "php", - "scaffolding" - ], - "support": { - "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v4.2.1" - }, - "time": "2026-02-09T05:43:31+00:00" - }, - { - "name": "nette/utils", - "version": "v4.1.2", - "source": { - "type": "git", - "url": "https://github.com/nette/utils.git", - "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", - "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", - "shasum": "" - }, - "require": { - "php": "8.2 - 8.5" - }, - "conflict": { - "nette/finder": "<3", - "nette/schema": "<1.2.2" - }, - "require-dev": { - "jetbrains/phpstorm-attributes": "^1.2", - "nette/tester": "^2.5", - "phpstan/phpstan": "^2.0@stable", - "tracy/tracy": "^2.9" - }, - "suggest": { - "ext-gd": "to use Image", - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", - "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", - "ext-json": "to use Nette\\Utils\\Json", - "ext-mbstring": "to use Strings::lower() etc...", - "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Nette\\": "src" - }, - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" - ], - "authors": [ - { - "name": "David Grudl", - "homepage": "https://davidgrudl.com" - }, - { - "name": "Nette Community", - "homepage": "https://nette.org/contributors" - } - ], - "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", - "homepage": "https://nette.org", - "keywords": [ - "array", - "core", - "datetime", - "images", - "json", - "nette", - "paginator", - "password", - "slugify", - "string", - "unicode", - "utf-8", - "utility", - "validation" - ], - "support": { - "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.2" - }, - "time": "2026-02-03T17:21:09+00:00" + "time": "2026-02-20T19:59:49+00:00" }, { "name": "php-di/invoker", @@ -520,16 +359,16 @@ }, { "name": "symfony/console", - "version": "v7.4.4", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", - "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", "shasum": "" }, "require": { @@ -594,7 +433,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.4" + "source": "https://github.com/symfony/console/tree/v7.4.7" }, "funding": [ { @@ -614,7 +453,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T11:36:38+00:00" + "time": "2026-03-06T14:06:20+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1172,16 +1011,16 @@ }, { "name": "symfony/string", - "version": "v8.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "758b372d6882506821ed666032e43020c4f57194" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", - "reference": "758b372d6882506821ed666032e43020c4f57194", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { @@ -1238,7 +1077,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -1258,20 +1097,20 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.1", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "24dd4de28d2e3988b311751ac49e684d783e2345" + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345", - "reference": "24dd4de28d2e3988b311751ac49e684d783e2345", + "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", + "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", "shasum": "" }, "require": { @@ -1314,7 +1153,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.1" + "source": "https://github.com/symfony/yaml/tree/v7.4.6" }, "funding": [ { @@ -1334,7 +1173,7 @@ "type": "tidelift" } ], - "time": "2025-12-04T18:11:45+00:00" + "time": "2026-02-09T09:33:46+00:00" }, { "name": "zhamao/logger", @@ -2217,7 +2056,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.28.0", + "version": "5.29.0", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2271,7 +2110,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.28.0" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.29.0" }, "funding": [ { @@ -2968,16 +2807,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.93.1", + "version": "v3.94.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a" + "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b3546ab487c0762c39f308dc1ec0ea2c461fc21a", - "reference": "b3546ab487c0762c39f308dc1ec0ea2c461fc21a", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63", + "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63", "shasum": "" }, "require": { @@ -2994,7 +2833,7 @@ "react/event-loop": "^1.5", "react/socket": "^1.16", "react/stream": "^1.4", - "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", @@ -3008,18 +2847,18 @@ "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.32", - "justinrainbow/json-schema": "^6.6", + "facile-it/paraunit": "^1.3.1 || ^2.7.1", + "infection/infection": "^0.32.3", + "justinrainbow/json-schema": "^6.6.4", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.9", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.31 || ^10.5.60 || ^11.5.48", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51", "symfony/polyfill-php85": "^1.33", - "symfony/var-dumper": "^5.4.48 || ^6.4.26 || ^7.4.0 || ^8.0", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0" + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -3060,7 +2899,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.93.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2" }, "funding": [ { @@ -3068,20 +2907,20 @@ "type": "github" } ], - "time": "2026-01-28T23:50:50+00:00" + "time": "2026-02-20T16:13:53+00:00" }, { "name": "humbug/box", - "version": "4.6.10", + "version": "4.7.0", "source": { "type": "git", "url": "https://github.com/box-project/box.git", - "reference": "6dc6a1314d63e9d75c8195c996e1081e68514c36" + "reference": "9c2a430118f61ba4a20bc4969931494503f5da6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/box-project/box/zipball/6dc6a1314d63e9d75c8195c996e1081e68514c36", - "reference": "6dc6a1314d63e9d75c8195c996e1081e68514c36", + "url": "https://api.github.com/repos/box-project/box/zipball/9c2a430118f61ba4a20bc4969931494503f5da6a", + "reference": "9c2a430118f61ba4a20bc4969931494503f5da6a", "shasum": "" }, "require": { @@ -3180,9 +3019,9 @@ ], "support": { "issues": "https://github.com/box-project/box/issues", - "source": "https://github.com/box-project/box/tree/4.6.10" + "source": "https://github.com/box-project/box/tree/4.7.0" }, - "time": "2025-10-31T18:38:02+00:00" + "time": "2026-03-18T09:34:43+00:00" }, { "name": "humbug/php-scoper", @@ -3317,16 +3156,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.4", + "version": "v6.7.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" + "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", - "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", + "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", "shasum": "" }, "require": { @@ -3386,9 +3225,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" + "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" }, - "time": "2025-12-19T15:01:32+00:00" + "time": "2026-02-15T15:06:22+00:00" }, { "name": "kelunik/certificate", @@ -3450,20 +3289,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -3536,7 +3375,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -3544,20 +3383,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -3620,7 +3459,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -3628,7 +3467,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "marc-mabe/php-enum", @@ -4260,16 +4099,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.6", + "version": "5.6.7", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8" + "reference": "31a105931bc8ffa3a123383829772e832fd8d903" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8", - "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/31a105931bc8ffa3a123383829772e832fd8d903", + "reference": "31a105931bc8ffa3a123383829772e832fd8d903", "shasum": "" }, "require": { @@ -4318,9 +4157,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.7" }, - "time": "2025-12-22T21:13:58+00:00" + "time": "2026-03-18T20:47:46+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4429,11 +4268,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.38", + "version": "2.1.42", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", - "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", "shasum": "" }, "require": { @@ -4478,7 +4317,7 @@ "type": "github" } ], - "time": "2026-01-30T17:12:46+00:00" + "time": "2026-03-17T14:58:32+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6894,16 +6733,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.0", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", - "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", + "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", "shasum": "" }, "require": { @@ -6940,7 +6779,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.0" + "source": "https://github.com/symfony/filesystem/tree/v7.4.6" }, "funding": [ { @@ -6960,20 +6799,20 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2026-02-25T16:50:00+00:00" }, { "name": "symfony/finder", - "version": "v7.4.5", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", - "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", "shasum": "" }, "require": { @@ -7008,7 +6847,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.5" + "source": "https://github.com/symfony/finder/tree/v7.4.6" }, "funding": [ { @@ -7028,7 +6867,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-01-29T09:40:50+00:00" }, { "name": "symfony/options-resolver", @@ -7333,16 +7172,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.4", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", - "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", "shasum": "" }, "require": { @@ -7396,7 +7235,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" }, "funding": [ { @@ -7416,20 +7255,20 @@ "type": "tidelift" } ], - "time": "2026-01-01T22:13:48+00:00" + "time": "2026-02-15T10:53:20+00:00" }, { "name": "thecodingmachine/safe", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", - "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", "shasum": "" }, "require": { @@ -7539,7 +7378,7 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" }, "funding": [ { @@ -7550,12 +7389,16 @@ "url": "https://github.com/shish", "type": "github" }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, { "url": "https://github.com/staabm", "type": "github" } ], - "time": "2025-05-14T06:15:44+00:00" + "time": "2026-02-04T18:08:13+00:00" }, { "name": "theseer/tokenizer", From ee854eed41deeffc5903d724bdaa0d963b4622fc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 11:08:05 +0800 Subject: [PATCH 554/682] Refactor extension configuration and improve Windows build support --- composer.json | 2 +- config/pkg/ext/builtin-extensions.yml | 38 ++++++++++++++++-------- config/pkg/ext/ext-excimer.yml | 4 +++ config/pkg/ext/ext-parallel.yml | 4 +++ src/Package/Artifact/php_src.php | 8 ++++- src/Package/Extension/ev.php | 30 +++++++++++++++++++ src/Package/Extension/intl.php | 29 ++++++++++++++++++ src/Package/Extension/mbregex.php | 1 + src/Package/Extension/mbstring.php | 6 ++++ src/Package/Library/libjpeg.php | 1 + src/StaticPHP/Command/ResetCommand.php | 13 +++++--- src/StaticPHP/Config/ConfigValidator.php | 2 ++ 12 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 src/Package/Extension/ev.php create mode 100644 src/Package/Extension/intl.php diff --git a/composer.json b/composer.json index e6c2354c0..d006fbbfd 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "scripts": { "analyse": "phpstan analyse --memory-limit 300M", "cs-fix": "php-cs-fixer fix", - "lint-config": "bin/spc dev:lint-config", + "lint-config": "php bin/spc dev:lint-config", "test": "vendor/bin/phpunit tests/ --no-coverage", "build:phar": "vendor/bin/box compile" }, diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index e5a6d28a9..c4a938d16 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -9,6 +9,14 @@ ext-bz2: arg-type@windows: with ext-calendar: type: php-extension +ext-com_dotnet: + type: php-extension + php-extension: + support: + Linux: 'no' + Darwin: 'no' + BSD: 'no' + arg-type@windows: '--enable-com-dotnet=yes' ext-ctype: type: php-extension ext-curl: @@ -30,17 +38,20 @@ ext-dba: ext-dom: type: php-extension depends: - - libxml2 - ext-xml php-extension: - arg-type: '--enable-dom@shared_suffix@ --with-libxml=@build_root_path@' + arg-type: enable arg-type@windows: with ext-exif: type: php-extension + depends@windows: + - ext-mbstring ext-ffi: type: php-extension depends@unix: - libffi + depends@windows: + - libffi-win php-extension: arg-type@unix: '--with-ffi=@shared_suffix@ --enable-zend-signals' arg-type@windows: with @@ -101,11 +112,12 @@ ext-ldap: ext-libxml: type: php-extension depends: - - ext-xml + - libxml2 php-extension: build-with-php: true build-shared: false - arg-type: none + arg-type@unix: with-path + arg-type@windows: with ext-mbregex: type: php-extension depends: @@ -239,7 +251,7 @@ ext-simplexml: depends: - ext-xml php-extension: - arg-type@unix: '--enable-simplexml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@unix: enable arg-type@windows: with build-with-php: true ext-snmp: @@ -254,7 +266,7 @@ ext-soap: - ext-xml - ext-session php-extension: - arg-type@unix: '--enable-soap@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@unix: enable arg-type@windows: with build-with-php: true ext-sockets: @@ -306,27 +318,27 @@ ext-tokenizer: ext-xml: type: php-extension depends: - - libxml2 + - ext-libxml depends@windows: - - libxml2 - ext-iconv + - ext-libxml php-extension: - arg-type: '--enable-xml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@unix: enable arg-type@windows: with build-with-php: true ext-xmlreader: type: php-extension depends: - - libxml2 + - ext-xml php-extension: - arg-type: '--enable-xmlreader@shared_suffix@ --with-libxml=@build_root_path@' + arg-type: enable build-with-php: true ext-xmlwriter: type: php-extension depends: - - libxml2 + - ext-xml php-extension: - arg-type: '--enable-xmlwriter@shared_suffix@ --with-libxml=@build_root_path@' + arg-type: enable build-with-php: true ext-xsl: type: php-extension diff --git a/config/pkg/ext/ext-excimer.yml b/config/pkg/ext/ext-excimer.yml index 3d0858882..a896fe0bd 100644 --- a/config/pkg/ext/ext-excimer.yml +++ b/config/pkg/ext/ext-excimer.yml @@ -7,3 +7,7 @@ ext-excimer: metadata: license-files: [LICENSE] license: PHP-3.01 + php-extension: + os: + - Linux + - Darwin diff --git a/config/pkg/ext/ext-parallel.yml b/config/pkg/ext/ext-parallel.yml index a3e91efe5..94103f578 100644 --- a/config/pkg/ext/ext-parallel.yml +++ b/config/pkg/ext/ext-parallel.yml @@ -7,3 +7,7 @@ ext-parallel: metadata: license-files: [LICENSE] license: PHP-3.01 + depends@windows: + - pthreads4w + php-extension: + arg-type@windows: with diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php index 119f9056e..b52ba7282 100644 --- a/src/Package/Artifact/php_src.php +++ b/src/Package/Artifact/php_src.php @@ -45,7 +45,13 @@ public function patchGDWin32(): void FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gd/libgd/gdft.c', '#ifndef MSWIN32', '#ifndef _WIN32'); } // custom config.w32, because official config.w32 is hard-coded many things - $origin = $ver_id >= 80100 ? file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32') : file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32'); + if ($ver_id >= 80500) { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_85.w32'); + } elseif ($ver_id >= 80100) { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32'); + } else { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32'); + } file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32.bak', file_get_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32')); file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32', $origin); } diff --git a/src/Package/Extension/ev.php b/src/Package/Extension/ev.php new file mode 100644 index 000000000..b4b4b9427 --- /dev/null +++ b/src/Package/Extension/ev.php @@ -0,0 +1,30 @@ +getSourceDir()}\\config.w32", + 'EXTENSION(\'ev\'', + " EXTENSION('ev', php_ev_sources, PHP_EV_SHARED, ' /DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');" + ); + return true; + } +} diff --git a/src/Package/Extension/intl.php b/src/Package/Extension/intl.php new file mode 100644 index 000000000..f5e17c1fe --- /dev/null +++ b/src/Package/Extension/intl.php @@ -0,0 +1,29 @@ +getTargetPackage('php')->getSourceDir(); + FileSystem::replaceFileStr( + "{$php_src}/ext/intl/config.w32", + 'EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", true,', + 'EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", PHP_INTL_SHARED,' + ); + } +} diff --git a/src/Package/Extension/mbregex.php b/src/Package/Extension/mbregex.php index f01c7c787..20b025467 100644 --- a/src/Package/Extension/mbregex.php +++ b/src/Package/Extension/mbregex.php @@ -12,6 +12,7 @@ class mbregex { #[CustomPhpConfigureArg('Linux')] #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Windows')] public function getUnixConfigureArg(): string { return ''; diff --git a/src/Package/Extension/mbstring.php b/src/Package/Extension/mbstring.php index 5c3c31d13..b6d818f16 100644 --- a/src/Package/Extension/mbstring.php +++ b/src/Package/Extension/mbstring.php @@ -19,4 +19,10 @@ public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): $arg .= $installer->isPackageResolved('ext-mbregex') === false ? ' --disable-mbregex' : ' --enable-mbregex'; return $arg; } + + #[CustomPhpConfigureArg('Windows')] + public function getWinConfigureArg(PackageInstaller $installer): string + { + return '--enable-mbstring ' . ($installer->isPackageResolved('ext-mbregex') ? '--enable-mbregex' : ' --disable-mbregex'); + } } diff --git a/src/Package/Library/libjpeg.php b/src/Package/Library/libjpeg.php index 6e06bfb70..85b0f6d49 100644 --- a/src/Package/Library/libjpeg.php +++ b/src/Package/Library/libjpeg.php @@ -37,6 +37,7 @@ public function buildWin(LibraryPackage $lib): void '-DENABLE_STATIC=ON', '-DBUILD_TESTING=OFF', '-DWITH_JAVA=OFF', + '-DWITH_SIMD=OFF', '-DWITH_CRT_DLL=OFF', ) ->optionalPackage('zlib', '-DENABLE_ZLIB_COMPRESSION=ON', '-DENABLE_ZLIB_COMPRESSION=OFF') diff --git a/src/StaticPHP/Command/ResetCommand.php b/src/StaticPHP/Command/ResetCommand.php index 4a55f792a..207f6484e 100644 --- a/src/StaticPHP/Command/ResetCommand.php +++ b/src/StaticPHP/Command/ResetCommand.php @@ -4,6 +4,7 @@ namespace StaticPHP\Command; +use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; @@ -87,20 +88,24 @@ private function removeDirectoryWindows(string $path): void // Try using PowerShell for force deletion $escaped_path = escapeshellarg($path); + Shell::passthruCallback(fn () => InteractiveTerm::advance()); // Use PowerShell Remove-Item with -Force and -Recurse - $ps_cmd = "powershell -Command \"Remove-Item -Path {$escaped_path} -Recurse -Force -ErrorAction SilentlyContinue\""; - f_exec($ps_cmd, $output, $ret_code); - + $result = cmd()->execWithResult("powershell -Command \"Remove-Item -Path {$escaped_path} -Recurse -Force -ErrorAction SilentlyContinue\"", false); + $ret_code = $result[0]; // If PowerShell fails or directory still exists, try cmd rmdir if ($ret_code !== 0 || is_dir($path)) { $cmd_command = "rmdir /s /q {$escaped_path}"; - f_exec($cmd_command, $output, $ret_code); + $result = cmd()->execWithResult($cmd_command, false); + if ($result[0] === 0) { + return; + } } // Final fallback: use FileSystem::removeDir if (is_dir($path)) { FileSystem::removeDir($path); } + Shell::passthruCallback(null); } } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 919de86df..b66250cc9 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -34,6 +34,7 @@ class ConfigValidator 'build-with-php' => ConfigType::BOOL, 'notes' => ConfigType::BOOL, 'display-name' => ConfigType::STRING, + 'os' => ConfigType::LIST_ARRAY, // library and target fields 'headers' => ConfigType::LIST_ARRAY, // @ @@ -88,6 +89,7 @@ class ConfigValidator 'build-with-php' => false, 'notes' => false, 'display-name' => false, + 'os' => false, ]; public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] From a3624b1510e8b1bff039d6b550807e370128be51 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 11:22:45 +0800 Subject: [PATCH 555/682] Forward-port #1078, add sqlsrv and pdo_sqlsrv extension support for win --- src/Package/Extension/sqlsrv.php | 28 ++++++++++++++++++++++++++++ src/StaticPHP/Util/FileSystem.php | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/Package/Extension/sqlsrv.php diff --git a/src/Package/Extension/sqlsrv.php b/src/Package/Extension/sqlsrv.php new file mode 100644 index 000000000..39f64f5eb --- /dev/null +++ b/src/Package/Extension/sqlsrv.php @@ -0,0 +1,28 @@ +getTargetPackage('php')->getSourceDir() . '\Makefile'; + $makeContent = file_get_contents($makefile); + $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/W4\b/m', '$1', $makeContent); + $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/WX\b/m', '$1', $makeContent); + file_put_contents($makefile, $makeContent); + return true; + } +} diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 38a614e01..77d7f52b9 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': - cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i'); + cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/y/i'); break; case 'Linux': case 'Darwin': From ad631f9b6e123d3486b446dbefcb97eb9734d519 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 14:49:38 +0800 Subject: [PATCH 556/682] Add pdo_sqlsrv, libyaml patches from v2 --- config/pkg/ext/builtin-extensions.yml | 6 ++--- src/Package/Extension/pdo_sqlsrv.php | 39 +++++++++++++++++++++++++++ src/Package/Extension/yaml.php | 28 +++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/Package/Extension/pdo_sqlsrv.php create mode 100644 src/Package/Extension/yaml.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index c4a938d16..6b37a7d87 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -12,10 +12,8 @@ ext-calendar: ext-com_dotnet: type: php-extension php-extension: - support: - Linux: 'no' - Darwin: 'no' - BSD: 'no' + os: + - Windows arg-type@windows: '--enable-com-dotnet=yes' ext-ctype: type: php-extension diff --git a/src/Package/Extension/pdo_sqlsrv.php b/src/Package/Extension/pdo_sqlsrv.php new file mode 100644 index 000000000..00134db14 --- /dev/null +++ b/src/Package/Extension/pdo_sqlsrv.php @@ -0,0 +1,39 @@ + Date: Wed, 8 Apr 2026 22:11:45 +0800 Subject: [PATCH 557/682] Add gettext-win --- config/pkg/lib/gettext-win.yml | 9 +++++ src/Package/Library/gettext_win.php | 55 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 config/pkg/lib/gettext-win.yml create mode 100644 src/Package/Library/gettext_win.php diff --git a/config/pkg/lib/gettext-win.yml b/config/pkg/lib/gettext-win.yml new file mode 100644 index 000000000..142383077 --- /dev/null +++ b/config/pkg/lib/gettext-win.yml @@ -0,0 +1,9 @@ +gettext-win: + type: library + artifact: + source: + type: git + url: 'https://github.com/winlibs/gettext.git' + rev: master + static-libs@windows: + - libintl_a.lib diff --git a/src/Package/Library/gettext_win.php b/src/Package/Library/gettext_win.php new file mode 100644 index 000000000..093e4a3a0 --- /dev/null +++ b/src/Package/Library/gettext_win.php @@ -0,0 +1,55 @@ + '\MSVC17', + '16' => '\MSVC16', + default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), + }; + ApplicationContext::set('gettext_win_vs_ver_dir', $vs_ver_dir); + } + + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + $vs_ver_dir = ApplicationContext::get('gettext_win_vs_ver_dir'); + $vcxproj = "{$lib->getSourceDir()}{$vs_ver_dir}\\libintl_static\\libintl_static.vcxproj"; + // libintl_static uses /MD (MultiThreadedDLL) in Release configs, which causes unresolved __imp_* symbols + // when linking into PHP statically. Patch to /MT (MultiThreaded) for static CRT compatibility. + FileSystem::replaceFileStr($vcxproj, 'MultiThreadedDLL', 'MultiThreaded'); + } + + #[BuildFor('Windows')] + public function build(LibraryPackage $lib): void + { + $vs_ver_dir = ApplicationContext::get('gettext_win_vs_ver_dir'); + cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}\\libintl_static") + ->exec('msbuild libintl_static.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=10.0'); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + // libintl_a.lib is the static library output; copy as libintl.lib for linker compatibility + FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\libintl_static\\x64\\Release\\libintl_a.lib", "{$lib->getLibDir()}\\libintl_a.lib"); + // libgnuintl.h is the public API header, installed as libintl.h + FileSystem::copy("{$lib->getSourceDir()}\\source\\gettext-runtime\\intl\\libgnuintl.h", "{$lib->getIncludeDir()}\\libintl.h"); + } +} From d7ee94686472900925bc94b64eada01977b14b4a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:12:13 +0800 Subject: [PATCH 558/682] Add mpir for windows gmp support --- config/pkg/ext/builtin-extensions.yml | 5 ++- config/pkg/lib/mpir.yml | 9 ++++++ src/Package/Library/mpir.php | 44 +++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 config/pkg/lib/mpir.yml create mode 100644 src/Package/Library/mpir.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 6b37a7d87..0709e27f0 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -82,10 +82,13 @@ ext-gettext: arg-type: with-path ext-gmp: type: php-extension - depends: + depends@unix: - gmp + depends@windows: + - mpir php-extension: arg-type: with-path + arg-type@windows: with ext-iconv: type: php-extension depends@unix: diff --git a/config/pkg/lib/mpir.yml b/config/pkg/lib/mpir.yml new file mode 100644 index 000000000..6fc8012f0 --- /dev/null +++ b/config/pkg/lib/mpir.yml @@ -0,0 +1,9 @@ +mpir: + type: library + artifact: + source: + type: git + url: 'https://github.com/winlibs/mpir.git' + rev: master + static-libs@windows: + - mpir_a.lib diff --git a/src/Package/Library/mpir.php b/src/Package/Library/mpir.php new file mode 100644 index 000000000..951d218ad --- /dev/null +++ b/src/Package/Library/mpir.php @@ -0,0 +1,44 @@ + '\build.vc17', + '16' => '\build.vc16', + default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), + }; + ApplicationContext::set('mpir_vs_ver_dir', $vs_ver_dir); + } + + #[BuildFor('Windows')] + public function build(LibraryPackage $lib): void + { + $vs_ver_dir = ApplicationContext::get('mpir_vs_ver_dir'); + cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}\\lib_mpir_gc") + ->exec('msbuild lib_mpir_gc.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64'); + FileSystem::createDir($lib->getLibDir()); + FileSystem::createDir($lib->getIncludeDir()); + FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\lib_mpir_gc\\x64\\Release\\mpir_a.lib", "{$lib->getLibDir()}\\mpir_a.lib"); + // mpir.h and gmp.h are generated by the prebuild step into the source root + FileSystem::copy("{$lib->getSourceDir()}\\mpir.h", "{$lib->getIncludeDir()}\\mpir.h"); + FileSystem::copy("{$lib->getSourceDir()}\\gmp.h", "{$lib->getIncludeDir()}\\gmp.h"); + } +} From 2bc0d05242fb5d5e1ae2ec9c80f1229386b5bd96 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:12:36 +0800 Subject: [PATCH 559/682] Add ext-gettext for Windows --- config/pkg/ext/builtin-extensions.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 0709e27f0..479f45ca4 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -76,10 +76,13 @@ ext-gd: arg-type: custom ext-gettext: type: php-extension - depends: + depends@unix: - gettext + depends@windows: + - gettext-win php-extension: arg-type: with-path + arg-type@windows: with ext-gmp: type: php-extension depends@unix: From a5a3a990bfa1d87f3b1dc879ccc6b9a7fd007730 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:13:21 +0800 Subject: [PATCH 560/682] Use legacy --enable-micro-win32 way to workaround --- src/Package/Target/php/windows.php | 8 +++++++- src/StaticPHP/Util/SourcePatcher.php | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index e81de3b65..47eb3c9f5 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -35,11 +35,17 @@ public function beforeBuildconfWin(TargetPackage $package): void } #[Stage] - public function buildconfForWindows(TargetPackage $package): void + public function buildconfForWindows(TargetPackage $package, PackageInstaller $installer): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); V2CompatLayer::emitPatchPoint('before-php-buildconf'); cmd()->cd($package->getSourceDir())->exec('.\buildconf.bat'); + + if ($package->getBuildOption('enable-micro-win32') && $installer->isPackageResolved('php-micro')) { + SourcePatcher::patchMicroWin32(); + } else { + SourcePatcher::unpatchMicroWin32(); + } } #[Stage] diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index b4e2e1c7b..70525e67f 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -196,6 +196,22 @@ public static function unpatchMicroPhar(): void FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); } + public static function patchMicroWin32(): void + { + // patch micro win32 + if (!file_exists(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak')) { + copy(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c', SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak'); + FileSystem::replaceFileStr(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c', '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1"); + } + } + + public static function unpatchMicroWin32(): void + { + if (file_exists(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak')) { + rename(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak', SOURCE_PATH . '\php-src\sapi\micro\php_micro.c'); + } + } + public static function patchPhpSrc(?array $items = null): bool { $patch_dir = ROOT_DIR . '/src/globals/patch/php-src-patches'; From 097af804a7a504619190bf5f95170e0091e626bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:13:44 +0800 Subject: [PATCH 561/682] Add OS support checks to PhpExtensionPackage --- src/StaticPHP/Package/PhpExtensionPackage.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index ca3dab739..6ee1edf55 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -61,6 +61,28 @@ public function getExtensionName(): string return str_replace('ext-', '', $this->getName()); } + /** + * Get the list of OS platforms that this extension supports. + * Returns an empty array if no restriction is defined (all platforms supported). + */ + public function getSupportedOSList(): array + { + return $this->extension_config['os'] ?? []; + } + + /** + * Check if this extension is supported on the current target OS. + * Returns true if no 'os' restriction is defined, or if the current OS is in the list. + */ + public function isSupportedOnCurrentOS(): bool + { + $osList = $this->getSupportedOSList(); + if (empty($osList)) { + return true; + } + return in_array(SystemTarget::getTargetOS(), $osList, true); + } + public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void { if ($os === '') { From c2072699981b550288eddc81ac218dd852c7b7a1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:13:59 +0800 Subject: [PATCH 562/682] Add php extension OS support checks to PackageInstaller --- src/StaticPHP/Package/PackageInstaller.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index a15ec4737..ac4aa2e01 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -755,6 +755,14 @@ private function printArrayInfo(array $info): void private function validatePackagesBeforeBuild(): void { foreach ($this->packages as $package) { + // Check OS support for php-extension packages + if ($package instanceof PhpExtensionPackage && !$package->isSupportedOnCurrentOS()) { + $supported = implode(', ', $package->getSupportedOSList()); + throw new WrongUsageException( + "Extension '{$package->getName()}' is not supported on current OS: " . SystemTarget::getTargetOS() . + ". Supported OS: [{$supported}]" + ); + } if ($package->getType() !== 'library') { continue; } From 9182cf1e348e01408019a220cdc4d10f781ef67d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:14:37 +0800 Subject: [PATCH 563/682] Add ext-glfw support for Windows --- config/pkg/lib/glfw.yml | 5 +++++ src/Package/Extension/glfw.php | 1 + src/Package/Library/glfw.php | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/config/pkg/lib/glfw.yml b/config/pkg/lib/glfw.yml index 13fba596e..f7d015492 100644 --- a/config/pkg/lib/glfw.yml +++ b/config/pkg/lib/glfw.yml @@ -1,6 +1,11 @@ glfw: type: library artifact: glfw + headers: + - GLFW/glfw3.h + - GLFW/glfw3native.h lang: cpp static-libs@unix: - libglfw3.a + static-libs@windows: + - glfw3.lib diff --git a/src/Package/Extension/glfw.php b/src/Package/Extension/glfw.php index 2a9c7ee51..8c73cb483 100644 --- a/src/Package/Extension/glfw.php +++ b/src/Package/Extension/glfw.php @@ -16,6 +16,7 @@ class glfw extends PhpExtensionPackage { #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-glfw')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-glfw')] #[PatchDescription('Patch glfw extension before buildconf')] public function patchBeforeBuildconf(): void { diff --git a/src/Package/Library/glfw.php b/src/Package/Library/glfw.php index 9348489cd..f4a261493 100644 --- a/src/Package/Library/glfw.php +++ b/src/Package/Library/glfw.php @@ -11,6 +11,7 @@ use StaticPHP\Exception\ValidationException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; @@ -97,4 +98,19 @@ public function buildForMac(LibraryPackage $lib): void // patch pkgconf $lib->patchPkgconfPrefix(['glfw3.pc']); } + + #[BuildFor('Windows')] + public function buildForWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->setWorkingDir("{$lib->getSourceDir()}/vendor/glfw") + ->setBuildDir("{$lib->getSourceDir()}/vendor/glfw") + ->setReset(false) + ->addConfigureArgs( + '-DGLFW_BUILD_EXAMPLES=OFF', + '-DGLFW_BUILD_TESTS=OFF', + '-DGLFW_BUILD_DOCS=OFF', + ) + ->build(); + } } From 2f260e4d09d908f13f1ffa55d3aa2fa1728bb385 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 8 Apr 2026 22:14:55 +0800 Subject: [PATCH 564/682] Add moveFileOrDir method to handle cross-device file and directory moves --- src/Package/Artifact/imagick.php | 31 +++++++++++++++++++++++++ src/StaticPHP/Util/FileSystem.php | 38 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/Package/Artifact/imagick.php diff --git a/src/Package/Artifact/imagick.php b/src/Package/Artifact/imagick.php new file mode 100644 index 000000000..568729e18 --- /dev/null +++ b/src/Package/Artifact/imagick.php @@ -0,0 +1,31 @@ +debug('Replacing file with type[' . $replace_type . ']: ' . $filename); From 105f0328e6405735e1adf27dadb5788375d2ff74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 8 Apr 2026 21:33:59 +0200 Subject: [PATCH 565/682] fix(xlswriter): convert K&R function declaration to ANSI C in bundled minizip The bundled minizip in xlswriter (pinned at libxlsxwriter RELEASE_1.0.0) uses a K&R-style function declaration in mztools.c. Modern Clang on macOS (defaulting to C23) rejects this as a hard syntax error since K&R declarations were removed from the C23 standard. The upstream libxlsxwriter has already fixed this on their main branch. This patch applies the same fix during the build process. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/SPC/builder/extension/xlswriter.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/SPC/builder/extension/xlswriter.php b/src/SPC/builder/extension/xlswriter.php index 24d32d947..7d0350fcc 100644 --- a/src/SPC/builder/extension/xlswriter.php +++ b/src/SPC/builder/extension/xlswriter.php @@ -5,6 +5,7 @@ namespace SPC\builder\extension; use SPC\builder\Extension; +use SPC\store\FileSystem; use SPC\store\SourcePatcher; use SPC\util\CustomExt; @@ -28,6 +29,18 @@ public function getWindowsConfigureArg(bool $shared = false): string public function patchBeforeMake(): bool { $patched = parent::patchBeforeMake(); + + // Fix K&R C function declaration in bundled minizip rejected by modern Clang (C23 default) + $mztools = $this->source_dir . '/library/libxlsxwriter/third_party/minizip/mztools.c'; + if (file_exists($mztools)) { + FileSystem::replaceFileStr( + $mztools, + "extern int ZEXPORT unzRepair(file, fileOut, fileOutTmp, nRecovered, bytesRecovered)\nconst char* file;\nconst char* fileOut;\nconst char* fileOutTmp;\nuLong* nRecovered;\nuLong* bytesRecovered;\n{", + "extern int ZEXPORT unzRepair(const char* file, const char* fileOut, const char* fileOutTmp, uLong* nRecovered, uLong* bytesRecovered)\n{" + ); + $patched = true; + } + if (PHP_OS_FAMILY === 'Windows') { // fix windows build with openssl extension duplicate symbol bug SourcePatcher::patchFile('spc_fix_xlswriter_win32.patch', $this->source_dir); From 9b8e0c794a4da5c98386e77168ef291c2761c768 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:07:09 +0800 Subject: [PATCH 566/682] Update getBinaryExtractConfig to handle 'hosted' cache extraction path --- src/StaticPHP/Artifact/Artifact.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 0b5d8a6de..e241d4fe8 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -347,7 +347,11 @@ public function getSourceRoot(): string public function getBinaryExtractConfig(array $cache_info = []): array { if (is_string($cache_info['extract'] ?? null)) { - return ['path' => $this->replaceExtractPathVariables($cache_info['extract']), 'mode' => 'standard']; + $cache_extract = $cache_info['extract']; + if ($cache_extract === 'hosted') { + return ['path' => BUILD_ROOT_PATH, 'mode' => 'standard']; + } + return ['path' => $this->replaceExtractPathVariables($cache_extract), 'mode' => 'standard']; } $platform = SystemTarget::getCurrentPlatformString(); From 1bee20ac61f534908c9a483f4d7d925ecd7df327 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:07:21 +0800 Subject: [PATCH 567/682] Add Windows build configuration support for opcache extension --- src/Package/Extension/opcache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Extension/opcache.php b/src/Package/Extension/opcache.php index 93cb0a9ff..07758de26 100644 --- a/src/Package/Extension/opcache.php +++ b/src/Package/Extension/opcache.php @@ -29,6 +29,7 @@ public function validate(): void } #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-opcache')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-opcache')] #[PatchDescription('Fix static opcache build for PHP 8.2.0 to 8.4.x')] public function patchBeforeBuildconf(PackageInstaller $installer): bool { From ee2e887625b3def40c2c3fa04838bd12250b20e3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:19:12 +0800 Subject: [PATCH 568/682] Add ext-gd, ext-iconv, ext-intl for windows --- config/pkg/ext/builtin-extensions.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 479f45ca4..5177eb4ef 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -74,6 +74,7 @@ ext-gd: - freetype php-extension: arg-type: custom + arg-type@windows: with ext-gettext: type: php-extension depends@unix: @@ -96,12 +97,14 @@ ext-iconv: type: php-extension depends@unix: - libiconv + depends@windows: + - libiconv-win php-extension: arg-type@unix: with-path arg-type@windows: with ext-intl: type: php-extension - depends@unix: + depends: - icu ext-ldap: type: php-extension @@ -113,6 +116,7 @@ ext-ldap: - ext-openssl php-extension: arg-type: with-path + arg-type@windows: with ext-libxml: type: php-extension depends: From 4d73af45c2a18544e1fdc6a8bd2b6110c830bf89 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:19:53 +0800 Subject: [PATCH 569/682] Add ext-odbc, ext-pdo_odbc for windows --- config/pkg/ext/builtin-extensions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 5177eb4ef..ccde23143 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -161,6 +161,7 @@ ext-odbc: - unixodbc php-extension: arg-type@unix: '--with-unixODBC@shared_path_suffix@' + arg-type@windows: enable ext-opcache: type: php-extension php-extension: @@ -201,7 +202,6 @@ ext-pdo_odbc: type: php-extension depends: - ext-pdo - - ext-odbc depends@unix: - unixodbc - ext-pdo From 6630fbdce8085104a3b2c60a43e524be453f5933 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:40:03 +0800 Subject: [PATCH 570/682] Fix sqlsrv build configuration for Windows by removing /sdl, /W4, and /WX flags --- src/Package/Extension/sqlsrv.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Package/Extension/sqlsrv.php b/src/Package/Extension/sqlsrv.php index 39f64f5eb..b0abf4070 100644 --- a/src/Package/Extension/sqlsrv.php +++ b/src/Package/Extension/sqlsrv.php @@ -10,16 +10,28 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\FileSystem; #[Extension('sqlsrv')] class sqlsrv extends PhpExtensionPackage { + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-sqlsrv')] + #[PatchDescription('Remove /sdl /W4 /WX flags from sqlsrv config.w32 to prevent strict compilation failures on Windows (these flags get merged into STATIC_EXT_CFLAGS and applied to Zend engine files)')] + public function patchBeforeBuildconfForWindows(): void + { + // Fix the compilation issue of sqlsrv on Windows (/sdl causes C4703 to be treated as errors in Zend files) + if (file_exists(SOURCE_PATH . '/php-src/ext/sqlsrv/config.w32')) { + FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/sqlsrv/config.w32', '/sdl /W4 /WX', ''); + } + } + #[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-sqlsrv')] - #[PatchDescription('Fix sqlsrv Makefile: remove /W4 and /WX flags to prevent build errors on Windows')] + #[PatchDescription('Fix sqlsrv Makefile: remove /sdl /W4 /WX flags to prevent build errors on Windows')] public function patchBeforeMake(PackageInstaller $installer): bool { $makefile = $installer->getTargetPackage('php')->getSourceDir() . '\Makefile'; $makeContent = file_get_contents($makefile); + $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/sdl\b/m', '$1', $makeContent); $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/W4\b/m', '$1', $makeContent); $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/WX\b/m', '$1', $makeContent); file_put_contents($makefile, $makeContent); From 39f6a628da7780350a0c8e595f4459c089074b9b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:40:34 +0800 Subject: [PATCH 571/682] Limit password-argon2, pcntl to unix only --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index ccde23143..14013f183 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -185,10 +185,17 @@ ext-password-argon2: - libargon2 - ext-openssl php-extension: + os: + - Linux + - Darwin arg-type: custom display-name: '' ext-pcntl: type: php-extension + php-extension: + os: + - Linux + - Darwin ext-pdo: type: php-extension ext-pdo_mysql: From 402e105b6ba1afcc2bc37ba0d4a33878569963f4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:40:55 +0800 Subject: [PATCH 572/682] Add ext-pgsql, ext-pdo_pgsql --- config/pkg/ext/builtin-extensions.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 14013f183..4c6af9f44 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -217,7 +217,7 @@ ext-pdo_odbc: arg-type: custom ext-pdo_pgsql: type: php-extension - depends@unix: + depends: - ext-pdo - ext-pgsql - postgresql @@ -234,7 +234,7 @@ ext-pdo_sqlite: arg-type: with ext-pgsql: type: php-extension - depends@unix: + depends: - postgresql php-extension: arg-type: custom From 3805c06caacf6b02bfff81d3b52d8bac6223a339 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:41:10 +0800 Subject: [PATCH 573/682] Limit posix to unix-only --- config/pkg/ext/builtin-extensions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 4c6af9f44..de002b931 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -244,6 +244,10 @@ ext-phar: - zlib ext-posix: type: php-extension + php-extension: + os: + - Linux + - Darwin ext-readline: type: php-extension depends: From 631549073a3dec1d4394fd0a934bb7fdfd0a4901 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:50:59 +0800 Subject: [PATCH 574/682] Add wineditline --- config/pkg/lib/wineditline.yml | 14 ++++++++++++++ src/Package/Library/wineditline.php | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/lib/wineditline.yml create mode 100644 src/Package/Library/wineditline.php diff --git a/config/pkg/lib/wineditline.yml b/config/pkg/lib/wineditline.yml new file mode 100644 index 000000000..92f80ba2a --- /dev/null +++ b/config/pkg/lib/wineditline.yml @@ -0,0 +1,14 @@ +wineditline: + type: library + artifact: + source: + type: git + url: 'https://github.com/winlibs/wineditline.git' + rev: master + metadata: + license-files: [COPYING] + license: GPL-2.0-or-later + headers: + - editline + static-libs@windows: + - edit_a.lib diff --git a/src/Package/Library/wineditline.php b/src/Package/Library/wineditline.php new file mode 100644 index 000000000..f8ea67675 --- /dev/null +++ b/src/Package/Library/wineditline.php @@ -0,0 +1,23 @@ +build(); + FileSystem::copy($lib->getSourceDir() . '\lib64\edit_a.lib', $lib->getLibDir() . '\edit_a.lib'); + FileSystem::copyDir($lib->getSourceDir() . '\include\editline', $lib->getIncludeDir() . '\editline'); + } +} From 2256f47aed0b7eecfe9e995f807ca3ccf1b033df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 10:51:12 +0800 Subject: [PATCH 575/682] Add ext-readline for windows --- config/pkg/ext/builtin-extensions.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index de002b931..3d3205936 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -250,13 +250,13 @@ ext-posix: - Darwin ext-readline: type: php-extension - depends: + depends@unix: - libedit + depends@windows: + - wineditline php-extension: - support: - Windows: wip - BSD: wip arg-type: '--with-libedit --without-readline' + arg-type@windows: with build-shared: false build-static: true ext-session: From f1bd64ec0677b8646c04eae488fbf9eeac727c0e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:15:24 +0800 Subject: [PATCH 576/682] Add tidy for Windows --- src/Package/Library/tidy.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Package/Library/tidy.php b/src/Package/Library/tidy.php index b59160262..5e16b2d18 100644 --- a/src/Package/Library/tidy.php +++ b/src/Package/Library/tidy.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('tidy')] class tidy @@ -28,4 +30,18 @@ public function buildUnix(LibraryPackage $lib): void $cmake->build(); $lib->patchPkgconfPrefix(['tidy.pc']); } + + #[BuildFor('Windows')] + public function buildWindows(LibraryPackage $lib): void + { + $cmake = WindowsCMakeExecutor::create($lib) + ->setBuildDir("{$lib->getSourceDir()}/build-dir") + ->addConfigureArgs( + '-DSUPPORT_CONSOLE_APP=OFF', + '-DBUILD_SHARED_LIB=OFF' + )->build(); + + // rename tidy_static.lib to tidy_a.lib + FileSystem::moveFileOrDir($lib->getLibDir() . '\tidy_static.lib', $lib->getLibDir() . '\tidy_a.lib'); + } } From 6ed35eaa85fc978743f99f119ad589e91ddaf5b4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:15:30 +0800 Subject: [PATCH 577/682] Add tidy for Windows --- config/pkg/lib/tidy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/lib/tidy.yml b/config/pkg/lib/tidy.yml index 41487c1d3..a58a60d66 100644 --- a/config/pkg/lib/tidy.yml +++ b/config/pkg/lib/tidy.yml @@ -10,3 +10,5 @@ tidy: license: W3C static-libs@unix: - libtidy.a + static-libs@windows: + - tidy_a.lib From 1a027003b169cface64e8e855c5a73ee791d2f76 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:15:45 +0800 Subject: [PATCH 578/682] Add libxslt for Windows --- config/pkg/lib/libxslt.yml | 3 +++ src/Package/Library/libxslt.php | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/config/pkg/lib/libxslt.yml b/config/pkg/lib/libxslt.yml index 07955333a..a9898648a 100644 --- a/config/pkg/lib/libxslt.yml +++ b/config/pkg/lib/libxslt.yml @@ -13,3 +13,6 @@ libxslt: static-libs@unix: - libxslt.a - libexslt.a + static-libs@windows: + - libxslt_a.lib + - libexslt_a.lib diff --git a/src/Package/Library/libxslt.php b/src/Package/Library/libxslt.php index 11ba2bf84..7513d1468 100644 --- a/src/Package/Library/libxslt.php +++ b/src/Package/Library/libxslt.php @@ -9,7 +9,9 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\SPCConfigUtil; #[Library('libxslt')] @@ -49,4 +51,20 @@ public function buildUnix(LibraryPackage $lib, PackageInstaller $installer): voi ->exec("{$AR} -t libxslt.a | grep '\\.a$' | xargs -n1 {$AR} d libxslt.a") ->exec("{$AR} -t libexslt.a | grep '\\.a$' | xargs -n1 {$AR} d libexslt.a"); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib, PackageInstaller $installer): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DBUILD_SHARED_LIBS=OFF', + '-DLIBXSLT_WITH_PROFILER=OFF', + '-DLIBXSLT_WITH_PROGRAMS=OFF', + '-DLIBXSLT_WITH_PYTHON=OFF', + '-DLIBXSLT_WITH_TESTS=OFF', + ) + ->build(); + FileSystem::copy($lib->getLibDir() . '\libxslts.lib', $lib->getLibDir() . '\libxslt_a.lib'); + FileSystem::copy($lib->getLibDir() . '\libexslts.lib', $lib->getLibDir() . '\libexslt_a.lib'); + } } From b45e64ec167ecdee15a1f25f57983bad755be33c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:15:56 +0800 Subject: [PATCH 579/682] Add ext-swow for Windows --- src/Package/Extension/swow.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Extension/swow.php b/src/Package/Extension/swow.php index 333a3ed7b..884f0217a 100644 --- a/src/Package/Extension/swow.php +++ b/src/Package/Extension/swow.php @@ -17,6 +17,7 @@ class swow extends PhpExtensionPackage { #[CustomPhpConfigureArg('Darwin')] #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Windows')] public function configureArg(PackageInstaller $installer): string { $arg = '--enable-swow'; From e25d95c26b1ba6df7d344c6faafbfdf466bf20b5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:16:30 +0800 Subject: [PATCH 580/682] Add ext-soap for Windows --- config/pkg/ext/builtin-extensions.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 3d3205936..01a23caf9 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -285,8 +285,7 @@ ext-soap: - ext-xml - ext-session php-extension: - arg-type@unix: enable - arg-type@windows: with + arg-type: enable build-with-php: true ext-sockets: type: php-extension From 28e102100f04830e03b724b0b9db15fee1b29f2c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:17:00 +0800 Subject: [PATCH 581/682] Refactor support for sysv extensions and tidy for Windows --- config/pkg/ext/builtin-extensions.yml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 01a23caf9..d3769dead 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -306,28 +306,22 @@ ext-sqlite3: ext-sysvmsg: type: php-extension php-extension: - support: - Windows: 'no' - BSD: wip + os: + - Linux + - Darwin ext-sysvsem: type: php-extension php-extension: - support: - Windows: 'no' - BSD: wip + os: + - Linux + - Darwin ext-sysvshm: type: php-extension - php-extension: - support: - BSD: wip ext-tidy: type: php-extension depends: - tidy php-extension: - support: - Windows: wip - BSD: wip arg-type: with-path ext-tokenizer: type: php-extension From cd14f6253bff0035a4667cd0b0f82197c8b5fc20 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 12:25:12 +0800 Subject: [PATCH 582/682] Add ext-zip for Windows --- config/pkg/ext/ext-zip.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/pkg/ext/ext-zip.yml b/config/pkg/ext/ext-zip.yml index a5a9e4b54..9a4b0282b 100644 --- a/config/pkg/ext/ext-zip.yml +++ b/config/pkg/ext/ext-zip.yml @@ -8,10 +8,8 @@ ext-zip: metadata: license-files: [LICENSE] license: PHP-3.01 - depends@unix: + depends: - libzip php-extension: - support: - BSD: wip arg-type: custom arg-type@windows: enable From 4d2036f20ea693f34f667421793fb1f66d200ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 9 Apr 2026 08:49:49 +0200 Subject: [PATCH 583/682] fix(xlswriter): use -std=gnu17 to fix K&R declarations rejected by C23 The bundled minizip in xlswriter has K&R C function declarations in multiple files (mztools.c, ioapi.c). Apple Clang (Xcode 16+) defaults to C23, which removed K&R from the standard, causing hard build errors. Instead of patching individual files, downgrade the C standard to gnu17 for the whole build when xlswriter is enabled. This covers all K&R occurrences in the bundled code. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/SPC/builder/extension/xlswriter.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/SPC/builder/extension/xlswriter.php b/src/SPC/builder/extension/xlswriter.php index 7d0350fcc..cb5c82bc1 100644 --- a/src/SPC/builder/extension/xlswriter.php +++ b/src/SPC/builder/extension/xlswriter.php @@ -5,9 +5,9 @@ namespace SPC\builder\extension; use SPC\builder\Extension; -use SPC\store\FileSystem; use SPC\store\SourcePatcher; use SPC\util\CustomExt; +use SPC\util\GlobalEnvManager; #[CustomExt('xlswriter')] class xlswriter extends Extension @@ -30,14 +30,9 @@ public function patchBeforeMake(): bool { $patched = parent::patchBeforeMake(); - // Fix K&R C function declaration in bundled minizip rejected by modern Clang (C23 default) - $mztools = $this->source_dir . '/library/libxlsxwriter/third_party/minizip/mztools.c'; - if (file_exists($mztools)) { - FileSystem::replaceFileStr( - $mztools, - "extern int ZEXPORT unzRepair(file, fileOut, fileOutTmp, nRecovered, bytesRecovered)\nconst char* file;\nconst char* fileOut;\nconst char* fileOutTmp;\nuLong* nRecovered;\nuLong* bytesRecovered;\n{", - "extern int ZEXPORT unzRepair(const char* file, const char* fileOut, const char* fileOutTmp, uLong* nRecovered, uLong* bytesRecovered)\n{" - ); + // Bundled minizip uses K&R C function declarations rejected by C23 (default on macOS with Xcode 16+) + if (PHP_OS_FAMILY !== 'Windows') { + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -std=gnu17'); $patched = true; } @@ -53,4 +48,9 @@ public function patchBeforeMake(): bool } return $patched; } + + protected function getExtraEnv(): array + { + return ['CFLAGS' => '-std=gnu17']; + } } From fb7730989c08119326f23ea2d338042f782860c4 Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 9 Apr 2026 14:00:10 +0700 Subject: [PATCH 584/682] Apply suggestions from code review Co-authored-by: Marc --- src/SPC/builder/extension/xlswriter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/xlswriter.php b/src/SPC/builder/extension/xlswriter.php index cb5c82bc1..8ef826d00 100644 --- a/src/SPC/builder/extension/xlswriter.php +++ b/src/SPC/builder/extension/xlswriter.php @@ -30,7 +30,7 @@ public function patchBeforeMake(): bool { $patched = parent::patchBeforeMake(); - // Bundled minizip uses K&R C function declarations rejected by C23 (default on macOS with Xcode 16+) + // Remove when https://github.com/viest/php-ext-xlswriter/pull/560 is merged if (PHP_OS_FAMILY !== 'Windows') { GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -std=gnu17'); $patched = true; @@ -49,6 +49,7 @@ public function patchBeforeMake(): bool return $patched; } + // Remove when https://github.com/viest/php-ext-xlswriter/pull/560 is merged protected function getExtraEnv(): array { return ['CFLAGS' => '-std=gnu17']; From 1d0ccdec45878dac7cd57c8c222560c1bb152589 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 15:16:44 +0800 Subject: [PATCH 585/682] Refactor micro:combine command on v3 --- TODO.md | 57 +++++++++ src/StaticPHP/Command/MicroCombineCommand.php | 120 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 3 files changed, 179 insertions(+) create mode 100644 TODO.md create mode 100644 src/StaticPHP/Command/MicroCombineCommand.php diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..52d6133a9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,57 @@ +# v3 TODO List + +Tracking items identified during the v2 → v3 migration audit. + +--- + +## Commands + +- [ ] Implement `craft` command (drives full build from `craft.yml`; should be easier with v3 vendor/registry mode) +- [x] Migrate `micro:combine` command (combine `micro.sfx` with PHP code + INI injection) +- [ ] Implement `dump-extensions` command (extract required extensions from `composer.json` / `composer.lock`) +- [ ] Design and implement v3 dev toolchain commands (WIP — needs design decision): + - [ ] `dev:extensions` / equivalent listing command + - [ ] `dev:php-version`, `dev:ext-version`, `dev:lib-version` + - [ ] Doc generation commands (`dev:gen-ext-docs`, `dev:gen-ext-dep-docs`, `dev:gen-lib-dep-docs`) — pending v3 doc design + +--- + +## Source Patches (SourcePatcher → Artifact migration) + +The following v2 `SourcePatcher` hooks are not yet migrated to v3 `src/Package/Artifact/` classes: + +- [ ] Migrate `patchSQLSRVWin32` — removes `/sdl` compile flag to prevent Zend build failure on Windows +- [ ] Migrate `patchSQLSRVPhp85` — fixes `pdo_sqlsrv` directory layout for PHP 8.5 +- [ ] Migrate `patchYamlWin32` — patches `config.w32` `_a.lib` detection logic for the `yaml` extension +- [ ] Migrate `patchImagickWith84` — applies PHP 8.4 compatibility patch for `imagick` based on version detection + +--- + +## Extension Package Classes (Unix) + +Extensions that had non-trivial v2 build logic and are missing a v3 `src/Package/Extension/` class: + +- [x] `gettext` — macOS: fix `config.m4` bracket syntax for cross-version compatibility + append frameworks to linker flags (critical for macOS linking; this is a Unix-side gap, not Windows-only) + +--- + +## Windows Extensions (Early Stage) + +Windows extension support is still in early stage. The following extensions had Windows-specific configure args or patches in v2 and are pending v3 Windows implementation: + +- [ ] `amqp` — Windows configure args +- [ ] `com_dotnet` — Windows-only extension +- [ ] `dom` — remove `dllmain.c` from `config.w32` +- [ ] `ev` — fix `PHP_EV_SHARED` in `config.w32` +- [ ] `gmssl` — add `CHECK_LIB("gmssl.lib")` to `config.w32` +- [ ] `intl` — fix `PHP_INTL_SHARED` in `config.w32` +- [ ] `lz4` — Windows configure args +- [ ] `mbregex` — Windows configure args +- [ ] `sqlsrv` / `pdo_sqlsrv` — complex conditional build logic (independent `sqlsrv` without `pdo_sqlsrv`) +- [ ] `xml` — remove `dllmain.c` from `config.w32`; handles `soap`, `xmlreader`, `xmlwriter`, `simplexml` + +--- + +## Documentation + +- [ ] Write v3 user documentation (currently zero v3 docs) diff --git a/src/StaticPHP/Command/MicroCombineCommand.php b/src/StaticPHP/Command/MicroCombineCommand.php new file mode 100644 index 000000000..b46d995f7 --- /dev/null +++ b/src/StaticPHP/Command/MicroCombineCommand.php @@ -0,0 +1,120 @@ +addArgument('file', InputArgument::REQUIRED, 'The php or phar file to be combined'); + $this->addOption('with-micro', 'M', InputOption::VALUE_REQUIRED, 'Customize your micro.sfx file'); + $this->addOption('with-ini-set', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'ini to inject into micro.sfx when combining'); + $this->addOption('with-ini-file', 'N', InputOption::VALUE_REQUIRED, 'ini file to inject into micro.sfx when combining'); + $this->addOption('output', 'O', InputOption::VALUE_REQUIRED, 'Customize your output binary file name'); + } + + public function handle(): int + { + // 0. Initialize path variables + $internal = FileSystem::convertPath(BUILD_ROOT_PATH . '/bin/micro.sfx'); + $micro_file = $this->getOption('with-micro'); + $file = $this->getArgument('file'); + $ini_set = $this->getOption('with-ini-set'); + $ini_file = $this->getOption('with-ini-file'); + $target_ini = []; + $output = $this->getOption('output') ?? 'my-app'; + $ini_part = ''; + // 1. Make sure specified micro.sfx file exists + if ($micro_file !== null && !file_exists($micro_file)) { + $this->output->writeln('The micro.sfx file you specified is incorrect or does not exist!'); + return static::FAILURE; + } + // 2. Make sure buildroot/bin/micro.sfx exists + if ($micro_file === null && !file_exists($internal)) { + $this->output->writeln('You haven\'t compiled micro.sfx yet, please use "build" command and "--build-micro" to compile phpmicro first!'); + return static::FAILURE; + } + // 3. Use buildroot/bin/micro.sfx + if ($micro_file === null) { + $micro_file = $internal; + } + // 4. Make sure php or phar file exists + if (!is_file(FileSystem::convertPath($file))) { + $this->output->writeln('The file to combine does not exist!'); + return static::FAILURE; + } + // 5. Confirm ini files (ini-set has higher priority) + if ($ini_file !== null) { + // Check file exist first + if (!file_exists($ini_file)) { + $this->output->writeln('The ini file to combine does not exist! (' . $ini_file . ')'); + return static::FAILURE; + } + $arr = parse_ini_file($ini_file); + if ($arr === false) { + $this->output->writeln('Cannot parse ini file'); + return static::FAILURE; + } + $target_ini = array_merge($target_ini, $arr); + } + // 6. Confirm ini sets + if ($ini_set !== []) { + foreach ($ini_set as $item) { + $arr = parse_ini_string($item); + if ($arr === false) { + $this->output->writeln('--with-ini-set parse failed'); + return static::FAILURE; + } + $target_ini = array_merge($target_ini, $arr); + } + } + // 7. Generate ini injection parts + if (!empty($target_ini)) { + $ini_str = $this->encodeINI($target_ini); + logger()->debug('Injecting ini parts: ' . PHP_EOL . $ini_str); + $ini_part = "\xfd\xf6\x69\xe6"; + $ini_part .= pack('N', strlen($ini_str)); + $ini_part .= $ini_str; + } + // 8. Combine ! + $output = FileSystem::isRelativePath($output) ? (WORKING_DIR . '/' . $output) : $output; + $file_target = file_get_contents($micro_file) . $ini_part . file_get_contents($file); + if (PHP_OS_FAMILY === 'Windows' && !str_ends_with(strtolower($output), '.exe')) { + $output .= '.exe'; + } + $output = FileSystem::convertPath($output); + $result = file_put_contents($output, $file_target); + if ($result === false) { + $this->output->writeln('Combine failed.'); + return static::FAILURE; + } + // 9. chmod +x + chmod($output, 0755); + $this->output->writeln('Combine success! Binary file: ' . $output . ''); + return static::SUCCESS; + } + + private function encodeINI(array $array): string + { + $res = []; + foreach ($array as $key => $val) { + if (is_array($val)) { + $res[] = "[{$key}]"; + foreach ($val as $skey => $sval) { + $res[] = "{$skey}=" . (is_numeric($sval) ? $sval : '"' . $sval . '"'); + } + } else { + $res[] = "{$key}=" . (is_numeric($val) ? $val : '"' . $val . '"'); + } + } + return implode("\n", $res); + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 2882b5741..0c8fec832 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -20,6 +20,7 @@ use StaticPHP\Command\DumpLicenseCommand; use StaticPHP\Command\ExtractCommand; use StaticPHP\Command\InstallPackageCommand; +use StaticPHP\Command\MicroCombineCommand; use StaticPHP\Command\ResetCommand; use StaticPHP\Command\SPCConfigCommand; use StaticPHP\Package\TargetPackage; @@ -65,6 +66,7 @@ public function __construct() new DumpLicenseCommand(), new ResetCommand(), new CheckUpdateCommand(), + new MicroCombineCommand(), // dev commands new ShellCommand(), From 9d777ca650a3d6da510dbf2e78b92c239a9459f9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 9 Apr 2026 23:37:36 +0800 Subject: [PATCH 586/682] fix(icu_static_win): update paths for ICU static libraries and includes --- src/SPC/ConsoleApplication.php | 2 +- src/SPC/builder/windows/library/icu_static_win.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 79f7d724a..9685a1d88 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.4'; + public const string VERSION = '2.8.5'; public function __construct() { diff --git a/src/SPC/builder/windows/library/icu_static_win.php b/src/SPC/builder/windows/library/icu_static_win.php index c152c6e7d..fdd9e6cd4 100644 --- a/src/SPC/builder/windows/library/icu_static_win.php +++ b/src/SPC/builder/windows/library/icu_static_win.php @@ -12,16 +12,16 @@ class icu_static_win extends WindowsLibraryBase protected function build(): void { - copy("{$this->source_dir}\\x64-windows-static\\lib\\icudt.lib", "{$this->getLibDir()}\\icudt.lib"); - copy("{$this->source_dir}\\x64-windows-static\\lib\\icuin.lib", "{$this->getLibDir()}\\icuin.lib"); - copy("{$this->source_dir}\\x64-windows-static\\lib\\icuio.lib", "{$this->getLibDir()}\\icuio.lib"); - copy("{$this->source_dir}\\x64-windows-static\\lib\\icuuc.lib", "{$this->getLibDir()}\\icuuc.lib"); + copy("{$this->source_dir}\\lib\\icudt.lib", "{$this->getLibDir()}\\icudt.lib"); + copy("{$this->source_dir}\\lib\\icuin.lib", "{$this->getLibDir()}\\icuin.lib"); + copy("{$this->source_dir}\\lib\\icuio.lib", "{$this->getLibDir()}\\icuio.lib"); + copy("{$this->source_dir}\\lib\\icuuc.lib", "{$this->getLibDir()}\\icuuc.lib"); // create libpq folder in buildroot/includes/libpq if (!file_exists("{$this->getIncludeDir()}\\unicode")) { mkdir("{$this->getIncludeDir()}\\unicode"); } - FileSystem::copyDir("{$this->source_dir}\\x64-windows-static\\include\\unicode", "{$this->getIncludeDir()}\\unicode"); + FileSystem::copyDir("{$this->source_dir}\\include\\unicode", "{$this->getIncludeDir()}\\unicode"); } } From 6b62255091827573434cffb13edf3010263fb21e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 12:21:21 +0800 Subject: [PATCH 587/682] test --- src/globals/test-extensions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 7ae79aca2..f7abee9fd 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -23,15 +23,15 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-15-intel', // bin/spc for x86_64 - 'macos-15', // bin/spc for arm64 + // 'macos-15-intel', // bin/spc for x86_64 + // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 // 'ubuntu-24.04', // bin/spc for x86_64 // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - // 'windows-2025', + 'windows-2025', ]; // whether enable thread safe @@ -51,7 +51,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { 'Linux', 'Darwin' => 'sqlsrv,pdo_sqlsrv', - 'Windows' => 'amqp,apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,ds,exif,ffi,fileinfo,filter,ftp,gd,iconv,igbinary,libxml,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pdo,pdo_mysql,pdo_sqlite,pdo_sqlsrv,phar,rar,redis,session,shmop,simdjson,simplexml,soap,sockets,sqlite3,sqlsrv,ssh2,sysvshm,tokenizer,xml,xmlreader,xmlwriter,yac,yaml,zip,zlib', + 'Windows' => 'intl', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). From 7fc5dd428d7ce6e04ef45a6a5523faa0e418f646 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 12:24:25 +0800 Subject: [PATCH 588/682] Fix krb5 CI build by the way --- src/SPC/builder/unix/library/krb5.php | 4 +++- src/globals/test-extensions.php | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/SPC/builder/unix/library/krb5.php b/src/SPC/builder/unix/library/krb5.php index 798346459..3665a054c 100644 --- a/src/SPC/builder/unix/library/krb5.php +++ b/src/SPC/builder/unix/library/krb5.php @@ -13,7 +13,9 @@ protected function build(): void { $origin_source_dir = $this->source_dir; $this->source_dir .= '/src'; - shell()->cd($this->source_dir)->exec('autoreconf -if'); + if (!file_exists($this->source_dir . '/configure')) { + shell()->cd($this->source_dir)->exec('autoreconf -if'); + } $libs = array_map(fn ($x) => $x->getName(), $this->getDependencies(true)); $spc = new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]); $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false)); diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index f7abee9fd..58eeaa523 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -23,8 +23,8 @@ // test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - // 'macos-15-intel', // bin/spc for x86_64 - // 'macos-15', // bin/spc for arm64 + 'macos-15-intel', // bin/spc for x86_64 + 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 // 'ubuntu-24.04', // bin/spc for x86_64 @@ -50,7 +50,7 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'sqlsrv,pdo_sqlsrv', + 'Linux', 'Darwin' => 'curl', 'Windows' => 'intl', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => '', + 'Linux', 'Darwin' => 'krb5', 'Windows' => '', }; From da49c056c94468d51f80adff288a19d5d1d13833 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 12:49:10 +0800 Subject: [PATCH 589/682] test --- src/SPC/builder/unix/library/krb5.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/library/krb5.php b/src/SPC/builder/unix/library/krb5.php index 3665a054c..4cf0ad44e 100644 --- a/src/SPC/builder/unix/library/krb5.php +++ b/src/SPC/builder/unix/library/krb5.php @@ -13,6 +13,7 @@ protected function build(): void { $origin_source_dir = $this->source_dir; $this->source_dir .= '/src'; + shell()->cd($this->source_dir)->exec('ls -lah'); if (!file_exists($this->source_dir . '/configure')) { shell()->cd($this->source_dir)->exec('autoreconf -if'); } From b04b07926758bddf826da649d401967a1e2b857e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 13:22:06 +0800 Subject: [PATCH 590/682] test --- .github/workflows/tests.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 132b33f00..8d91397ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -180,6 +180,13 @@ jobs: - name: "Run Build Tests (doctor)" run: php src/globals/test-extensions.php doctor_cmd ${{ matrix.os }} ${{ matrix.php }} + - name: "ACLOCAL" + run: | + ls -lah /opt/homebrew/share/aclocal + autoconf --version + aclocal --print-ac-dir + echo $ACLOCAL_PATH + - name: "Prepare UPX for Windows" if: ${{ startsWith(matrix.os, 'windows-') }} run: | From 544fd15c0b6ba58dffbc95a9f88cc0fb0fa7f233 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 13:26:14 +0800 Subject: [PATCH 591/682] test tmate --- .github/workflows/tests.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8d91397ae..0e065b147 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -180,13 +180,6 @@ jobs: - name: "Run Build Tests (doctor)" run: php src/globals/test-extensions.php doctor_cmd ${{ matrix.os }} ${{ matrix.php }} - - name: "ACLOCAL" - run: | - ls -lah /opt/homebrew/share/aclocal - autoconf --version - aclocal --print-ac-dir - echo $ACLOCAL_PATH - - name: "Prepare UPX for Windows" if: ${{ startsWith(matrix.os, 'windows-') }} run: | @@ -216,6 +209,6 @@ jobs: name: build-logs-${{ matrix.os }}-${{ matrix.php }} path: log -# - name: Setup tmate session -# if: ${{ failure() }} -# uses: mxschmitt/action-tmate@v3 + - name: Setup tmate session + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 From cf48d131b37a9d1e209125e620709b1b7f41a63a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 13:52:21 +0800 Subject: [PATCH 592/682] Use direct link to official release instead of mirror --- .github/workflows/tests.yml | 6 +++--- config/source.json | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0e065b147..132b33f00 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -209,6 +209,6 @@ jobs: name: build-logs-${{ matrix.os }}-${{ matrix.php }} path: log - - name: Setup tmate session - if: ${{ failure() }} - uses: mxschmitt/action-tmate@v3 +# - name: Setup tmate session +# if: ${{ failure() }} +# uses: mxschmitt/action-tmate@v3 diff --git a/config/source.json b/config/source.json index 4b3f7b594..18bca217b 100644 --- a/config/source.json +++ b/config/source.json @@ -462,10 +462,8 @@ } }, "krb5": { - "type": "ghtagtar", - "repo": "krb5/krb5", - "match": "krb5.+-final", - "prefer-stable": true, + "type": "url", + "url": "https://web.mit.edu/kerberos/dist/krb5/1.22/krb5-1.22.2.tar.gz", "license": { "type": "file", "path": "NOTICE" From 26a14bccbe6ed8c0f4271ceade3095444f906ca1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 14:21:06 +0800 Subject: [PATCH 593/682] Disable iouring on glibc build --- src/SPC/builder/extension/swoole.php | 2 +- src/globals/test-extensions.php | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index 86098ec51..7da30ba69 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -59,7 +59,7 @@ public function getUnixConfigureArg(bool $shared = false): string $arg .= $this->builder->getLib('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; $arg .= $this->builder->getLib('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : ''; $arg .= $this->builder->getLib('zstd') ? ' --enable-zstd' : ''; - $arg .= $this->builder->getLib('liburing') ? ' --enable-iouring --enable-uring-socket' : ''; + $arg .= $this->builder->getLib('liburing') && getenv('SPC_LIBC') !== 'glibc' ? ' --enable-iouring --enable-uring-socket' : '--disable-iouring'; $arg .= $this->builder->getExt('sockets') ? ' --enable-sockets' : ''; // enable additional features that require the pdo extension, but conflict with pdo_* extensions diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 58eeaa523..f5bb025bc 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -15,7 +15,7 @@ $test_php_version = [ // '8.1', // '8.2', - // '8.3', + '8.3', // '8.4', '8.5', // 'git', @@ -26,12 +26,12 @@ 'macos-15-intel', // bin/spc for x86_64 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', + // 'windows-2025', ]; // whether enable thread safe @@ -43,14 +43,14 @@ $upx = false; // whether to test frankenphp build, only available for macOS and linux -$frankenphp = true; +$frankenphp = false; // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'curl', + 'Linux', 'Darwin' => 'curl,swoole', 'Windows' => 'intl', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'krb5', + 'Linux', 'Darwin' => 'krb5,libiouring', 'Windows' => '', }; From a27f5cc8bec46cc7dc41d27da2a78c79fe989cb2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 14:26:03 +0800 Subject: [PATCH 594/682] Oops --- src/globals/test-extensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index f5bb025bc..8281f88b7 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'krb5,libiouring', + 'Linux', 'Darwin' => 'krb5', 'Windows' => '', }; From 4c07bcc95fa627ab837180b8ab15edd739602a09 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:05:15 +0800 Subject: [PATCH 595/682] Allow xz build statically --- src/Package/Extension/xz.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Package/Extension/xz.php diff --git a/src/Package/Extension/xz.php b/src/Package/Extension/xz.php new file mode 100644 index 000000000..a1f0d88d3 --- /dev/null +++ b/src/Package/Extension/xz.php @@ -0,0 +1,21 @@ +getSourceDir() . '/config.w32', 'true', 'PHP_XZ_SHARED'); + } +} From 1320a7446091f2e2a94409f847e4313ece09849b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:32:50 +0800 Subject: [PATCH 596/682] Add ext-trader --- config/pkg/ext/ext-trader.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/pkg/ext/ext-trader.yml b/config/pkg/ext/ext-trader.yml index 8e16afbbe..03dcadc60 100644 --- a/config/pkg/ext/ext-trader.yml +++ b/config/pkg/ext/ext-trader.yml @@ -8,7 +8,4 @@ ext-trader: license-files: [LICENSE] license: BSD-2-Clause php-extension: - support: - BSD: wip - Windows: wip arg-type: enable From 6b29d92579b49f53e3e5a6e8b6b0f762d38af8c3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:33:31 +0800 Subject: [PATCH 597/682] Disable ext-snmp, ext-ldap on Windows --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index d3769dead..52478e2c3 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -115,6 +115,9 @@ ext-ldap: - libsodium - ext-openssl php-extension: + os: + - Linux + - Darwin arg-type: with-path arg-type@windows: with ext-libxml: @@ -278,6 +281,9 @@ ext-snmp: depends: - net-snmp php-extension: + os: + - Linux + - Darwin arg-type: with ext-soap: type: php-extension From 323f1ec00eeebeb0ceab012838947bb4c97a69eb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:33:52 +0800 Subject: [PATCH 598/682] Use correct ext-xz arg-type --- config/pkg/ext/ext-xz.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/pkg/ext/ext-xz.yml b/config/pkg/ext/ext-xz.yml index 0d625ad29..1551eaec4 100644 --- a/config/pkg/ext/ext-xz.yml +++ b/config/pkg/ext/ext-xz.yml @@ -13,3 +13,4 @@ ext-xz: - xz php-extension: arg-type: with-path + arg-type@windows: enable From 35b23a532f56db41d7362da37cd7db920324edae Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:34:55 +0800 Subject: [PATCH 599/682] Add ext-snappy and snappy support on Windows --- config/pkg/ext/ext-snappy.yml | 1 + config/pkg/lib/snappy.yml | 7 +++++++ src/Package/Library/snappy.php | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/config/pkg/ext/ext-snappy.yml b/config/pkg/ext/ext-snappy.yml index 7ddec2618..394527779 100644 --- a/config/pkg/ext/ext-snappy.yml +++ b/config/pkg/ext/ext-snappy.yml @@ -16,3 +16,4 @@ ext-snappy: lang: cpp php-extension: arg-type@unix: '--enable-snappy --with-snappy-includedir=@build_root_path@' + arg-type@windows: '--enable-snappy' diff --git a/config/pkg/lib/snappy.yml b/config/pkg/lib/snappy.yml index a369fa339..9875e784f 100644 --- a/config/pkg/lib/snappy.yml +++ b/config/pkg/lib/snappy.yml @@ -15,6 +15,13 @@ snappy: - snappy-c.h - snappy-sinksource.h - snappy-stubs-public.h + headers@windows: + - snappy.h + - snappy-c.h + - snappy-sinksource.h + - snappy-stubs-public.h lang: cpp static-libs@unix: - libsnappy.a + static-libs@windows: + - snappy.lib diff --git a/src/Package/Library/snappy.php b/src/Package/Library/snappy.php index d822c3cfd..ca5a230af 100644 --- a/src/Package/Library/snappy.php +++ b/src/Package/Library/snappy.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('snappy')] class snappy @@ -24,4 +25,15 @@ public function buildUnix(LibraryPackage $lib): void ) ->build('../..'); } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DSNAPPY_BUILD_TESTS=OFF', + '-DSNAPPY_BUILD_BENCHMARKS=OFF', + ) + ->build(); + } } From f46240b55e6e6f8b72a9fe2b8a13b9863698d2a0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:35:22 +0800 Subject: [PATCH 600/682] Remove old support config --- config/pkg/ext/ext-xlswriter.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/pkg/ext/ext-xlswriter.yml b/config/pkg/ext/ext-xlswriter.yml index c13751af0..01b99ed90 100644 --- a/config/pkg/ext/ext-xlswriter.yml +++ b/config/pkg/ext/ext-xlswriter.yml @@ -13,7 +13,5 @@ ext-xlswriter: suggests: - openssl php-extension: - support: - BSD: wip arg-type: custom arg-type@windows: '--with-xlswriter' From d29bd12bcc6ce5a46ee417037172cc81f1d0bcad Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:36:21 +0800 Subject: [PATCH 601/682] Remove old support config --- config/pkg/ext/ext-event.yml | 5 +++-- config/pkg/ext/ext-grpc.yml | 3 +++ config/pkg/ext/ext-imagick.yml | 3 +++ config/pkg/ext/ext-imap.yml | 3 +++ config/pkg/ext/ext-inotify.yml | 3 +++ config/pkg/ext/ext-protobuf.yml | 4 ++++ config/pkg/ext/ext-rdkafka.yml | 3 +++ config/pkg/ext/ext-spx.yml | 3 +++ config/pkg/ext/ext-swoole.yml | 15 +++++++++++++++ config/pkg/ext/ext-xdebug.yml | 3 +++ config/pkg/ext/ext-xhprof.yml | 6 +++--- 11 files changed, 46 insertions(+), 5 deletions(-) diff --git a/config/pkg/ext/ext-event.yml b/config/pkg/ext/ext-event.yml index dd9c1c8ec..537af066b 100644 --- a/config/pkg/ext/ext-event.yml +++ b/config/pkg/ext/ext-event.yml @@ -14,6 +14,7 @@ ext-event: suggests: - ext-sockets php-extension: - support: - Windows: wip + os: + - Linux + - Darwin arg-type: custom diff --git a/config/pkg/ext/ext-grpc.yml b/config/pkg/ext/ext-grpc.yml index ff5bae7b8..ae63cad2b 100644 --- a/config/pkg/ext/ext-grpc.yml +++ b/config/pkg/ext/ext-grpc.yml @@ -11,4 +11,7 @@ ext-grpc: - grpc lang: cpp php-extension: + os: + - Linux + - Darwin arg-type@unix: enable-path diff --git a/config/pkg/ext/ext-imagick.yml b/config/pkg/ext/ext-imagick.yml index e6f9843eb..2a1c221c3 100644 --- a/config/pkg/ext/ext-imagick.yml +++ b/config/pkg/ext/ext-imagick.yml @@ -10,4 +10,7 @@ ext-imagick: depends: - imagemagick php-extension: + os: + - Linux + - Darwin arg-type: custom diff --git a/config/pkg/ext/ext-imap.yml b/config/pkg/ext/ext-imap.yml index a6c18daca..3abcebb8b 100644 --- a/config/pkg/ext/ext-imap.yml +++ b/config/pkg/ext/ext-imap.yml @@ -12,4 +12,7 @@ ext-imap: suggests: - ext-openssl php-extension: + os: + - Linux + - Darwin arg-type: custom diff --git a/config/pkg/ext/ext-inotify.yml b/config/pkg/ext/ext-inotify.yml index 0956f9e40..d69847ee4 100644 --- a/config/pkg/ext/ext-inotify.yml +++ b/config/pkg/ext/ext-inotify.yml @@ -7,3 +7,6 @@ ext-inotify: metadata: license-files: [LICENSE] license: PHP-3.01 + php-extension: + os: + - Linux diff --git a/config/pkg/ext/ext-protobuf.yml b/config/pkg/ext/ext-protobuf.yml index 020059d39..f9d6b2080 100644 --- a/config/pkg/ext/ext-protobuf.yml +++ b/config/pkg/ext/ext-protobuf.yml @@ -7,3 +7,7 @@ ext-protobuf: metadata: license-files: [LICENSE] license: BSD-3-Clause + php-extension: + os: + - Linux + - Darwin diff --git a/config/pkg/ext/ext-rdkafka.yml b/config/pkg/ext/ext-rdkafka.yml index 1f26e49cb..1c7b55e3a 100644 --- a/config/pkg/ext/ext-rdkafka.yml +++ b/config/pkg/ext/ext-rdkafka.yml @@ -12,4 +12,7 @@ ext-rdkafka: - librdkafka lang: cpp php-extension: + os: + - Linux + - Darwin arg-type: custom diff --git a/config/pkg/ext/ext-spx.yml b/config/pkg/ext/ext-spx.yml index a379cdd4d..edf41f514 100644 --- a/config/pkg/ext/ext-spx.yml +++ b/config/pkg/ext/ext-spx.yml @@ -11,4 +11,7 @@ ext-spx: depends: - ext-zlib php-extension: + os: + - Linux + - Darwin arg-type: '--enable-SPX@shared_suffix@' diff --git a/config/pkg/ext/ext-swoole.yml b/config/pkg/ext/ext-swoole.yml index b6499e85e..31bbb1dc0 100644 --- a/config/pkg/ext/ext-swoole.yml +++ b/config/pkg/ext/ext-swoole.yml @@ -34,6 +34,9 @@ ext-swoole: - ext-swoole-hook-odbc lang: cpp php-extension: + os: + - Linux + - Darwin arg-type: custom ext-swoole-hook-mysql: type: php-extension @@ -44,6 +47,9 @@ ext-swoole-hook-mysql: suggests: - ext-mysqli php-extension: + os: + - Linux + - Darwin arg-type: none display-name: swoole ext-swoole-hook-odbc: @@ -52,6 +58,9 @@ ext-swoole-hook-odbc: - ext-pdo - unixodbc php-extension: + os: + - Linux + - Darwin arg-type: none display-name: swoole ext-swoole-hook-pgsql: @@ -60,6 +69,9 @@ ext-swoole-hook-pgsql: - ext-pgsql - ext-pdo php-extension: + os: + - Linux + - Darwin arg-type: none display-name: swoole ext-swoole-hook-sqlite: @@ -68,5 +80,8 @@ ext-swoole-hook-sqlite: - ext-sqlite3 - ext-pdo php-extension: + os: + - Linux + - Darwin arg-type: none display-name: swoole diff --git a/config/pkg/ext/ext-xdebug.yml b/config/pkg/ext/ext-xdebug.yml index 0374e573b..20c6b4600 100644 --- a/config/pkg/ext/ext-xdebug.yml +++ b/config/pkg/ext/ext-xdebug.yml @@ -8,6 +8,9 @@ ext-xdebug: license-files: [LICENSE] license: Xdebug-1.03 php-extension: + os: + - Linux + - Darwin zend-extension: true build-static: false build-shared: true diff --git a/config/pkg/ext/ext-xhprof.yml b/config/pkg/ext/ext-xhprof.yml index b075f65bd..a554d8375 100644 --- a/config/pkg/ext/ext-xhprof.yml +++ b/config/pkg/ext/ext-xhprof.yml @@ -11,8 +11,8 @@ ext-xhprof: depends: - ext-ctype php-extension: - support: - Windows: wip - BSD: wip + os: + - Linux + - Darwin arg-type: enable build-with-php: true From 3808457b52cae809bcd9e5b971b41b2694d845bd Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:51:18 +0800 Subject: [PATCH 602/682] Add lz4 and ext-lz4 for Windows --- config/pkg/ext/ext-lz4.yml | 5 +++-- config/pkg/lib/liblz4.yml | 2 ++ src/Package/Library/liblz4.php | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config/pkg/ext/ext-lz4.yml b/config/pkg/ext/ext-lz4.yml index 8a3bb4dba..8e16d54e4 100644 --- a/config/pkg/ext/ext-lz4.yml +++ b/config/pkg/ext/ext-lz4.yml @@ -2,8 +2,9 @@ ext-lz4: type: php-extension artifact: source: - type: ghtagtar - repo: kjdev/php-ext-lz4 + type: git + url: 'https://github.com/kjdev/php-ext-lz4.git' + rev: master extract: php-src/ext/lz4 metadata: license-files: [LICENSE] diff --git a/config/pkg/lib/liblz4.yml b/config/pkg/lib/liblz4.yml index 298b3abf3..bb7a74aef 100644 --- a/config/pkg/lib/liblz4.yml +++ b/config/pkg/lib/liblz4.yml @@ -11,3 +11,5 @@ liblz4: license: BSD-2-Clause static-libs@unix: - liblz4.a + static-libs@windows: + - lz4.lib diff --git a/src/Package/Library/liblz4.php b/src/Package/Library/liblz4.php index fb52a4fba..5dfbc9956 100644 --- a/src/Package/Library/liblz4.php +++ b/src/Package/Library/liblz4.php @@ -10,6 +10,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; use StaticPHP\Util\FileSystem; #[Library('liblz4')] @@ -22,6 +23,16 @@ public function patchBeforeBuild(LibraryPackage $lib): void FileSystem::replaceFileStr($lib->getSourceDir() . '/programs/Makefile', 'install: lz4', "install: lz4\n\ninstallewfwef: lz4"); } + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->setWorkingDir("{$lib->getSourceDir()}/build/cmake") + ->setBuildDir("{$lib->getSourceDir()}/_win_build") + ->addConfigureArgs('-DLZ4_BUILD_CLI=OFF') + ->build(); + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function buildUnix(LibraryPackage $lib, PackageBuilder $builder): void From db8520d8f012c5fc10bbc18d8b000f219a91a4be Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 15:56:39 +0800 Subject: [PATCH 603/682] Change config --- config/pkg/ext/ext-uuid.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/pkg/ext/ext-uuid.yml b/config/pkg/ext/ext-uuid.yml index 68080531d..3d71aa8b4 100644 --- a/config/pkg/ext/ext-uuid.yml +++ b/config/pkg/ext/ext-uuid.yml @@ -10,7 +10,7 @@ ext-uuid: depends: - libuuid php-extension: - support: - Windows: wip - BSD: wip + os: + - Linux + - Darwin arg-type: with-path From 864fa9d0eb96958de4d75c7982ebe08440220520 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 16:09:35 +0800 Subject: [PATCH 604/682] Fix #1083 --- src/SPC/builder/extension/grpc.php | 9 +++++++++ src/SPC/builder/unix/library/libjpeg.php | 5 ++++- src/SPC/util/executor/UnixCMakeExecutor.php | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index ba15518f2..732f7cbe5 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -28,6 +28,15 @@ public function patchBeforeBuildconf(): bool 'zend_ce_exception,', ); + // Fix include path conflict with pdo_sqlsrv: grpc's PHP ext dir is added to the global include path via + $grpc_php_dir = "{$this->source_dir}/src/php/ext/grpc"; + if (file_exists("{$grpc_php_dir}/version.h")) { + copy("{$grpc_php_dir}/version.h", "{$grpc_php_dir}/php_grpc_version.h"); + unlink("{$grpc_php_dir}/version.h"); + FileSystem::replaceFileStr("{$grpc_php_dir}/php_grpc.h", '#include "version.h"', '#include "php_grpc_version.h"'); + FileSystem::replaceFileStr("{$grpc_php_dir}/php_grpc.c", '#include "version.h"', '#include "php_grpc_version.h"'); + } + $config_m4 = <<<'M4' PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])]) diff --git a/src/SPC/builder/unix/library/libjpeg.php b/src/SPC/builder/unix/library/libjpeg.php index 98881b762..969fa0608 100644 --- a/src/SPC/builder/unix/library/libjpeg.php +++ b/src/SPC/builder/unix/library/libjpeg.php @@ -14,7 +14,10 @@ protected function build(): void ->addConfigureArgs( '-DENABLE_STATIC=ON', '-DENABLE_SHARED=OFF', - '-DWITH_SYSTEM_ZLIB=ON' + '-DWITH_SYSTEM_ZLIB=ON', + '-DWITH_TOOLS=OFF', + '-DWITH_TESTS=OFF', + '-DWITH_SIMD=OFF', ) ->build(); // patch pkgconfig diff --git a/src/SPC/util/executor/UnixCMakeExecutor.php b/src/SPC/util/executor/UnixCMakeExecutor.php index eceab9014..d0241f567 100644 --- a/src/SPC/util/executor/UnixCMakeExecutor.php +++ b/src/SPC/util/executor/UnixCMakeExecutor.php @@ -183,6 +183,7 @@ private function makeCmakeToolchainFile(): string $cflags = getenv('SPC_DEFAULT_C_FLAGS'); $cc = getenv('CC'); $cxx = getenv('CCX'); + $include = BUILD_INCLUDE_PATH; logger()->debug("making cmake tool chain file for {$os} {$target_arch} with CFLAGS='{$cflags}'"); $root = BUILD_ROOT_PATH; $pkgConfigExecutable = PkgConfigUtil::findPkgConfig(); @@ -210,6 +211,8 @@ private function makeCmakeToolchainFile(): string set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "{$include}") +set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "{$include}") set(CMAKE_EXE_LINKER_FLAGS "-ldl -lpthread -lm -lutil") CMAKE; // Whoops, linux may need CMAKE_AR sometimes From 6ef012e204d91e00af1dab4b7a1f27d7b44cc48b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 16:15:07 +0800 Subject: [PATCH 605/682] Add ext-uv and libuv support on Windows --- config/pkg/lib/libuv.yml | 2 ++ src/Package/Extension/uv.php | 15 +++++++++++++++ src/Package/Library/libuv.php | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/config/pkg/lib/libuv.yml b/config/pkg/lib/libuv.yml index 3c41906dc..1548ebcd8 100644 --- a/config/pkg/lib/libuv.yml +++ b/config/pkg/lib/libuv.yml @@ -9,3 +9,5 @@ libuv: license: MIT static-libs@unix: - libuv.a + static-libs@windows: + - libuv.lib diff --git a/src/Package/Extension/uv.php b/src/Package/Extension/uv.php index 869f4ad99..68a796ada 100644 --- a/src/Package/Extension/uv.php +++ b/src/Package/Extension/uv.php @@ -24,6 +24,21 @@ public function validate(): void } } + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-uv')] + public function patchBeforeBuild(): void + { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/php_uv.c", + '#if !defined(PHP_WIN32) || defined(HAVE_SOCKET)', + '#if !defined(PHP_WIN32) || (defined(HAVE_SOCKETS) && !defined(COMPILE_DL_SOCKETS))', + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.w32", + 'CHECK_LIB("Ws2_32.lib","uv", PHP_UV);', + "CHECK_LIB(\"Ws2_32.lib\",\"uv\" , PHP_UV);\n\tCHECK_LIB(\"dbghelp.lib\",\"uv\", PHP_UV);", + ); + } + #[BeforeStage('ext-uv', [PhpExtensionPackage::class, 'makeForUnix'])] public function patchBeforeSharedMake(PhpExtensionPackage $pkg): bool { diff --git a/src/Package/Library/libuv.php b/src/Package/Library/libuv.php index ed8c58381..c27b499c9 100644 --- a/src/Package/Library/libuv.php +++ b/src/Package/Library/libuv.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; #[Library('libuv')] class libuv @@ -22,4 +23,12 @@ public function buildUnix(LibraryPackage $lib): void // patch pkgconfig $lib->patchPkgconfPrefix(['libuv-static.pc']); } + + #[BuildFor('Windows')] + public function buildWindows(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs('-DLIBUV_BUILD_SHARED=OFF') + ->build(); + } } From 4ba565b461580d642ab59770943b4bb1e675d5d0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 16:15:13 +0800 Subject: [PATCH 606/682] Add ext-uv and libuv support on Windows --- config/pkg/ext/ext-uv.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/pkg/ext/ext-uv.yml b/config/pkg/ext/ext-uv.yml index f1a3031bf..59d4a8c90 100644 --- a/config/pkg/ext/ext-uv.yml +++ b/config/pkg/ext/ext-uv.yml @@ -12,7 +12,4 @@ ext-uv: - libuv - ext-sockets php-extension: - support: - Windows: wip - BSD: wip arg-type: with-path From 869c9a06e37a4687a0b7f91f58b7b8b15fe3df21 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 16:25:24 +0800 Subject: [PATCH 607/682] Add ext-maxminddb and libmaxminddb support on Windows --- config/pkg/lib/libmaxminddb.yml | 2 ++ src/Package/Extension/maxminddb.php | 1 + src/Package/Library/libmaxminddb.php | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/config/pkg/lib/libmaxminddb.yml b/config/pkg/lib/libmaxminddb.yml index a0c3a307f..1f67800e2 100644 --- a/config/pkg/lib/libmaxminddb.yml +++ b/config/pkg/lib/libmaxminddb.yml @@ -14,3 +14,5 @@ libmaxminddb: - maxminddb_config.h static-libs@unix: - libmaxminddb.a + static-libs@windows: + - libmaxminddb.lib diff --git a/src/Package/Extension/maxminddb.php b/src/Package/Extension/maxminddb.php index bda8d34c7..9d04fcb94 100644 --- a/src/Package/Extension/maxminddb.php +++ b/src/Package/Extension/maxminddb.php @@ -15,6 +15,7 @@ class maxminddb extends PhpExtensionPackage { #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-maxminddb')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-maxminddb')] #[PatchDescription('Patch maxminddb extension for buildconf to support new source structure')] public function patchBeforeBuildconf(): void { diff --git a/src/Package/Library/libmaxminddb.php b/src/Package/Library/libmaxminddb.php index a045e4f12..54c045189 100644 --- a/src/Package/Library/libmaxminddb.php +++ b/src/Package/Library/libmaxminddb.php @@ -8,6 +8,8 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; +use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('libmaxminddb')] class libmaxminddb @@ -23,4 +25,18 @@ public function buildUnix(LibraryPackage $lib): void ) ->build(); } + + #[BuildFor('Windows')] + public function buildWindows(LibraryPackage $lib): void + { + WindowsCMakeExecutor::create($lib) + ->addConfigureArgs( + '-DBUILD_TESTING=OFF', + '-DMAXMINDDB_BUILD_BINARIES=OFF', + ) + ->build(); + if (!file_exists($lib->getLibDir() . '\libmaxminddb.lib')) { + FileSystem::copy("{$lib->getLibDir()}\\maxminddb.lib", "{$lib->getLibDir()}\\libmaxminddb.lib"); + } + } } From e33aff18cc94f547fb686e5ac9b528449c23768e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 20:37:25 +0800 Subject: [PATCH 608/682] Forward-port #1082 --- config/pkg/lib/watcher.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/lib/watcher.yml b/config/pkg/lib/watcher.yml index 6cf376f69..82911ec41 100644 --- a/config/pkg/lib/watcher.yml +++ b/config/pkg/lib/watcher.yml @@ -8,6 +8,8 @@ watcher: metadata: license-files: [license] license: MIT + frameworks: + - CoreServices headers: - wtr/watcher-c.h lang: cpp From 58eb769ddf0f10ccb12688e1abf1f3db10b2d783 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 20:37:40 +0800 Subject: [PATCH 609/682] Forward-port #1086 --- src/Package/Extension/xlswriter.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Package/Extension/xlswriter.php b/src/Package/Extension/xlswriter.php index ae11f307e..f4d155302 100644 --- a/src/Package/Extension/xlswriter.php +++ b/src/Package/Extension/xlswriter.php @@ -11,6 +11,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\SourcePatcher; #[Extension('xlswriter')] @@ -27,6 +28,13 @@ public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): return $arg; } + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-xlswriter')] + #[PatchDescription('Fix Unix build: add -std=gnu17 to CFLAGS to fix build errors on older GCC versions')] + public function patchBeforeUnixMake(): void + { + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -std=gnu17'); + } + #[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-xlswriter')] #[PatchDescription('Fix Windows build: apply win32 patch and add UTF-8 BOM to theme.c')] public function patchBeforeMakeForWindows(): void @@ -39,4 +47,11 @@ public function patchBeforeMakeForWindows(): void file_put_contents($this->getSourceDir() . '/library/libxlsxwriter/src/theme.c', $bom . $content); } } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=gnu17'; + return $parent; + } } From e83a997d0c0027c126dc4f92bdea8f5dac3e1739 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 10 Apr 2026 21:09:46 +0800 Subject: [PATCH 610/682] Add ext-zstd for Windows --- config/pkg/ext/ext-zstd.yml | 3 +++ config/pkg/lib/zstd.yml | 2 +- src/Package/Library/zstd.php | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/ext-zstd.yml b/config/pkg/ext/ext-zstd.yml index 1f004f131..9b01422be 100644 --- a/config/pkg/ext/ext-zstd.yml +++ b/config/pkg/ext/ext-zstd.yml @@ -11,5 +11,8 @@ ext-zstd: license: MIT depends: - zstd + suggests: + - ext-apcu php-extension: arg-type: '--enable-zstd --with-libzstd=@build_root_path@' + arg-type@windows: '--enable-zstd' diff --git a/config/pkg/lib/zstd.yml b/config/pkg/lib/zstd.yml index 875380d1f..c1d15cf6e 100644 --- a/config/pkg/lib/zstd.yml +++ b/config/pkg/lib/zstd.yml @@ -18,4 +18,4 @@ zstd: static-libs@unix: - libzstd.a static-libs@windows: - - zstd_static.lib + - zstd.lib diff --git a/src/Package/Library/zstd.php b/src/Package/Library/zstd.php index f12bf3e02..4b4a490f1 100644 --- a/src/Package/Library/zstd.php +++ b/src/Package/Library/zstd.php @@ -9,6 +9,7 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Util\FileSystem; #[Library('zstd')] class zstd @@ -24,6 +25,7 @@ public function buildWin(LibraryPackage $package): void '-DZSTD_BUILD_SHARED=OFF', ) ->build(); + FileSystem::copy($package->getLibDir() . '\zstd_static.lib', $package->getLibDir() . '/zstd.lib'); } #[BuildFor('Linux')] From 25891a8648c97af5ba34047f05c0961b7d652d7e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 11 Apr 2026 00:39:30 +0800 Subject: [PATCH 611/682] Add ext-mongodb for Windows --- config/pkg/ext/ext-mongodb.yml | 7 +++++-- src/Package/Extension/mongodb.php | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/config/pkg/ext/ext-mongodb.yml b/config/pkg/ext/ext-mongodb.yml index 7cbdbb140..97f9f0a0d 100644 --- a/config/pkg/ext/ext-mongodb.yml +++ b/config/pkg/ext/ext-mongodb.yml @@ -9,7 +9,9 @@ ext-mongodb: metadata: license-files: [LICENSE] license: PHP-3.01 - suggests: + depends@windows: + - ext-openssl + suggests@unix: - icu - openssl - zstd @@ -18,4 +20,5 @@ ext-mongodb: - CoreFoundation - Security php-extension: - arg-type: custom + arg-type@unix: custom + arg-type@windows: '--enable-mongodb --with-mongodb-client-side-encryption' diff --git a/src/Package/Extension/mongodb.php b/src/Package/Extension/mongodb.php index 3434491d6..944ce86af 100644 --- a/src/Package/Extension/mongodb.php +++ b/src/Package/Extension/mongodb.php @@ -4,14 +4,27 @@ namespace Package\Extension; +use Package\Target\php; +use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\FileSystem; #[Extension('mongodb')] class mongodb extends PhpExtensionPackage { + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-mongodb')] + public function patchBeforeBuild(): void + { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.w32", + 'ADD_FLAG("CFLAGS_MONGODB", "/D KMS_MESSAGE_LITTLE_ENDIAN=1 /D MONGOCRYPT_LITTLE_ENDIAN=1 /D MLIB_USER=1");', + 'ADD_FLAG("CFLAGS_MONGODB", "/D KMS_MESSAGE_LITTLE_ENDIAN=1 /D MONGOCRYPT_LITTLE_ENDIAN=1 /D MLIB_USER=1");' . "\n ADD_FLAG(\"CFLAGS_MONGODB\", \"/utf-8\");", + ); + } + #[CustomPhpConfigureArg('Darwin')] #[CustomPhpConfigureArg('Linux')] public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): string From ae7552f5a25e24c9e11f32de7c1627143478aa3d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 11 Apr 2026 00:45:31 +0800 Subject: [PATCH 612/682] Forward-port #1087 --- config/pkg/lib/krb5.yml | 5 ++--- src/Package/Extension/grpc.php | 9 +++++++++ src/Package/Library/krb5.php | 4 +++- src/Package/Library/libjpeg.php | 4 ++++ src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php | 3 +++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/config/pkg/lib/krb5.yml b/config/pkg/lib/krb5.yml index 07fa3327f..e3234ade5 100644 --- a/config/pkg/lib/krb5.yml +++ b/config/pkg/lib/krb5.yml @@ -2,9 +2,8 @@ krb5: type: library artifact: source: - type: ghtagtar - repo: krb5/krb5 - match: krb5.+-final + type: url + url: 'https://web.mit.edu/kerberos/dist/krb5/1.22/krb5-1.22.2.tar.gz' metadata: license-files: [NOTICE] license: BSD-3-Clause diff --git a/src/Package/Extension/grpc.php b/src/Package/Extension/grpc.php index c3b08f161..d1ce1511e 100644 --- a/src/Package/Extension/grpc.php +++ b/src/Package/Extension/grpc.php @@ -29,6 +29,15 @@ public function patchBeforeBuildconf(): void 'zend_ce_exception,', ); + // Fix include path conflict with pdo_sqlsrv: grpc's PHP ext dir is added to the global include path via + $grpc_php_dir = "{$this->getSourceDir()}/src/php/ext/grpc"; + if (file_exists("{$grpc_php_dir}/version.h")) { + copy("{$grpc_php_dir}/version.h", "{$grpc_php_dir}/php_grpc_version.h"); + unlink("{$grpc_php_dir}/version.h"); + FileSystem::replaceFileStr("{$grpc_php_dir}/php_grpc.h", '#include "version.h"', '#include "php_grpc_version.h"'); + FileSystem::replaceFileStr("{$grpc_php_dir}/php_grpc.c", '#include "version.h"', '#include "php_grpc_version.h"'); + } + // custom config.m4 content for grpc extension, to prevent building libgrpc.a again $config_m4 = <<<'M4' PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])]) diff --git a/src/Package/Library/krb5.php b/src/Package/Library/krb5.php index 303c3b63e..ad6ca1370 100644 --- a/src/Package/Library/krb5.php +++ b/src/Package/Library/krb5.php @@ -19,7 +19,9 @@ class krb5 #[BuildFor('Darwin')] public function build(LibraryPackage $lib, PackageInstaller $installer): void { - shell()->cd($lib->getSourceRoot())->exec('autoreconf -if'); + if (!file_exists($lib->getSourceRoot() . '/configure')) { + shell()->cd($lib->getSourceRoot())->exec('autoreconf -if'); + } $resolved = array_keys($installer->getResolvedPackages()); $spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); diff --git a/src/Package/Library/libjpeg.php b/src/Package/Library/libjpeg.php index 85b0f6d49..06512aaca 100644 --- a/src/Package/Library/libjpeg.php +++ b/src/Package/Library/libjpeg.php @@ -22,6 +22,10 @@ public function buildUnix(LibraryPackage $lib): void ->addConfigureArgs( '-DENABLE_STATIC=ON', '-DENABLE_SHARED=OFF', + '-DWITH_SYSTEM_ZLIB=ON', + '-DWITH_TOOLS=OFF', + '-DWITH_TESTS=OFF', + '-DWITH_SIMD=OFF', ) ->build(); // patch pkgconfig diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 73209fff0..1ddacb03b 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -270,6 +270,7 @@ private function makeCmakeToolchainFile(): string $cflags = getenv('SPC_DEFAULT_C_FLAGS'); $cc = getenv('CC'); $cxx = getenv('CXX'); + $include = BUILD_INCLUDE_PATH; logger()->debug("making cmake tool chain file for {$os} {$target_arch} with CFLAGS='{$cflags}'"); $root = BUILD_ROOT_PATH; $pkgConfigExecutable = PkgConfigUtil::findPkgConfig(); @@ -298,6 +299,8 @@ private function makeCmakeToolchainFile(): string set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "{$include}") +set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "{$include}") CMAKE; // Whoops, linux may need CMAKE_AR sometimes if (PHP_OS_FAMILY === 'Linux') { From 626bdc35091fee6b554428419cc489441020429b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:11:48 +0800 Subject: [PATCH 613/682] Allow fallback to builder options --- src/StaticPHP/Package/TargetPackage.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/StaticPHP/Package/TargetPackage.php b/src/StaticPHP/Package/TargetPackage.php index d29cf9238..ca80d83ad 100644 --- a/src/StaticPHP/Package/TargetPackage.php +++ b/src/StaticPHP/Package/TargetPackage.php @@ -80,6 +80,14 @@ public function getBuildOption(string $key, mixed $default = null): mixed if ($input !== null && $input->hasOption($key)) { return $input->getOption($key); } + + // try builder options + $builder = ApplicationContext::has(PackageBuilder::class) + ? ApplicationContext::get(PackageBuilder::class) + : null; + if ($builder !== null && ($option = $builder->getOption($key)) !== null) { + return $option; + } return $default; } From 4f1ed70c96d48fdc2ff5df5b65ecd277c65728c8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:11:59 +0800 Subject: [PATCH 614/682] Move out callback --- src/StaticPHP/Command/ResetCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/ResetCommand.php b/src/StaticPHP/Command/ResetCommand.php index 207f6484e..85928c2f6 100644 --- a/src/StaticPHP/Command/ResetCommand.php +++ b/src/StaticPHP/Command/ResetCommand.php @@ -62,8 +62,10 @@ public function handle(): int InteractiveTerm::indicateProgress("Removing: {$path}"); if (PHP_OS_FAMILY === 'Windows') { + Shell::passthruCallback(fn () => InteractiveTerm::advance()); // Force delete on Windows to handle git directories $this->removeDirectoryWindows($path); + Shell::passthruCallback(null); } else { // Use FileSystem::removeDir for Unix systems FileSystem::removeDir($path); @@ -88,7 +90,6 @@ private function removeDirectoryWindows(string $path): void // Try using PowerShell for force deletion $escaped_path = escapeshellarg($path); - Shell::passthruCallback(fn () => InteractiveTerm::advance()); // Use PowerShell Remove-Item with -Force and -Recurse $result = cmd()->execWithResult("powershell -Command \"Remove-Item -Path {$escaped_path} -Recurse -Force -ErrorAction SilentlyContinue\"", false); @@ -106,6 +107,5 @@ private function removeDirectoryWindows(string $path): void if (is_dir($path)) { FileSystem::removeDir($path); } - Shell::passthruCallback(null); } } From 39a975dc90c9a9a980982bb8b8b36cff31882da6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:12:17 +0800 Subject: [PATCH 615/682] Just skip sleep --- src/StaticPHP/Package/PackageInstaller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index ac4aa2e01..b4c041df3 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -154,10 +154,10 @@ public function run(bool $disable_delay_msg = false): void $this->resolvePackages(); } - if ($this->interactive && !$disable_delay_msg) { - // show install or build options in terminal with beautiful output - $this->printInstallerInfo(); + // show install or build options in terminal with beautiful output + $this->printInstallerInfo(); + if ($this->interactive && !$disable_delay_msg) { InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL); sleep(2); } From 6976e9db9640f7c891694b079834dfe6d762a032 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:12:32 +0800 Subject: [PATCH 616/682] Allow callback for removeDir --- src/StaticPHP/Util/FileSystem.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index cbb466030..5e86a1d91 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -287,10 +287,11 @@ public static function getClassesPsr4(string $dir, string $base_namespace, mixed /** * Remove directory recursively * - * @param string $dir Directory to remove - * @return bool Success status + * @param string $dir Directory to remove + * @param null|callable $callback Callback for every single scan items + * @return bool Success status */ - public static function removeDir(string $dir): bool + public static function removeDir(string $dir, ?callable $callback = null): bool { $dir = self::convertPath($dir); logger()->debug('Removing path recursively: "' . $dir . '"'); @@ -311,7 +312,9 @@ public static function removeDir(string $dir): bool } // 遍历目录 foreach ($scan_list as $v) { - InteractiveTerm::advance(); + if ($callback) { + $callback($v); + } // Unix 系统排除这俩目录 if ($v == '.' || $v == '..') { continue; From 8ee9d134b3206bfd08c4908aa39c1dd11872fe59 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:12:53 +0800 Subject: [PATCH 617/682] Add craft command --- src/StaticPHP/Command/CraftCommand.php | 224 +++++++++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 2 files changed, 226 insertions(+) create mode 100644 src/StaticPHP/Command/CraftCommand.php diff --git a/src/StaticPHP/Command/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php new file mode 100644 index 000000000..9b1126aca --- /dev/null +++ b/src/StaticPHP/Command/CraftCommand.php @@ -0,0 +1,224 @@ +addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml'); + } + + public function handle(): int + { + $craft_file = $this->getArgument('craft'); + if (!file_exists($craft_file)) { + $this->output->writeln('craft.yml not found, please create one!'); + return static::USER_ERROR; + } + + $craft = $this->validateAndParseCraftFile($craft_file); + + // set verbosity + $this->output->setVerbosity($craft['verbosity']); + + // apply env + array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}")); + + // run doctor + if ($craft['craft-options']['doctor']) { + $doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX); + if ($doctor->checkAll()) { + Doctor::markPassed(); + $this->output->writeln(''); + } else { + $this->output->writeln('Doctor check failed, please fix the issues and try again.'); + return static::ENVIRONMENT_ERROR; + } + } + + // parse download-options to installer's dl options + $build_options = $craft['build-options']; + if (!$craft['craft-options']['download']) { + $build_options['no-download'] = true; + } + foreach ($craft['download-options'] as $k => $v) { + $build_options["dl-{$k}"] = $v; + } + + // parse SAPI + foreach ($craft['sapi'] as $name) { + $build_options["build-{$name}"] = true; + } + + // clean build + if ($craft['clean-build']) { + FileSystem::resetDir(BUILD_ROOT_PATH); + FileSystem::resetDir(SOURCE_PATH); + } + + $starttime = microtime(true); + // run installer + $installer = new PackageInstaller($build_options); + $installer->addBuildPackage('php'); + $installer->run(true); + + $usedtime = round(microtime(true) - $starttime, 1); + $this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); + $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + $installer->printBuildPackageOutputs(); + + return static::SUCCESS; + } + + /** + * Validate and parse craft.yml file to array. + * + * @param string $craft_file craft.yml path + * @return array{ + * php-version: string, + * extensions: array, + * shared-extensions: array, + * packages: array, + * sapi: array, + * verbosity: int, + * debug: bool, + * clean-build: bool, + * build-options: array, + * download-options: array, + * extra-env: array, + * craft-options: array{ + * doctor: bool, + * download: bool, + * build: bool + * } + * } Parsed craft content + */ + private function validateAndParseCraftFile(string $craft_file): array + { + $build_options = $this->getApplication()->find('build:php')->getDefinition()->getOptions(); + $download_options = $this->getApplication()->find('download')->getDefinition()->getOptions(); + try { + $craft = Yaml::parseFile($craft_file); + } catch (ParseException $e) { + throw new ValidationException("Craft file '{$craft_file}' is broken: {$e->getMessage()}"); + } + if (!is_assoc_array($craft)) { + throw new ValidationException("Craft file '{$craft_file}' must be an associative array."); + } + + // check php-version + if (isset($craft['php-version']) && !preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', strval($craft['php-version']))) { + throw new ValidationException("Craft file '{$craft_file}' has invalid 'php-version' field, it should be in format of '8.0.0'."); + } + + // check php extensions field + if (!isset($craft['extensions'])) { + throw new ValidationException("Craft file '{$craft_file}' must have 'extensions' field."); + } + // parse extension if not list + if (is_string($craft['extensions'])) { + $craft['extensions'] = parse_extension_list($craft['extensions']); + } + + // check shared-extensions field + if (!isset($craft['shared-extensions'])) { + $craft['shared-extensions'] = []; + } elseif (is_string($craft['shared-extensions'])) { + $craft['shared-extensions'] = parse_extension_list($craft['shared-extensions']); + } + + // check libs and additional packages + $v2_libs = parse_comma_list($craft['libs'] ?? []); + $v3_packages = parse_comma_list($craft['packages'] ?? []); + $craft['packages'] = array_merge($v2_libs, $v3_packages); + + // check PHP SAPI + if (!isset($craft['sapi'])) { + throw new ValidationException('Craft file "sapi" is required.'); + } + if (is_string($craft['sapi'])) { + $craft['sapi'] = parse_comma_list($craft['sapi']); + } + + // verbosity + $verbosity_level = $craft['verbosity'] ?? OutputInterface::VERBOSITY_NORMAL; + $debug = $craft['debug'] ?? false; + if ($debug) { + $verbosity_level = OutputInterface::VERBOSITY_DEBUG; + } + $craft['verbosity'] = $verbosity_level; + + // clean-build (if true, reset before all builds) + $craft['clean-build'] ??= false; + + // build-options + if (isset($craft['build-options'])) { + if (!is_assoc_array($craft['build-options'])) { + throw new ValidationException('Craft file "build" options must be an associative array.'); + } + foreach ($craft['build-options'] as $key => $value) { + if (!isset($build_options[$key])) { + throw new ValidationException('Craft file "build" option "' . $key . '" is invalid.'); + } + if ($build_options[$key]->isArray() && !is_array($value)) { + throw new ValidationException('Craft file "build" option "' . $key . '" must be an array.'); + } + } + } else { + $craft['build-options'] = []; + } + + // download-options + if (isset($craft['download-options'])) { + if (!is_assoc_array($craft['download-options'])) { + throw new ValidationException('Craft file "download" options must be an associative array.'); + } + foreach ($craft['download-options'] as $key => $value) { + if (!isset($download_options[$key])) { + throw new ValidationException('Craft file "download" option "' . $key . '" is invalid.'); + } + if ($download_options[$key]->isArray() && !is_array($value)) { + throw new ValidationException('Craft file "download" option "' . $key . '" must be an array.'); + } + } + } else { + $craft['download-options'] = []; + } + + // post-parse: parse php-version field to download options + if (isset($craft['php-version'])) { + $craft['download-options']['with-php'] = strval($craft['php-version']); + $craft['download-options']['ignore-cache'] = (($craft['download-options']['ignore-cache'] ?? false) === true ? true : 'php-src'); + } + + // extra-env + if (isset($craft['extra-env'])) { + if (!is_assoc_array($craft['extra-env'])) { + throw new ValidationException('Craft file "extra-env" must be an associative array.'); + } + } else { + $craft['extra-env'] = []; + } + + // craft-options + $craft['craft-options']['doctor'] ??= true; + $craft['craft-options']['download'] ??= true; + + return $craft; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 0c8fec832..21e3ee8b6 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -7,6 +7,7 @@ use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; use StaticPHP\Command\CheckUpdateCommand; +use StaticPHP\Command\CraftCommand; use StaticPHP\Command\Dev\DumpCapabilitiesCommand; use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; @@ -67,6 +68,7 @@ public function __construct() new ResetCommand(), new CheckUpdateCommand(), new MicroCombineCommand(), + new CraftCommand(), // dev commands new ShellCommand(), From e7bf945b96fe2666110a1c17664722bb4b9973e2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 12:51:16 +0800 Subject: [PATCH 618/682] Forward-port #1094 --- config/pkg/ext/ext-deepclone.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 config/pkg/ext/ext-deepclone.yml diff --git a/config/pkg/ext/ext-deepclone.yml b/config/pkg/ext/ext-deepclone.yml new file mode 100644 index 000000000..ae54dbb59 --- /dev/null +++ b/config/pkg/ext/ext-deepclone.yml @@ -0,0 +1,10 @@ +ext-deepclone: + type: php-extension + artifact: + source: + type: ghtagtar + repo: symfony/php-ext-deepclone + extract: php-src/ext/deepclone + metadata: + license-files: [LICENSE] + license: PHP-3.01 From bca1fb54108447cca86ce15a1fc17b7f85bee3ee Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:01:34 +0800 Subject: [PATCH 619/682] Remove TODO.md --- TODO.md | 57 --------------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 52d6133a9..000000000 --- a/TODO.md +++ /dev/null @@ -1,57 +0,0 @@ -# v3 TODO List - -Tracking items identified during the v2 → v3 migration audit. - ---- - -## Commands - -- [ ] Implement `craft` command (drives full build from `craft.yml`; should be easier with v3 vendor/registry mode) -- [x] Migrate `micro:combine` command (combine `micro.sfx` with PHP code + INI injection) -- [ ] Implement `dump-extensions` command (extract required extensions from `composer.json` / `composer.lock`) -- [ ] Design and implement v3 dev toolchain commands (WIP — needs design decision): - - [ ] `dev:extensions` / equivalent listing command - - [ ] `dev:php-version`, `dev:ext-version`, `dev:lib-version` - - [ ] Doc generation commands (`dev:gen-ext-docs`, `dev:gen-ext-dep-docs`, `dev:gen-lib-dep-docs`) — pending v3 doc design - ---- - -## Source Patches (SourcePatcher → Artifact migration) - -The following v2 `SourcePatcher` hooks are not yet migrated to v3 `src/Package/Artifact/` classes: - -- [ ] Migrate `patchSQLSRVWin32` — removes `/sdl` compile flag to prevent Zend build failure on Windows -- [ ] Migrate `patchSQLSRVPhp85` — fixes `pdo_sqlsrv` directory layout for PHP 8.5 -- [ ] Migrate `patchYamlWin32` — patches `config.w32` `_a.lib` detection logic for the `yaml` extension -- [ ] Migrate `patchImagickWith84` — applies PHP 8.4 compatibility patch for `imagick` based on version detection - ---- - -## Extension Package Classes (Unix) - -Extensions that had non-trivial v2 build logic and are missing a v3 `src/Package/Extension/` class: - -- [x] `gettext` — macOS: fix `config.m4` bracket syntax for cross-version compatibility + append frameworks to linker flags (critical for macOS linking; this is a Unix-side gap, not Windows-only) - ---- - -## Windows Extensions (Early Stage) - -Windows extension support is still in early stage. The following extensions had Windows-specific configure args or patches in v2 and are pending v3 Windows implementation: - -- [ ] `amqp` — Windows configure args -- [ ] `com_dotnet` — Windows-only extension -- [ ] `dom` — remove `dllmain.c` from `config.w32` -- [ ] `ev` — fix `PHP_EV_SHARED` in `config.w32` -- [ ] `gmssl` — add `CHECK_LIB("gmssl.lib")` to `config.w32` -- [ ] `intl` — fix `PHP_INTL_SHARED` in `config.w32` -- [ ] `lz4` — Windows configure args -- [ ] `mbregex` — Windows configure args -- [ ] `sqlsrv` / `pdo_sqlsrv` — complex conditional build logic (independent `sqlsrv` without `pdo_sqlsrv`) -- [ ] `xml` — remove `dllmain.c` from `config.w32`; handles `soap`, `xmlreader`, `xmlwriter`, `simplexml` - ---- - -## Documentation - -- [ ] Write v3 user documentation (currently zero v3 docs) From 661b0fe887697377cc288121bdcea78cb4be84b5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:19:59 +0800 Subject: [PATCH 620/682] Remove v2 from v3 branch --- bin/spc-alpine-docker | 2 +- bin/spc-gnu-docker | 236 --- config/artifact.json | 1061 ------------ config/ext.json | 1293 -------------- config/lib.json | 1066 ------------ config/pkg.ext.json | 1520 ----------------- config/pkg.lib.json | 992 ----------- config/pre-built.json | 7 - config/source.json | 1294 -------------- src/SPC/ConsoleApplication.php | 78 - src/SPC/builder/BuilderBase.php | 543 ------ src/SPC/builder/BuilderProvider.php | 47 - src/SPC/builder/Extension.php | 617 ------- src/SPC/builder/LibraryBase.php | 420 ----- src/SPC/builder/LibraryInterface.php | 21 - src/SPC/builder/extension/amqp.php | 36 - src/SPC/builder/extension/bz2.php | 21 - src/SPC/builder/extension/com_dotnet.php | 17 - src/SPC/builder/extension/curl.php | 129 -- src/SPC/builder/extension/dba.php | 24 - src/SPC/builder/extension/dio.php | 22 - src/SPC/builder/extension/dom.php | 31 - src/SPC/builder/extension/enchant.php | 30 - src/SPC/builder/extension/ev.php | 27 - src/SPC/builder/extension/event.php | 45 - src/SPC/builder/extension/ffi.php | 32 - src/SPC/builder/extension/gd.php | 22 - src/SPC/builder/extension/gettext.php | 35 - src/SPC/builder/extension/glfw.php | 38 - src/SPC/builder/extension/gmssl.php | 22 - src/SPC/builder/extension/grpc.php | 101 -- src/SPC/builder/extension/iconv.php | 27 - src/SPC/builder/extension/imagick.php | 32 - src/SPC/builder/extension/imap.php | 53 - src/SPC/builder/extension/intl.php | 32 - src/SPC/builder/extension/ldap.php | 23 - src/SPC/builder/extension/lz4.php | 22 - src/SPC/builder/extension/maxminddb.php | 25 - src/SPC/builder/extension/mbregex.php | 42 - src/SPC/builder/extension/mbstring.php | 34 - src/SPC/builder/extension/memcache.php | 72 - src/SPC/builder/extension/memcached.php | 26 - src/SPC/builder/extension/mongodb.php | 32 - src/SPC/builder/extension/mysqlnd_ed25519.php | 22 - src/SPC/builder/extension/mysqlnd_parsec.php | 22 - src/SPC/builder/extension/odbc.php | 17 - src/SPC/builder/extension/opcache.php | 68 - src/SPC/builder/extension/openssl.php | 45 - src/SPC/builder/extension/opentelemetry.php | 43 - src/SPC/builder/extension/parallel.php | 27 - src/SPC/builder/extension/password_argon2.php | 36 - src/SPC/builder/extension/pdo_odbc.php | 29 - src/SPC/builder/extension/pdo_pgsql.php | 17 - src/SPC/builder/extension/pdo_sqlite.php | 23 - src/SPC/builder/extension/pgsql.php | 51 - src/SPC/builder/extension/phar.php | 37 - src/SPC/builder/extension/protobuf.php | 25 - src/SPC/builder/extension/rar.php | 24 - src/SPC/builder/extension/rdkafka.php | 45 - src/SPC/builder/extension/readline.php | 47 - src/SPC/builder/extension/redis.php | 41 - src/SPC/builder/extension/simdjson.php | 50 - src/SPC/builder/extension/snappy.php | 29 - src/SPC/builder/extension/snmp.php | 29 - src/SPC/builder/extension/spx.php | 55 - src/SPC/builder/extension/sqlsrv.php | 46 - src/SPC/builder/extension/ssh2.php | 23 - src/SPC/builder/extension/swoole.php | 86 - .../builder/extension/swoole_hook_mysql.php | 30 - .../builder/extension/swoole_hook_odbc.php | 40 - .../builder/extension/swoole_hook_pgsql.php | 46 - .../builder/extension/swoole_hook_sqlite.php | 40 - src/SPC/builder/extension/swow.php | 41 - src/SPC/builder/extension/trader.php | 19 - src/SPC/builder/extension/uv.php | 31 - src/SPC/builder/extension/xhprof.php | 33 - src/SPC/builder/extension/xlswriter.php | 57 - src/SPC/builder/extension/xml.php | 52 - src/SPC/builder/extension/yac.php | 26 - src/SPC/builder/extension/zip.php | 17 - src/SPC/builder/extension/zlib.php | 18 - src/SPC/builder/extension/zstd.php | 17 - src/SPC/builder/freebsd/BSDBuilder.php | 234 --- src/SPC/builder/freebsd/SystemUtil.php | 46 - .../freebsd/library/BSDLibraryBase.php | 27 - src/SPC/builder/freebsd/library/bzip2.php | 12 - src/SPC/builder/freebsd/library/curl.php | 12 - src/SPC/builder/freebsd/library/onig.php | 12 - src/SPC/builder/freebsd/library/openssl.php | 56 - src/SPC/builder/freebsd/library/pkgconfig.php | 15 - src/SPC/builder/freebsd/library/watcher.php | 12 - src/SPC/builder/freebsd/library/zlib.php | 12 - src/SPC/builder/linux/LinuxBuilder.php | 353 ---- src/SPC/builder/linux/SystemUtil.php | 198 --- .../linux/library/LinuxLibraryBase.php | 36 - src/SPC/builder/linux/library/attr.php | 12 - src/SPC/builder/linux/library/brotli.php | 12 - src/SPC/builder/linux/library/bzip2.php | 12 - src/SPC/builder/linux/library/curl.php | 12 - src/SPC/builder/linux/library/fastlz.php | 12 - src/SPC/builder/linux/library/freetype.php | 12 - src/SPC/builder/linux/library/gettext.php | 12 - src/SPC/builder/linux/library/gmp.php | 15 - src/SPC/builder/linux/library/gmssl.php | 12 - src/SPC/builder/linux/library/grpc.php | 12 - src/SPC/builder/linux/library/icu.php | 44 - src/SPC/builder/linux/library/idn2.php | 12 - src/SPC/builder/linux/library/imagemagick.php | 15 - src/SPC/builder/linux/library/imap.php | 69 - src/SPC/builder/linux/library/jbig.php | 12 - src/SPC/builder/linux/library/krb5.php | 12 - src/SPC/builder/linux/library/ldap.php | 12 - src/SPC/builder/linux/library/lerc.php | 12 - src/SPC/builder/linux/library/libacl.php | 12 - src/SPC/builder/linux/library/libaom.php | 12 - src/SPC/builder/linux/library/libargon2.php | 20 - src/SPC/builder/linux/library/libavif.php | 12 - src/SPC/builder/linux/library/libcares.php | 12 - src/SPC/builder/linux/library/libde265.php | 12 - src/SPC/builder/linux/library/libedit.php | 12 - src/SPC/builder/linux/library/libevent.php | 12 - src/SPC/builder/linux/library/libffi.php | 24 - src/SPC/builder/linux/library/libheif.php | 12 - src/SPC/builder/linux/library/libiconv.php | 12 - src/SPC/builder/linux/library/libjpeg.php | 12 - src/SPC/builder/linux/library/libjxl.php | 12 - src/SPC/builder/linux/library/liblz4.php | 12 - .../builder/linux/library/libmaxminddb.php | 12 - .../builder/linux/library/libmemcached.php | 19 - src/SPC/builder/linux/library/libpng.php | 50 - src/SPC/builder/linux/library/librabbitmq.php | 12 - src/SPC/builder/linux/library/librdkafka.php | 13 - src/SPC/builder/linux/library/libsodium.php | 12 - src/SPC/builder/linux/library/libssh2.php | 12 - src/SPC/builder/linux/library/libtiff.php | 12 - .../builder/linux/library/libunistring.php | 12 - src/SPC/builder/linux/library/liburing.php | 53 - src/SPC/builder/linux/library/libuuid.php | 12 - src/SPC/builder/linux/library/libuv.php | 12 - src/SPC/builder/linux/library/libwebp.php | 29 - src/SPC/builder/linux/library/libxml2.php | 12 - src/SPC/builder/linux/library/libxslt.php | 15 - src/SPC/builder/linux/library/libyaml.php | 12 - src/SPC/builder/linux/library/libzip.php | 12 - src/SPC/builder/linux/library/mimalloc.php | 12 - src/SPC/builder/linux/library/ncurses.php | 15 - src/SPC/builder/linux/library/net_snmp.php | 12 - src/SPC/builder/linux/library/nghttp2.php | 12 - src/SPC/builder/linux/library/nghttp3.php | 12 - src/SPC/builder/linux/library/ngtcp2.php | 12 - src/SPC/builder/linux/library/onig.php | 12 - src/SPC/builder/linux/library/openssl.php | 89 - src/SPC/builder/linux/library/pkgconfig.php | 15 - src/SPC/builder/linux/library/postgresql.php | 12 - src/SPC/builder/linux/library/qdbm.php | 12 - src/SPC/builder/linux/library/re2c.php | 15 - src/SPC/builder/linux/library/readline.php | 12 - src/SPC/builder/linux/library/snappy.php | 12 - src/SPC/builder/linux/library/sqlite.php | 12 - src/SPC/builder/linux/library/tidy.php | 12 - src/SPC/builder/linux/library/unixodbc.php | 12 - src/SPC/builder/linux/library/watcher.php | 12 - src/SPC/builder/linux/library/xz.php | 12 - src/SPC/builder/linux/library/zlib.php | 12 - src/SPC/builder/linux/library/zstd.php | 12 - src/SPC/builder/macos/MacOSBuilder.php | 303 ---- src/SPC/builder/macos/SystemUtil.php | 46 - .../macos/library/MacOSLibraryBase.php | 33 - src/SPC/builder/macos/library/brotli.php | 29 - src/SPC/builder/macos/library/bzip2.php | 29 - src/SPC/builder/macos/library/curl.php | 51 - src/SPC/builder/macos/library/fastlz.php | 12 - src/SPC/builder/macos/library/freetype.php | 15 - src/SPC/builder/macos/library/gettext.php | 12 - src/SPC/builder/macos/library/glfw.php | 26 - src/SPC/builder/macos/library/gmp.php | 15 - src/SPC/builder/macos/library/gmssl.php | 12 - src/SPC/builder/macos/library/grpc.php | 12 - src/SPC/builder/macos/library/icu.php | 27 - src/SPC/builder/macos/library/idn2.php | 12 - src/SPC/builder/macos/library/imagemagick.php | 15 - src/SPC/builder/macos/library/imap.php | 62 - src/SPC/builder/macos/library/jbig.php | 12 - src/SPC/builder/macos/library/krb5.php | 12 - src/SPC/builder/macos/library/ldap.php | 12 - src/SPC/builder/macos/library/lerc.php | 12 - src/SPC/builder/macos/library/libaom.php | 12 - src/SPC/builder/macos/library/libargon2.php | 12 - src/SPC/builder/macos/library/libavif.php | 29 - src/SPC/builder/macos/library/libcares.php | 12 - src/SPC/builder/macos/library/libde265.php | 12 - src/SPC/builder/macos/library/libedit.php | 12 - src/SPC/builder/macos/library/libevent.php | 12 - src/SPC/builder/macos/library/libffi.php | 24 - src/SPC/builder/macos/library/libheif.php | 12 - src/SPC/builder/macos/library/libiconv.php | 29 - src/SPC/builder/macos/library/libjpeg.php | 12 - src/SPC/builder/macos/library/libjxl.php | 12 - src/SPC/builder/macos/library/liblz4.php | 12 - .../builder/macos/library/libmaxminddb.php | 12 - .../builder/macos/library/libmemcached.php | 17 - src/SPC/builder/macos/library/libpng.php | 53 - src/SPC/builder/macos/library/librabbitmq.php | 12 - src/SPC/builder/macos/library/librdkafka.php | 12 - src/SPC/builder/macos/library/libsodium.php | 12 - src/SPC/builder/macos/library/libssh2.php | 12 - src/SPC/builder/macos/library/libtiff.php | 12 - .../builder/macos/library/libunistring.php | 12 - src/SPC/builder/macos/library/libuuid.php | 12 - src/SPC/builder/macos/library/libuv.php | 12 - src/SPC/builder/macos/library/libwebp.php | 29 - src/SPC/builder/macos/library/libxml2.php | 12 - src/SPC/builder/macos/library/libxslt.php | 15 - src/SPC/builder/macos/library/libyaml.php | 12 - src/SPC/builder/macos/library/libzip.php | 12 - src/SPC/builder/macos/library/mimalloc.php | 12 - src/SPC/builder/macos/library/ncurses.php | 15 - src/SPC/builder/macos/library/net_snmp.php | 12 - src/SPC/builder/macos/library/nghttp2.php | 29 - src/SPC/builder/macos/library/nghttp3.php | 12 - src/SPC/builder/macos/library/ngtcp2.php | 12 - src/SPC/builder/macos/library/onig.php | 12 - src/SPC/builder/macos/library/openssl.php | 69 - src/SPC/builder/macos/library/pkgconfig.php | 15 - src/SPC/builder/macos/library/postgresql.php | 12 - src/SPC/builder/macos/library/qdbm.php | 12 - src/SPC/builder/macos/library/re2c.php | 12 - src/SPC/builder/macos/library/readline.php | 12 - src/SPC/builder/macos/library/snappy.php | 12 - src/SPC/builder/macos/library/sqlite.php | 12 - src/SPC/builder/macos/library/tidy.php | 12 - src/SPC/builder/macos/library/unixodbc.php | 12 - src/SPC/builder/macos/library/watcher.php | 12 - src/SPC/builder/macos/library/xz.php | 12 - src/SPC/builder/macos/library/zlib.php | 29 - src/SPC/builder/macos/library/zstd.php | 12 - src/SPC/builder/traits/UnixLibraryTrait.php | 115 -- .../builder/traits/UnixSystemUtilTrait.php | 127 -- src/SPC/builder/traits/openssl.php | 38 - src/SPC/builder/unix/UnixBuilderBase.php | 488 ------ src/SPC/builder/unix/library/attr.php | 23 - src/SPC/builder/unix/library/brotli.php | 33 - src/SPC/builder/unix/library/bzip2.php | 25 - src/SPC/builder/unix/library/curl.php | 40 - src/SPC/builder/unix/library/fastlz.php | 24 - src/SPC/builder/unix/library/freetype.php | 34 - src/SPC/builder/unix/library/gettext.php | 41 - src/SPC/builder/unix/library/gmp.php | 23 - src/SPC/builder/unix/library/gmssl.php | 15 - src/SPC/builder/unix/library/grpc.php | 58 - src/SPC/builder/unix/library/icu.php | 24 - src/SPC/builder/unix/library/idn2.php | 27 - src/SPC/builder/unix/library/imagemagick.php | 75 - src/SPC/builder/unix/library/jbig.php | 27 - src/SPC/builder/unix/library/krb5.php | 60 - src/SPC/builder/unix/library/ldap.php | 45 - src/SPC/builder/unix/library/lerc.php | 16 - src/SPC/builder/unix/library/libacl.php | 32 - src/SPC/builder/unix/library/libaom.php | 27 - src/SPC/builder/unix/library/libargon2.php | 30 - src/SPC/builder/unix/library/libavif.php | 24 - src/SPC/builder/unix/library/libcares.php | 27 - src/SPC/builder/unix/library/libde265.php | 21 - src/SPC/builder/unix/library/libedit.php | 30 - src/SPC/builder/unix/library/libevent.php | 72 - src/SPC/builder/unix/library/libheif.php | 39 - src/SPC/builder/unix/library/libiconv.php | 22 - src/SPC/builder/unix/library/libjpeg.php | 26 - src/SPC/builder/unix/library/libjxl.php | 47 - src/SPC/builder/unix/library/liblz4.php | 37 - src/SPC/builder/unix/library/libmaxminddb.php | 20 - src/SPC/builder/unix/library/librabbitmq.php | 15 - src/SPC/builder/unix/library/librdkafka.php | 43 - src/SPC/builder/unix/library/libsodium.php | 16 - src/SPC/builder/unix/library/libssh2.php | 23 - src/SPC/builder/unix/library/libtiff.php | 44 - src/SPC/builder/unix/library/libunistring.php | 18 - src/SPC/builder/unix/library/libuuid.php | 34 - src/SPC/builder/unix/library/libuv.php | 19 - src/SPC/builder/unix/library/libwebp.php | 38 - src/SPC/builder/unix/library/libxml2.php | 49 - src/SPC/builder/unix/library/libxslt.php | 45 - src/SPC/builder/unix/library/libyaml.php | 35 - src/SPC/builder/unix/library/libzip.php | 30 - src/SPC/builder/unix/library/mimalloc.php | 25 - src/SPC/builder/unix/library/ncurses.php | 57 - src/SPC/builder/unix/library/net_snmp.php | 49 - src/SPC/builder/unix/library/nghttp2.php | 42 - src/SPC/builder/unix/library/nghttp3.php | 17 - src/SPC/builder/unix/library/ngtcp2.php | 46 - src/SPC/builder/unix/library/onig.php | 16 - src/SPC/builder/unix/library/pkgconfig.php | 31 - src/SPC/builder/unix/library/postgresql.php | 101 -- src/SPC/builder/unix/library/qdbm.php | 20 - src/SPC/builder/unix/library/re2c.php | 32 - src/SPC/builder/unix/library/readline.php | 21 - src/SPC/builder/unix/library/snappy.php | 21 - src/SPC/builder/unix/library/sqlite.php | 16 - src/SPC/builder/unix/library/tidy.php | 25 - src/SPC/builder/unix/library/unixodbc.php | 45 - src/SPC/builder/unix/library/watcher.php | 26 - src/SPC/builder/unix/library/xz.php | 24 - src/SPC/builder/unix/library/zlib.php | 16 - src/SPC/builder/unix/library/zstd.php | 22 - src/SPC/builder/windows/SystemUtil.php | 106 -- src/SPC/builder/windows/WindowsBuilder.php | 423 ----- .../windows/library/WindowsLibraryBase.php | 22 - src/SPC/builder/windows/library/brotli.php | 36 - src/SPC/builder/windows/library/bzip2.php | 21 - src/SPC/builder/windows/library/curl.php | 58 - src/SPC/builder/windows/library/freetype.php | 38 - src/SPC/builder/windows/library/gmssl.php | 33 - .../windows/library/icu_static_win.php | 27 - src/SPC/builder/windows/library/libaom.php | 41 - src/SPC/builder/windows/library/libavif.php | 40 - .../builder/windows/library/libffi_win.php | 52 - .../builder/windows/library/libiconv_win.php | 40 - src/SPC/builder/windows/library/libjpeg.php | 42 - src/SPC/builder/windows/library/libpng.php | 40 - .../builder/windows/library/librabbitmq.php | 36 - src/SPC/builder/windows/library/libsodium.php | 63 - src/SPC/builder/windows/library/libssh2.php | 38 - src/SPC/builder/windows/library/libwebp.php | 52 - src/SPC/builder/windows/library/libxml2.php | 44 - src/SPC/builder/windows/library/libyaml.php | 44 - src/SPC/builder/windows/library/libzip.php | 46 - src/SPC/builder/windows/library/nghttp2.php | 44 - src/SPC/builder/windows/library/nghttp3.php | 39 - src/SPC/builder/windows/library/ngtcp2.php | 40 - src/SPC/builder/windows/library/onig.php | 37 - src/SPC/builder/windows/library/openssl.php | 61 - .../windows/library/postgresql_win.php | 27 - .../builder/windows/library/pthreads4w.php | 29 - src/SPC/builder/windows/library/qdbm.php | 24 - src/SPC/builder/windows/library/sqlite.php | 21 - src/SPC/builder/windows/library/xz.php | 39 - src/SPC/builder/windows/library/zlib.php | 54 - src/SPC/command/BaseCommand.php | 213 --- src/SPC/command/BuildCommand.php | 24 - src/SPC/command/BuildLibsCommand.php | 71 - src/SPC/command/BuildPHPCommand.php | 315 ---- src/SPC/command/CraftCommand.php | 180 -- src/SPC/command/DeleteDownloadCommand.php | 81 - src/SPC/command/DoctorCommand.php | 95 -- src/SPC/command/DownloadCommand.php | 365 ---- src/SPC/command/DumpExtensionsCommand.php | 160 -- src/SPC/command/DumpLicenseCommand.php | 68 - src/SPC/command/ExtractCommand.php | 34 - src/SPC/command/InstallPkgCommand.php | 84 - src/SPC/command/MicroCombineCommand.php | 120 -- src/SPC/command/SPCConfigCommand.php | 55 - src/SPC/command/SwitchPhpVersionCommand.php | 65 - src/SPC/command/dev/AllExtCommand.php | 88 - src/SPC/command/dev/EnvCommand.php | 37 - src/SPC/command/dev/ExtVerCommand.php | 49 - .../command/dev/GenerateExtDepDocsCommand.php | 166 -- src/SPC/command/dev/GenerateExtDocCommand.php | 80 - .../command/dev/GenerateLibDepDocsCommand.php | 172 -- src/SPC/command/dev/LibVerCommand.php | 64 - src/SPC/command/dev/PackLibCommand.php | 175 -- src/SPC/command/dev/PhpVerCommand.php | 41 - src/SPC/command/dev/SortConfigCommand.php | 80 - src/SPC/doctor/AsCheckItem.php | 18 - src/SPC/doctor/AsFixItem.php | 11 - src/SPC/doctor/CheckResult.php | 46 - src/SPC/doctor/DoctorHandler.php | 63 - src/SPC/doctor/OptionalCheck.php | 11 - src/SPC/doctor/item/BSDToolCheckList.php | 62 - src/SPC/doctor/item/LinuxMuslCheck.php | 100 -- src/SPC/doctor/item/LinuxToolCheckList.php | 136 -- src/SPC/doctor/item/MacOSToolCheckList.php | 109 -- src/SPC/doctor/item/OSCheckList.php | 26 - src/SPC/doctor/item/PkgConfigCheck.php | 50 - src/SPC/doctor/item/Re2cVersionCheck.php | 53 - src/SPC/doctor/item/WindowsToolCheckList.php | 101 -- src/SPC/doctor/item/ZigCheck.php | 46 - src/SPC/exception/BuildFailureException.php | 13 - src/SPC/exception/DownloaderException.php | 13 - src/SPC/exception/EnvironmentException.php | 25 - src/SPC/exception/ExceptionHandler.php | 227 --- src/SPC/exception/ExecutionException.php | 58 - src/SPC/exception/FileSystemException.php | 7 - src/SPC/exception/InterruptException.php | 10 - src/SPC/exception/PatchException.php | 25 - src/SPC/exception/SPCException.php | 147 -- src/SPC/exception/SPCInternalException.php | 12 - src/SPC/exception/ValidationException.php | 61 - src/SPC/exception/WrongUsageException.php | 13 - src/SPC/store/Config.php | 211 --- src/SPC/store/CurlHook.php | 39 - src/SPC/store/DirDiff.php | 95 -- src/SPC/store/Downloader.php | 711 -------- src/SPC/store/FileSystem.php | 761 --------- src/SPC/store/LockFile.php | 215 --- src/SPC/store/PackageManager.php | 110 -- src/SPC/store/SourceManager.php | 97 -- src/SPC/store/SourcePatcher.php | 685 -------- src/SPC/store/pkg/CustomPackage.php | 49 - src/SPC/store/pkg/GoXcaddy.php | 106 -- src/SPC/store/pkg/Zig.php | 163 -- src/SPC/store/scripts/zig-cc.sh | 60 - src/SPC/store/source/CustomSourceBase.php | 28 - src/SPC/store/source/PhpSource.php | 59 - src/SPC/store/source/PostgreSQLSource.php | 29 - src/SPC/toolchain/ClangNativeToolchain.php | 53 - src/SPC/toolchain/GccNativeToolchain.php | 50 - src/SPC/toolchain/MSVCToolchain.php | 17 - src/SPC/toolchain/MuslToolchain.php | 50 - src/SPC/toolchain/ToolchainInterface.php | 38 - src/SPC/toolchain/ToolchainManager.php | 77 - src/SPC/toolchain/ZigToolchain.php | 80 - src/SPC/util/AttributeMapper.php | 133 -- src/SPC/util/ConfigValidator.php | 688 -------- src/SPC/util/CustomExt.php | 24 - src/SPC/util/DependencyUtil.php | 233 --- src/SPC/util/GlobalEnvManager.php | 177 -- src/SPC/util/GlobalValueTrait.php | 28 - src/SPC/util/LicenseDumper.php | 142 -- src/SPC/util/PkgConfigUtil.php | 124 -- src/SPC/util/SPCConfigUtil.php | 360 ---- src/SPC/util/SPCTarget.php | 130 -- src/SPC/util/executor/Executor.php | 20 - .../util/executor/UnixAutoconfExecutor.php | 167 -- src/SPC/util/executor/UnixCMakeExecutor.php | 230 --- src/SPC/util/shell/Shell.php | 173 -- src/SPC/util/shell/UnixShell.php | 116 -- src/SPC/util/shell/WindowsCmd.php | 135 -- src/StaticPHP/Package/PackageInstaller.php | 5 +- 428 files changed, 3 insertions(+), 31014 deletions(-) delete mode 100755 bin/spc-gnu-docker delete mode 100644 config/artifact.json delete mode 100644 config/ext.json delete mode 100644 config/lib.json delete mode 100644 config/pkg.ext.json delete mode 100644 config/pkg.lib.json delete mode 100644 config/pre-built.json delete mode 100644 config/source.json delete mode 100644 src/SPC/ConsoleApplication.php delete mode 100644 src/SPC/builder/BuilderBase.php delete mode 100644 src/SPC/builder/BuilderProvider.php delete mode 100644 src/SPC/builder/Extension.php delete mode 100644 src/SPC/builder/LibraryBase.php delete mode 100644 src/SPC/builder/LibraryInterface.php delete mode 100644 src/SPC/builder/extension/amqp.php delete mode 100644 src/SPC/builder/extension/bz2.php delete mode 100644 src/SPC/builder/extension/com_dotnet.php delete mode 100644 src/SPC/builder/extension/curl.php delete mode 100644 src/SPC/builder/extension/dba.php delete mode 100644 src/SPC/builder/extension/dio.php delete mode 100644 src/SPC/builder/extension/dom.php delete mode 100644 src/SPC/builder/extension/enchant.php delete mode 100644 src/SPC/builder/extension/ev.php delete mode 100644 src/SPC/builder/extension/event.php delete mode 100644 src/SPC/builder/extension/ffi.php delete mode 100644 src/SPC/builder/extension/gd.php delete mode 100644 src/SPC/builder/extension/gettext.php delete mode 100644 src/SPC/builder/extension/glfw.php delete mode 100644 src/SPC/builder/extension/gmssl.php delete mode 100644 src/SPC/builder/extension/grpc.php delete mode 100644 src/SPC/builder/extension/iconv.php delete mode 100644 src/SPC/builder/extension/imagick.php delete mode 100644 src/SPC/builder/extension/imap.php delete mode 100644 src/SPC/builder/extension/intl.php delete mode 100644 src/SPC/builder/extension/ldap.php delete mode 100644 src/SPC/builder/extension/lz4.php delete mode 100644 src/SPC/builder/extension/maxminddb.php delete mode 100644 src/SPC/builder/extension/mbregex.php delete mode 100644 src/SPC/builder/extension/mbstring.php delete mode 100644 src/SPC/builder/extension/memcache.php delete mode 100644 src/SPC/builder/extension/memcached.php delete mode 100644 src/SPC/builder/extension/mongodb.php delete mode 100644 src/SPC/builder/extension/mysqlnd_ed25519.php delete mode 100644 src/SPC/builder/extension/mysqlnd_parsec.php delete mode 100644 src/SPC/builder/extension/odbc.php delete mode 100644 src/SPC/builder/extension/opcache.php delete mode 100644 src/SPC/builder/extension/openssl.php delete mode 100644 src/SPC/builder/extension/opentelemetry.php delete mode 100644 src/SPC/builder/extension/parallel.php delete mode 100644 src/SPC/builder/extension/password_argon2.php delete mode 100644 src/SPC/builder/extension/pdo_odbc.php delete mode 100644 src/SPC/builder/extension/pdo_pgsql.php delete mode 100644 src/SPC/builder/extension/pdo_sqlite.php delete mode 100644 src/SPC/builder/extension/pgsql.php delete mode 100644 src/SPC/builder/extension/phar.php delete mode 100644 src/SPC/builder/extension/protobuf.php delete mode 100644 src/SPC/builder/extension/rar.php delete mode 100644 src/SPC/builder/extension/rdkafka.php delete mode 100644 src/SPC/builder/extension/readline.php delete mode 100644 src/SPC/builder/extension/redis.php delete mode 100644 src/SPC/builder/extension/simdjson.php delete mode 100644 src/SPC/builder/extension/snappy.php delete mode 100644 src/SPC/builder/extension/snmp.php delete mode 100644 src/SPC/builder/extension/spx.php delete mode 100644 src/SPC/builder/extension/sqlsrv.php delete mode 100644 src/SPC/builder/extension/ssh2.php delete mode 100644 src/SPC/builder/extension/swoole.php delete mode 100644 src/SPC/builder/extension/swoole_hook_mysql.php delete mode 100644 src/SPC/builder/extension/swoole_hook_odbc.php delete mode 100644 src/SPC/builder/extension/swoole_hook_pgsql.php delete mode 100644 src/SPC/builder/extension/swoole_hook_sqlite.php delete mode 100644 src/SPC/builder/extension/swow.php delete mode 100644 src/SPC/builder/extension/trader.php delete mode 100644 src/SPC/builder/extension/uv.php delete mode 100644 src/SPC/builder/extension/xhprof.php delete mode 100644 src/SPC/builder/extension/xlswriter.php delete mode 100644 src/SPC/builder/extension/xml.php delete mode 100644 src/SPC/builder/extension/yac.php delete mode 100644 src/SPC/builder/extension/zip.php delete mode 100644 src/SPC/builder/extension/zlib.php delete mode 100644 src/SPC/builder/extension/zstd.php delete mode 100644 src/SPC/builder/freebsd/BSDBuilder.php delete mode 100644 src/SPC/builder/freebsd/SystemUtil.php delete mode 100644 src/SPC/builder/freebsd/library/BSDLibraryBase.php delete mode 100644 src/SPC/builder/freebsd/library/bzip2.php delete mode 100644 src/SPC/builder/freebsd/library/curl.php delete mode 100644 src/SPC/builder/freebsd/library/onig.php delete mode 100644 src/SPC/builder/freebsd/library/openssl.php delete mode 100644 src/SPC/builder/freebsd/library/pkgconfig.php delete mode 100644 src/SPC/builder/freebsd/library/watcher.php delete mode 100644 src/SPC/builder/freebsd/library/zlib.php delete mode 100644 src/SPC/builder/linux/LinuxBuilder.php delete mode 100644 src/SPC/builder/linux/SystemUtil.php delete mode 100644 src/SPC/builder/linux/library/LinuxLibraryBase.php delete mode 100644 src/SPC/builder/linux/library/attr.php delete mode 100644 src/SPC/builder/linux/library/brotli.php delete mode 100644 src/SPC/builder/linux/library/bzip2.php delete mode 100644 src/SPC/builder/linux/library/curl.php delete mode 100644 src/SPC/builder/linux/library/fastlz.php delete mode 100644 src/SPC/builder/linux/library/freetype.php delete mode 100644 src/SPC/builder/linux/library/gettext.php delete mode 100644 src/SPC/builder/linux/library/gmp.php delete mode 100644 src/SPC/builder/linux/library/gmssl.php delete mode 100644 src/SPC/builder/linux/library/grpc.php delete mode 100644 src/SPC/builder/linux/library/icu.php delete mode 100644 src/SPC/builder/linux/library/idn2.php delete mode 100644 src/SPC/builder/linux/library/imagemagick.php delete mode 100644 src/SPC/builder/linux/library/imap.php delete mode 100644 src/SPC/builder/linux/library/jbig.php delete mode 100644 src/SPC/builder/linux/library/krb5.php delete mode 100644 src/SPC/builder/linux/library/ldap.php delete mode 100644 src/SPC/builder/linux/library/lerc.php delete mode 100644 src/SPC/builder/linux/library/libacl.php delete mode 100644 src/SPC/builder/linux/library/libaom.php delete mode 100644 src/SPC/builder/linux/library/libargon2.php delete mode 100644 src/SPC/builder/linux/library/libavif.php delete mode 100644 src/SPC/builder/linux/library/libcares.php delete mode 100644 src/SPC/builder/linux/library/libde265.php delete mode 100644 src/SPC/builder/linux/library/libedit.php delete mode 100644 src/SPC/builder/linux/library/libevent.php delete mode 100644 src/SPC/builder/linux/library/libffi.php delete mode 100644 src/SPC/builder/linux/library/libheif.php delete mode 100644 src/SPC/builder/linux/library/libiconv.php delete mode 100644 src/SPC/builder/linux/library/libjpeg.php delete mode 100644 src/SPC/builder/linux/library/libjxl.php delete mode 100644 src/SPC/builder/linux/library/liblz4.php delete mode 100644 src/SPC/builder/linux/library/libmaxminddb.php delete mode 100644 src/SPC/builder/linux/library/libmemcached.php delete mode 100644 src/SPC/builder/linux/library/libpng.php delete mode 100644 src/SPC/builder/linux/library/librabbitmq.php delete mode 100644 src/SPC/builder/linux/library/librdkafka.php delete mode 100644 src/SPC/builder/linux/library/libsodium.php delete mode 100644 src/SPC/builder/linux/library/libssh2.php delete mode 100644 src/SPC/builder/linux/library/libtiff.php delete mode 100644 src/SPC/builder/linux/library/libunistring.php delete mode 100644 src/SPC/builder/linux/library/liburing.php delete mode 100644 src/SPC/builder/linux/library/libuuid.php delete mode 100644 src/SPC/builder/linux/library/libuv.php delete mode 100644 src/SPC/builder/linux/library/libwebp.php delete mode 100644 src/SPC/builder/linux/library/libxml2.php delete mode 100644 src/SPC/builder/linux/library/libxslt.php delete mode 100644 src/SPC/builder/linux/library/libyaml.php delete mode 100644 src/SPC/builder/linux/library/libzip.php delete mode 100644 src/SPC/builder/linux/library/mimalloc.php delete mode 100644 src/SPC/builder/linux/library/ncurses.php delete mode 100644 src/SPC/builder/linux/library/net_snmp.php delete mode 100644 src/SPC/builder/linux/library/nghttp2.php delete mode 100644 src/SPC/builder/linux/library/nghttp3.php delete mode 100644 src/SPC/builder/linux/library/ngtcp2.php delete mode 100644 src/SPC/builder/linux/library/onig.php delete mode 100644 src/SPC/builder/linux/library/openssl.php delete mode 100644 src/SPC/builder/linux/library/pkgconfig.php delete mode 100644 src/SPC/builder/linux/library/postgresql.php delete mode 100644 src/SPC/builder/linux/library/qdbm.php delete mode 100644 src/SPC/builder/linux/library/re2c.php delete mode 100644 src/SPC/builder/linux/library/readline.php delete mode 100644 src/SPC/builder/linux/library/snappy.php delete mode 100644 src/SPC/builder/linux/library/sqlite.php delete mode 100644 src/SPC/builder/linux/library/tidy.php delete mode 100644 src/SPC/builder/linux/library/unixodbc.php delete mode 100644 src/SPC/builder/linux/library/watcher.php delete mode 100644 src/SPC/builder/linux/library/xz.php delete mode 100644 src/SPC/builder/linux/library/zlib.php delete mode 100644 src/SPC/builder/linux/library/zstd.php delete mode 100644 src/SPC/builder/macos/MacOSBuilder.php delete mode 100644 src/SPC/builder/macos/SystemUtil.php delete mode 100644 src/SPC/builder/macos/library/MacOSLibraryBase.php delete mode 100644 src/SPC/builder/macos/library/brotli.php delete mode 100644 src/SPC/builder/macos/library/bzip2.php delete mode 100644 src/SPC/builder/macos/library/curl.php delete mode 100644 src/SPC/builder/macos/library/fastlz.php delete mode 100644 src/SPC/builder/macos/library/freetype.php delete mode 100644 src/SPC/builder/macos/library/gettext.php delete mode 100644 src/SPC/builder/macos/library/glfw.php delete mode 100644 src/SPC/builder/macos/library/gmp.php delete mode 100644 src/SPC/builder/macos/library/gmssl.php delete mode 100644 src/SPC/builder/macos/library/grpc.php delete mode 100644 src/SPC/builder/macos/library/icu.php delete mode 100644 src/SPC/builder/macos/library/idn2.php delete mode 100644 src/SPC/builder/macos/library/imagemagick.php delete mode 100644 src/SPC/builder/macos/library/imap.php delete mode 100644 src/SPC/builder/macos/library/jbig.php delete mode 100644 src/SPC/builder/macos/library/krb5.php delete mode 100644 src/SPC/builder/macos/library/ldap.php delete mode 100644 src/SPC/builder/macos/library/lerc.php delete mode 100644 src/SPC/builder/macos/library/libaom.php delete mode 100644 src/SPC/builder/macos/library/libargon2.php delete mode 100644 src/SPC/builder/macos/library/libavif.php delete mode 100644 src/SPC/builder/macos/library/libcares.php delete mode 100644 src/SPC/builder/macos/library/libde265.php delete mode 100644 src/SPC/builder/macos/library/libedit.php delete mode 100644 src/SPC/builder/macos/library/libevent.php delete mode 100644 src/SPC/builder/macos/library/libffi.php delete mode 100644 src/SPC/builder/macos/library/libheif.php delete mode 100644 src/SPC/builder/macos/library/libiconv.php delete mode 100644 src/SPC/builder/macos/library/libjpeg.php delete mode 100644 src/SPC/builder/macos/library/libjxl.php delete mode 100644 src/SPC/builder/macos/library/liblz4.php delete mode 100644 src/SPC/builder/macos/library/libmaxminddb.php delete mode 100644 src/SPC/builder/macos/library/libmemcached.php delete mode 100644 src/SPC/builder/macos/library/libpng.php delete mode 100644 src/SPC/builder/macos/library/librabbitmq.php delete mode 100644 src/SPC/builder/macos/library/librdkafka.php delete mode 100644 src/SPC/builder/macos/library/libsodium.php delete mode 100644 src/SPC/builder/macos/library/libssh2.php delete mode 100644 src/SPC/builder/macos/library/libtiff.php delete mode 100644 src/SPC/builder/macos/library/libunistring.php delete mode 100644 src/SPC/builder/macos/library/libuuid.php delete mode 100644 src/SPC/builder/macos/library/libuv.php delete mode 100644 src/SPC/builder/macos/library/libwebp.php delete mode 100644 src/SPC/builder/macos/library/libxml2.php delete mode 100644 src/SPC/builder/macos/library/libxslt.php delete mode 100644 src/SPC/builder/macos/library/libyaml.php delete mode 100644 src/SPC/builder/macos/library/libzip.php delete mode 100644 src/SPC/builder/macos/library/mimalloc.php delete mode 100644 src/SPC/builder/macos/library/ncurses.php delete mode 100644 src/SPC/builder/macos/library/net_snmp.php delete mode 100644 src/SPC/builder/macos/library/nghttp2.php delete mode 100644 src/SPC/builder/macos/library/nghttp3.php delete mode 100644 src/SPC/builder/macos/library/ngtcp2.php delete mode 100644 src/SPC/builder/macos/library/onig.php delete mode 100644 src/SPC/builder/macos/library/openssl.php delete mode 100644 src/SPC/builder/macos/library/pkgconfig.php delete mode 100644 src/SPC/builder/macos/library/postgresql.php delete mode 100644 src/SPC/builder/macos/library/qdbm.php delete mode 100644 src/SPC/builder/macos/library/re2c.php delete mode 100644 src/SPC/builder/macos/library/readline.php delete mode 100644 src/SPC/builder/macos/library/snappy.php delete mode 100644 src/SPC/builder/macos/library/sqlite.php delete mode 100644 src/SPC/builder/macos/library/tidy.php delete mode 100644 src/SPC/builder/macos/library/unixodbc.php delete mode 100644 src/SPC/builder/macos/library/watcher.php delete mode 100644 src/SPC/builder/macos/library/xz.php delete mode 100644 src/SPC/builder/macos/library/zlib.php delete mode 100644 src/SPC/builder/macos/library/zstd.php delete mode 100644 src/SPC/builder/traits/UnixLibraryTrait.php delete mode 100644 src/SPC/builder/traits/UnixSystemUtilTrait.php delete mode 100644 src/SPC/builder/traits/openssl.php delete mode 100644 src/SPC/builder/unix/UnixBuilderBase.php delete mode 100644 src/SPC/builder/unix/library/attr.php delete mode 100644 src/SPC/builder/unix/library/brotli.php delete mode 100644 src/SPC/builder/unix/library/bzip2.php delete mode 100644 src/SPC/builder/unix/library/curl.php delete mode 100644 src/SPC/builder/unix/library/fastlz.php delete mode 100644 src/SPC/builder/unix/library/freetype.php delete mode 100644 src/SPC/builder/unix/library/gettext.php delete mode 100644 src/SPC/builder/unix/library/gmp.php delete mode 100644 src/SPC/builder/unix/library/gmssl.php delete mode 100644 src/SPC/builder/unix/library/grpc.php delete mode 100644 src/SPC/builder/unix/library/icu.php delete mode 100644 src/SPC/builder/unix/library/idn2.php delete mode 100644 src/SPC/builder/unix/library/imagemagick.php delete mode 100644 src/SPC/builder/unix/library/jbig.php delete mode 100644 src/SPC/builder/unix/library/krb5.php delete mode 100644 src/SPC/builder/unix/library/ldap.php delete mode 100644 src/SPC/builder/unix/library/lerc.php delete mode 100644 src/SPC/builder/unix/library/libacl.php delete mode 100644 src/SPC/builder/unix/library/libaom.php delete mode 100644 src/SPC/builder/unix/library/libargon2.php delete mode 100644 src/SPC/builder/unix/library/libavif.php delete mode 100644 src/SPC/builder/unix/library/libcares.php delete mode 100644 src/SPC/builder/unix/library/libde265.php delete mode 100644 src/SPC/builder/unix/library/libedit.php delete mode 100644 src/SPC/builder/unix/library/libevent.php delete mode 100644 src/SPC/builder/unix/library/libheif.php delete mode 100644 src/SPC/builder/unix/library/libiconv.php delete mode 100644 src/SPC/builder/unix/library/libjpeg.php delete mode 100644 src/SPC/builder/unix/library/libjxl.php delete mode 100644 src/SPC/builder/unix/library/liblz4.php delete mode 100644 src/SPC/builder/unix/library/libmaxminddb.php delete mode 100644 src/SPC/builder/unix/library/librabbitmq.php delete mode 100644 src/SPC/builder/unix/library/librdkafka.php delete mode 100644 src/SPC/builder/unix/library/libsodium.php delete mode 100644 src/SPC/builder/unix/library/libssh2.php delete mode 100644 src/SPC/builder/unix/library/libtiff.php delete mode 100644 src/SPC/builder/unix/library/libunistring.php delete mode 100644 src/SPC/builder/unix/library/libuuid.php delete mode 100644 src/SPC/builder/unix/library/libuv.php delete mode 100644 src/SPC/builder/unix/library/libwebp.php delete mode 100644 src/SPC/builder/unix/library/libxml2.php delete mode 100644 src/SPC/builder/unix/library/libxslt.php delete mode 100644 src/SPC/builder/unix/library/libyaml.php delete mode 100644 src/SPC/builder/unix/library/libzip.php delete mode 100644 src/SPC/builder/unix/library/mimalloc.php delete mode 100644 src/SPC/builder/unix/library/ncurses.php delete mode 100644 src/SPC/builder/unix/library/net_snmp.php delete mode 100644 src/SPC/builder/unix/library/nghttp2.php delete mode 100644 src/SPC/builder/unix/library/nghttp3.php delete mode 100644 src/SPC/builder/unix/library/ngtcp2.php delete mode 100644 src/SPC/builder/unix/library/onig.php delete mode 100644 src/SPC/builder/unix/library/pkgconfig.php delete mode 100644 src/SPC/builder/unix/library/postgresql.php delete mode 100644 src/SPC/builder/unix/library/qdbm.php delete mode 100644 src/SPC/builder/unix/library/re2c.php delete mode 100644 src/SPC/builder/unix/library/readline.php delete mode 100644 src/SPC/builder/unix/library/snappy.php delete mode 100644 src/SPC/builder/unix/library/sqlite.php delete mode 100644 src/SPC/builder/unix/library/tidy.php delete mode 100644 src/SPC/builder/unix/library/unixodbc.php delete mode 100644 src/SPC/builder/unix/library/watcher.php delete mode 100644 src/SPC/builder/unix/library/xz.php delete mode 100644 src/SPC/builder/unix/library/zlib.php delete mode 100644 src/SPC/builder/unix/library/zstd.php delete mode 100644 src/SPC/builder/windows/SystemUtil.php delete mode 100644 src/SPC/builder/windows/WindowsBuilder.php delete mode 100644 src/SPC/builder/windows/library/WindowsLibraryBase.php delete mode 100644 src/SPC/builder/windows/library/brotli.php delete mode 100644 src/SPC/builder/windows/library/bzip2.php delete mode 100644 src/SPC/builder/windows/library/curl.php delete mode 100644 src/SPC/builder/windows/library/freetype.php delete mode 100644 src/SPC/builder/windows/library/gmssl.php delete mode 100644 src/SPC/builder/windows/library/icu_static_win.php delete mode 100644 src/SPC/builder/windows/library/libaom.php delete mode 100644 src/SPC/builder/windows/library/libavif.php delete mode 100644 src/SPC/builder/windows/library/libffi_win.php delete mode 100644 src/SPC/builder/windows/library/libiconv_win.php delete mode 100644 src/SPC/builder/windows/library/libjpeg.php delete mode 100644 src/SPC/builder/windows/library/libpng.php delete mode 100644 src/SPC/builder/windows/library/librabbitmq.php delete mode 100644 src/SPC/builder/windows/library/libsodium.php delete mode 100644 src/SPC/builder/windows/library/libssh2.php delete mode 100644 src/SPC/builder/windows/library/libwebp.php delete mode 100644 src/SPC/builder/windows/library/libxml2.php delete mode 100644 src/SPC/builder/windows/library/libyaml.php delete mode 100644 src/SPC/builder/windows/library/libzip.php delete mode 100644 src/SPC/builder/windows/library/nghttp2.php delete mode 100644 src/SPC/builder/windows/library/nghttp3.php delete mode 100644 src/SPC/builder/windows/library/ngtcp2.php delete mode 100644 src/SPC/builder/windows/library/onig.php delete mode 100644 src/SPC/builder/windows/library/openssl.php delete mode 100644 src/SPC/builder/windows/library/postgresql_win.php delete mode 100644 src/SPC/builder/windows/library/pthreads4w.php delete mode 100644 src/SPC/builder/windows/library/qdbm.php delete mode 100644 src/SPC/builder/windows/library/sqlite.php delete mode 100644 src/SPC/builder/windows/library/xz.php delete mode 100644 src/SPC/builder/windows/library/zlib.php delete mode 100644 src/SPC/command/BaseCommand.php delete mode 100644 src/SPC/command/BuildCommand.php delete mode 100644 src/SPC/command/BuildLibsCommand.php delete mode 100644 src/SPC/command/BuildPHPCommand.php delete mode 100644 src/SPC/command/CraftCommand.php delete mode 100644 src/SPC/command/DeleteDownloadCommand.php delete mode 100644 src/SPC/command/DoctorCommand.php delete mode 100644 src/SPC/command/DownloadCommand.php delete mode 100644 src/SPC/command/DumpExtensionsCommand.php delete mode 100644 src/SPC/command/DumpLicenseCommand.php delete mode 100644 src/SPC/command/ExtractCommand.php delete mode 100644 src/SPC/command/InstallPkgCommand.php delete mode 100644 src/SPC/command/MicroCombineCommand.php delete mode 100644 src/SPC/command/SPCConfigCommand.php delete mode 100644 src/SPC/command/SwitchPhpVersionCommand.php delete mode 100644 src/SPC/command/dev/AllExtCommand.php delete mode 100644 src/SPC/command/dev/EnvCommand.php delete mode 100644 src/SPC/command/dev/ExtVerCommand.php delete mode 100644 src/SPC/command/dev/GenerateExtDepDocsCommand.php delete mode 100644 src/SPC/command/dev/GenerateExtDocCommand.php delete mode 100644 src/SPC/command/dev/GenerateLibDepDocsCommand.php delete mode 100644 src/SPC/command/dev/LibVerCommand.php delete mode 100644 src/SPC/command/dev/PackLibCommand.php delete mode 100644 src/SPC/command/dev/PhpVerCommand.php delete mode 100644 src/SPC/command/dev/SortConfigCommand.php delete mode 100644 src/SPC/doctor/AsCheckItem.php delete mode 100644 src/SPC/doctor/AsFixItem.php delete mode 100644 src/SPC/doctor/CheckResult.php delete mode 100644 src/SPC/doctor/DoctorHandler.php delete mode 100644 src/SPC/doctor/OptionalCheck.php delete mode 100644 src/SPC/doctor/item/BSDToolCheckList.php delete mode 100644 src/SPC/doctor/item/LinuxMuslCheck.php delete mode 100644 src/SPC/doctor/item/LinuxToolCheckList.php delete mode 100644 src/SPC/doctor/item/MacOSToolCheckList.php delete mode 100644 src/SPC/doctor/item/OSCheckList.php delete mode 100644 src/SPC/doctor/item/PkgConfigCheck.php delete mode 100644 src/SPC/doctor/item/Re2cVersionCheck.php delete mode 100644 src/SPC/doctor/item/WindowsToolCheckList.php delete mode 100644 src/SPC/doctor/item/ZigCheck.php delete mode 100644 src/SPC/exception/BuildFailureException.php delete mode 100644 src/SPC/exception/DownloaderException.php delete mode 100644 src/SPC/exception/EnvironmentException.php delete mode 100644 src/SPC/exception/ExceptionHandler.php delete mode 100644 src/SPC/exception/ExecutionException.php delete mode 100644 src/SPC/exception/FileSystemException.php delete mode 100644 src/SPC/exception/InterruptException.php delete mode 100644 src/SPC/exception/PatchException.php delete mode 100644 src/SPC/exception/SPCException.php delete mode 100644 src/SPC/exception/SPCInternalException.php delete mode 100644 src/SPC/exception/ValidationException.php delete mode 100644 src/SPC/exception/WrongUsageException.php delete mode 100644 src/SPC/store/Config.php delete mode 100644 src/SPC/store/CurlHook.php delete mode 100644 src/SPC/store/DirDiff.php delete mode 100644 src/SPC/store/Downloader.php delete mode 100644 src/SPC/store/FileSystem.php delete mode 100644 src/SPC/store/LockFile.php delete mode 100644 src/SPC/store/PackageManager.php delete mode 100644 src/SPC/store/SourceManager.php delete mode 100644 src/SPC/store/SourcePatcher.php delete mode 100644 src/SPC/store/pkg/CustomPackage.php delete mode 100644 src/SPC/store/pkg/GoXcaddy.php delete mode 100644 src/SPC/store/pkg/Zig.php delete mode 100644 src/SPC/store/scripts/zig-cc.sh delete mode 100644 src/SPC/store/source/CustomSourceBase.php delete mode 100644 src/SPC/store/source/PhpSource.php delete mode 100644 src/SPC/store/source/PostgreSQLSource.php delete mode 100644 src/SPC/toolchain/ClangNativeToolchain.php delete mode 100644 src/SPC/toolchain/GccNativeToolchain.php delete mode 100644 src/SPC/toolchain/MSVCToolchain.php delete mode 100644 src/SPC/toolchain/MuslToolchain.php delete mode 100644 src/SPC/toolchain/ToolchainInterface.php delete mode 100644 src/SPC/toolchain/ToolchainManager.php delete mode 100644 src/SPC/toolchain/ZigToolchain.php delete mode 100644 src/SPC/util/AttributeMapper.php delete mode 100644 src/SPC/util/ConfigValidator.php delete mode 100644 src/SPC/util/CustomExt.php delete mode 100644 src/SPC/util/DependencyUtil.php delete mode 100644 src/SPC/util/GlobalEnvManager.php delete mode 100644 src/SPC/util/GlobalValueTrait.php delete mode 100644 src/SPC/util/LicenseDumper.php delete mode 100644 src/SPC/util/PkgConfigUtil.php delete mode 100644 src/SPC/util/SPCConfigUtil.php delete mode 100644 src/SPC/util/SPCTarget.php delete mode 100644 src/SPC/util/executor/Executor.php delete mode 100644 src/SPC/util/executor/UnixAutoconfExecutor.php delete mode 100644 src/SPC/util/executor/UnixCMakeExecutor.php delete mode 100644 src/SPC/util/shell/Shell.php delete mode 100644 src/SPC/util/shell/UnixShell.php delete mode 100644 src/SPC/util/shell/WindowsCmd.php diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker index dc6eaa36b..cc223ba05 100755 --- a/bin/spc-alpine-docker +++ b/bin/spc-alpine-docker @@ -122,7 +122,7 @@ COPY ./composer.* /app/ ADD ./bin /app/bin RUN composer install --no-dev ADD ./config /app/config -ADD ./spc.registry.json /app/spc.registry.json +ADD ./spc.registry.yml /app/spc.registry.yml RUN bin/spc doctor --auto-fix RUN bin/spc install-pkg upx diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker deleted file mode 100755 index 286ef9859..000000000 --- a/bin/spc-gnu-docker +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env bash - -set -e - -# This file is using docker to run commands -SPC_DOCKER_VERSION=v6 - -# Detect docker can run -if ! which docker >/dev/null; then - echo "Docker is not installed, please install docker first !" - exit 1 -fi -DOCKER_EXECUTABLE="docker" -# shellcheck disable=SC2046 -if [ $(id -u) -ne 0 ]; then - if ! docker info > /dev/null 2>&1; then - if [ "$SPC_USE_SUDO" != "yes" ] && [ "$SPC_DOCKER_DEBUG" != "yes" ]; then - echo "Docker command requires sudo" - # shellcheck disable=SC2039 - echo -n 'To use sudo to run docker, run "export SPC_USE_SUDO=yes" and run command again' - exit 1 - fi - DOCKER_EXECUTABLE="sudo docker" - fi -fi - -# Convert uname to gnu arch -CURRENT_ARCH=$(uname -m) -if [ "$CURRENT_ARCH" = "arm64" ]; then - CURRENT_ARCH=aarch64 -fi -if [ -z "$SPC_USE_ARCH" ]; then - SPC_USE_ARCH=$CURRENT_ARCH -fi -# parse SPC_USE_ARCH -case $SPC_USE_ARCH in -x86_64|amd64) - SPC_USE_ARCH=x86_64 - SPC_USE_ARCH_DOCKER=amd64 - if [ "$CURRENT_ARCH" != "x86_64" ]; then - PLATFORM_ARG="--platform linux/amd64" - fi - ;; -aarch64|arm64) - SPC_USE_ARCH=aarch64 - SPC_USE_ARCH_DOCKER=arm64 - if [ "$CURRENT_ARCH" != "aarch64" ]; then - PLATFORM_ARG="--platform linux/arm64" - fi - ;; -*) - echo "Current arch is not supported to run in docker: $SPC_USE_ARCH" - exit 1 - ;; -esac -# detect if we need to use qemu-static -if [ "$SPC_USE_ARCH" != "$CURRENT_ARCH" ]; then - if [ "$(uname -s)" = "Linux" ]; then - echo "* Using different arch needs to setup qemu-static for docker !" - $DOCKER_EXECUTABLE run --rm --privileged multiarch/qemu-user-static --reset -p yes > /dev/null - fi -fi - -# Detect docker env is setup -if ! $DOCKER_EXECUTABLE images | grep -q cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION; then - echo "Docker container does not exist. Building docker image ..." - $DOCKER_EXECUTABLE buildx build $PLATFORM_ARG -t cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION -f- . <> /etc/bashrc -RUN source /etc/bashrc -RUN yum install -y which - -RUN curl -o cmake.tgz -#fSL https://github.com/Kitware/CMake/releases/download/v3.31.4/cmake-3.31.4-linux-$SPC_USE_ARCH.tar.gz && \ - mkdir /cmake && \ - tar -xzf cmake.tgz -C /cmake --strip-components 1 - -WORKDIR /app -COPY ./composer.* /app/ -ADD ./bin/setup-runtime /app/bin/setup-runtime -ADD ./bin/spc /app/bin/spc -RUN /app/bin/setup-runtime -ADD ./src /app/src -RUN /app/bin/php /app/bin/composer install --no-dev -ENV SPC_LIBC=glibc -ENV PATH="/app/bin:/cmake/bin:/opt/rh/devtoolset-10/root/usr/bin:\$PATH" - -ADD ./config /app/config -RUN CC=gcc bin/spc doctor --auto-fix --debug -RUN bin/spc install-pkg upx -RUN if [ -f /app/buildroot/bin/re2c ]; then \ - cp /app/buildroot/bin/re2c /usr/local/bin/re2c ;\ - fi - -RUN curl -o make.tgz -fsSL https://ftp.gnu.org/gnu/make/make-4.4.tar.gz && \ - tar -zxvf make.tgz && \ - cd make-4.4 && \ - ./configure && \ - make && \ - make install && \ - ln -sf /usr/local/bin/make /usr/bin/make - -RUN curl -o automake.tgz -fsSL https://ftp.gnu.org/gnu/automake/automake-1.17.tar.xz && \ - tar -xvf automake.tgz && \ - cd automake-1.17 && \ - ./configure && \ - make && \ - make install && \ - ln -sf /usr/local/bin/automake /usr/bin/automake - -RUN mv /app/pkgroot/\$(uname -m)-linux /app/pkgroot-private -ADD bin/docker-entrypoint.sh /bin/docker-entrypoint.sh -RUN chmod +x /bin/docker-entrypoint.sh -ENTRYPOINT ["/bin/docker-entrypoint.sh"] -EOF -fi - -# Check if in ci (local terminal can execute with -it) -if [ -t 0 ]; then - INTERACT=-it -else - INTERACT='' -fi - -# Mounting volumes -MOUNT_LIST="" -# shellcheck disable=SC2089 -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/config:/app/config" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/src:/app/src" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/buildroot:/app/buildroot" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" -MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log" -if [ -f "$(pwd)/craft.yml" ]; then - MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" -fi - -# Apply env in temp env file -echo 'SPC_DEFAULT_C_FLAGS=-fPIC' > /tmp/spc-gnu-docker.env -echo 'SPC_LIBC=glibc' >> /tmp/spc-gnu-docker.env - -# Environment variable passthrough -ENV_LIST="" -ENV_LIST="$ENV_LIST -e SPC_FIX_DEPLOY_ROOT="$(pwd)"" -if [ ! -z "$GITHUB_TOKEN" ]; then - ENV_LIST="$ENV_LIST -e GITHUB_TOKEN=$GITHUB_TOKEN" -fi - -# Intercept and rewrite --with-frankenphp-app option, and mount host path to /app/app -FRANKENPHP_APP_PATH="" -NEW_ARGS=() -while [ $# -gt 0 ]; do - case "$1" in - --with-frankenphp-app=*) - FRANKENPHP_APP_PATH="${1#*=}" - NEW_ARGS+=("--with-frankenphp-app=/app/app") - shift - ;; - --with-frankenphp-app) - if [ -n "${2:-}" ]; then - FRANKENPHP_APP_PATH="$2" - NEW_ARGS+=("--with-frankenphp-app=/app/app") - shift 2 - else - NEW_ARGS+=("$1") - shift - fi - ;; - *) - NEW_ARGS+=("$1") - shift - ;; - esac -done - -# Normalize the path and add mount if provided -if [ -n "$FRANKENPHP_APP_PATH" ]; then - # expand ~ to $HOME - if [ "${FRANKENPHP_APP_PATH#~}" != "$FRANKENPHP_APP_PATH" ]; then - FRANKENPHP_APP_PATH="$HOME${FRANKENPHP_APP_PATH#~}" - fi - # make absolute if relative - case "$FRANKENPHP_APP_PATH" in - /*) ABS_APP_PATH="$FRANKENPHP_APP_PATH" ;; - *) ABS_APP_PATH="$(pwd)/$FRANKENPHP_APP_PATH" ;; - esac - MOUNT_LIST="$MOUNT_LIST -v $ABS_APP_PATH:/app/app" -fi - -# Run docker -# shellcheck disable=SC2068 -# shellcheck disable=SC2086 -# shellcheck disable=SC2090 - -if [ "$SPC_DOCKER_DEBUG" = "yes" ]; then - echo "* Debug mode enabled, run docker in interactive mode." - echo "* You can use 'exit' to exit the docker container." - echo "* You can use 'bin/spc' like normal builds." - echo "*" - echo "* Mounted directories:" - echo "* ./config: $(pwd)/config" - echo "* ./src: $(pwd)/src" - echo "* ./buildroot: $(pwd)/buildroot" - echo "* ./source: $(pwd)/source" - echo "* ./dist: $(pwd)/dist" - echo "* ./downloads: $(pwd)/downloads" - echo "* ./pkgroot: $(pwd)/pkgroot" - echo "*" - set -ex - $DOCKER_EXECUTABLE run $PLATFORM_ARG --privileged --rm -it $INTERACT $ENV_LIST --env-file /tmp/spc-gnu-docker.env $MOUNT_LIST cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION /bin/bash -else - $DOCKER_EXECUTABLE run $PLATFORM_ARG --rm $INTERACT $ENV_LIST --env-file /tmp/spc-gnu-docker.env $MOUNT_LIST cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION bin/spc "${NEW_ARGS[@]}" -fi diff --git a/config/artifact.json b/config/artifact.json deleted file mode 100644 index ad8507b90..000000000 --- a/config/artifact.json +++ /dev/null @@ -1,1061 +0,0 @@ -{ - "amqp": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/amqp", - "filename": "amqp.tgz", - "extract": "php-src/ext/amqp" - } - }, - "apcu": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/APCu", - "filename": "apcu.tgz", - "extract": "php-src/ext/apcu" - } - }, - "ast": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/ast", - "filename": "ast.tgz", - "extract": "php-src/ext/ast" - } - }, - "attr": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz" - }, - "source-mirror": { - "type": "url", - "url": "https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz" - } - }, - "brotli": { - "binary": "hosted", - "source": { - "type": "ghtagtar", - "repo": "google/brotli", - "match": "v1\\.\\d.*" - } - }, - "bzip2": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz" - }, - "source-mirror": { - "type": "filelist", - "url": "https://sourceware.org/pub/bzip2/", - "regex": "/href=\"(?bzip2-(?[^\"]+)\\.tar\\.gz)\"/" - } - }, - "curl": { - "source": { - "type": "ghrel", - "repo": "curl/curl", - "match": "curl.+\\.tar\\.xz", - "prefer-stable": true - } - }, - "dio": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/dio", - "filename": "dio.tgz", - "extract": "php-src/ext/dio" - } - }, - "ev": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/ev", - "filename": "ev.tgz", - "extract": "php-src/ext/ev" - } - }, - "ext-brotli": { - "source": { - "type": "git", - "rev": "master", - "url": "https://github.com/kjdev/php-ext-brotli", - "extract": "php-src/ext/brotli" - } - }, - "ext-ds": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/ds", - "filename": "ds.tgz", - "extract": "php-src/ext/ds" - } - }, - "ext-event": { - "source": { - "type": "url", - "url": "https://bitbucket.org/osmanov/pecl-event/get/3.0.8.tar.gz", - "extract": "php-src/ext/event" - } - }, - "ext-glfw": { - "source": { - "type": "git", - "url": "https://github.com/mario-deluna/php-glfw", - "rev": "master" - } - }, - "ext-gmssl": { - "source": { - "type": "ghtar", - "repo": "gmssl/GmSSL-PHP", - "extract": "php-src/ext/gmssl" - } - }, - "ext-imagick": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/imagick", - "filename": "imagick.tgz", - "extract": "php-src/ext/imagick" - } - }, - "ext-imap": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/imap", - "filename": "imap.tgz", - "extract": "php-src/ext/imap" - } - }, - "ext-lz4": { - "source": { - "type": "ghtagtar", - "repo": "kjdev/php-ext-lz4", - "extract": "php-src/ext/lz4" - } - }, - "ext-memcache": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/memcache", - "filename": "memcache.tgz", - "extract": "php-src/ext/memcache" - } - }, - "ext-rdkafka": { - "source": { - "type": "ghtar", - "repo": "arnaud-lb/php-rdkafka", - "extract": "php-src/ext/rdkafka" - } - }, - "ext-simdjson": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/simdjson", - "filename": "simdjson.tgz", - "extract": "php-src/ext/simdjson" - } - }, - "ext-snappy": { - "source": { - "type": "git", - "rev": "master", - "url": "https://github.com/kjdev/php-ext-snappy", - "extract": "php-src/ext/snappy" - } - }, - "ext-ssh2": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/ssh2", - "filename": "ssh2.tgz", - "extract": "php-src/ext/ssh2" - } - }, - "ext-trader": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/trader", - "filename": "trader.tgz", - "extract": "php-src/ext/trader" - } - }, - "ext-uuid": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/uuid", - "filename": "uuid.tgz", - "extract": "php-src/ext/uuid" - } - }, - "ext-uv": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/uv", - "filename": "uv.tgz", - "extract": "php-src/ext/uv" - } - }, - "ext-xz": { - "source": { - "type": "git", - "rev": "main", - "url": "https://github.com/codemasher/php-ext-xz", - "extract": "php-src/ext/xz" - } - }, - "ext-zip": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/zip", - "filename": "ext-zip.tgz" - } - }, - "ext-zstd": { - "source": { - "type": "git", - "rev": "master", - "url": "https://github.com/kjdev/php-ext-zstd", - "extract": "php-src/ext/zstd" - } - }, - "fastlz": { - "source": { - "type": "git", - "url": "https://github.com/ariya/FastLZ.git", - "rev": "master" - } - }, - "freetype": { - "source": { - "type": "git", - "rev": "VER-2-13-2", - "url": "https://github.com/freetype/freetype" - } - }, - "gettext": { - "source": { - "type": "filelist", - "url": "https://ftp.gnu.org/pub/gnu/gettext/", - "regex": "/href=\"(?gettext-(?[^\"]+)\\.tar\\.xz)\"/" - } - }, - "gmp": { - "binary": "hosted", - "source": { - "type": "filelist", - "url": "https://gmplib.org/download/gmp/", - "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/" - }, - "source-mirror": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz" - } - }, - "gmssl": { - "binary": "hosted", - "source": { - "type": "ghtar", - "repo": "guanzhi/GmSSL" - } - }, - "go-xcaddy": { - "binary": "custom" - }, - "grpc": { - "binary": "hosted", - "source": { - "type": "git", - "regex": "v(?1.\\d+).x", - "url": "https://github.com/grpc/grpc.git" - } - }, - "icu": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "unicode-org/icu", - "match": "icu4c.+-src\\.tgz", - "prefer-stable": true - } - }, - "icu-static-win": { - "source": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/icu-static-windows-x64/icu-static-windows-x64.zip" - } - }, - "igbinary": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/igbinary", - "filename": "igbinary.tgz", - "extract": "php-src/ext/igbinary" - } - }, - "imagemagick": { - "source": { - "type": "ghtar", - "repo": "ImageMagick/ImageMagick" - } - }, - "imap": { - "source": { - "type": "git", - "url": "https://github.com/static-php/imap.git", - "rev": "master" - } - }, - "inotify": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/inotify", - "filename": "inotify.tgz", - "extract": "php-src/ext/inotify" - } - }, - "jbig": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/jbig/jbigkit-2.1.tar.gz" - }, - "source-mirror": { - "type": "url", - "url": "https://www.cl.cam.ac.uk/~mgk25/jbigkit/download/jbigkit-2.1.tar.gz" - } - }, - "ldap": { - "source": { - "type": "filelist", - "url": "https://www.openldap.org/software/download/OpenLDAP/openldap-release/", - "regex": "/href=\"(?openldap-(?[^\"]+)\\.tgz)\"/" - } - }, - "lerc": { - "binary": "hosted", - "source": { - "type": "ghtar", - "repo": "Esri/lerc", - "prefer-stable": true - } - }, - "libacl": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://download.savannah.nongnu.org/releases/acl/acl-2.3.2.tar.gz" - }, - "source-mirror": { - "type": "url", - "url": "https://mirror.souseiseki.middlendian.com/nongnu/acl/acl-2.3.2.tar.gz" - } - }, - "libaom": { - "binary": "hosted", - "source": { - "type": "git", - "rev": "main", - "url": "https://aomedia.googlesource.com/aom" - } - }, - "libargon2": { - "binary": "hosted", - "source": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/phc-winner-argon2" - } - }, - "libavif": { - "binary": "hosted", - "source": { - "type": "ghtar", - "repo": "AOMediaCodec/libavif" - } - }, - "libcares": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "c-ares/c-ares", - "match": "c-ares-.+\\.tar\\.gz", - "prefer-stable": true - }, - "source-mirror": { - "type": "filelist", - "url": "https://c-ares.org/download/", - "regex": "/href=\"\\/download\\/(?c-ares-(?[^\"]+)\\.tar\\.gz)\"/" - } - }, - "libde265": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "strukturag/libde265", - "match": "libde265-.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libedit": { - "binary": "hosted", - "source": { - "type": "filelist", - "url": "https://thrysoee.dk/editline/", - "regex": "/href=\"(?libedit-(?[^\"]+)\\.tar\\.gz)\"/" - } - }, - "libevent": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "libevent/libevent", - "match": "libevent.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libffi": { - "source": { - "type": "ghrel", - "repo": "libffi/libffi", - "match": "libffi.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libffi-win": { - "source": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/libffi-win.git" - } - }, - "libheif": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "strukturag/libheif", - "match": "libheif-.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libiconv": { - "binary": "hosted", - "source": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/libiconv/", - "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/" - }, - "source-mirror": "https://dl.static-php.dev/static-php-cli/deps/spc-download-mirror/libiconv/libiconv-spc-mirror.tar.gz" - }, - "libiconv-win": { - "source": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/libiconv-win.git" - } - }, - "libjpeg": { - "source": { - "type": "ghtar", - "repo": "libjpeg-turbo/libjpeg-turbo" - } - }, - "libjxl": { - "source": { - "type": "git", - "url": "https://github.com/libjxl/libjxl", - "rev": "main", - "submodules": [ - "third_party/highway", - "third_party/libjpeg-turbo", - "third_party/sjpeg", - "third_party/skcms" - ] - } - }, - "liblz4": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "lz4/lz4", - "match": "lz4-.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libmemcached": { - "source": { - "type": "ghtagtar", - "repo": "awesomized/libmemcached", - "match": "1.\\d.\\d" - } - }, - "libpng": { - "binary": "hosted", - "source": { - "type": "git", - "url": "https://github.com/glennrp/libpng.git", - "rev": "libpng16" - } - }, - "librabbitmq": { - "source": { - "type": "git", - "url": "https://github.com/alanxz/rabbitmq-c.git", - "rev": "master" - } - }, - "librdkafka": { - "source": { - "type": "ghtar", - "repo": "confluentinc/librdkafka" - } - }, - "libsodium": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "jedisct1/libsodium", - "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", - "prefer-stable": true - } - }, - "libssh2": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "libssh2/libssh2", - "match": "libssh2.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libtiff": { - "source": { - "type": "filelist", - "url": "https://download.osgeo.org/libtiff/", - "regex": "/href=\"(?tiff-(?[^\"]+)\\.tar\\.xz)\"/" - } - }, - "liburing": { - "source": { - "type": "ghtar", - "repo": "axboe/liburing", - "prefer-stable": true - } - }, - "libuuid": { - "source": { - "type": "git", - "url": "https://github.com/static-php/libuuid.git", - "rev": "master" - } - }, - "libuv": { - "source": { - "type": "ghtar", - "repo": "libuv/libuv" - } - }, - "libwebp": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.3.2.tar.gz" - } - }, - "libxml2": { - "source": { - "type": "url", - "url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.12.5.tar.gz" - } - }, - "libxslt": { - "source": { - "type": "filelist", - "url": "https://download.gnome.org/sources/libxslt/1.1/", - "regex": "/href=\"(?libxslt-(?[^\"]+)\\.tar\\.xz)\"/" - } - }, - "libyaml": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "yaml/libyaml", - "match": "yaml-.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "libzip": { - "source": { - "type": "ghrel", - "repo": "nih-at/libzip", - "match": "libzip.+\\.tar\\.xz", - "prefer-stable": true - } - }, - "memcached": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/memcached", - "filename": "memcached.tgz", - "extract": "php-src/ext/memcached" - } - }, - "micro": { - "source": { - "type": "git", - "extract": "php-src/sapi/micro", - "rev": "master", - "url": "https://github.com/static-php/phpmicro" - } - }, - "mimalloc": { - "source": { - "type": "ghtagtar", - "repo": "microsoft/mimalloc", - "match": "v2\\.\\d\\.[^3].*" - } - }, - "mongodb": { - "source": { - "type": "ghrel", - "repo": "mongodb/mongo-php-driver", - "match": "mongodb.+\\.tgz", - "prefer-stable": true, - "extract": "php-src/ext/mongodb" - } - }, - "msgpack": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/msgpack", - "filename": "msgpack.tgz", - "extract": "php-src/ext/msgpack" - } - }, - "musl-toolchain": { - "binary": { - "linux-x86_64": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", - "extract": "{pkg_root_path}/musl-toolchain" - }, - "linux-aarch64": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz", - "extract": "{pkg_root_path}/musl-toolchain" - } - } - }, - "musl-wrapper": { - "source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz" - }, - "nasm": { - "binary": { - "windows-x86_64": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", - "extract": { - "nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" - } - } - } - }, - "ncurses": { - "binary": "hosted", - "source": { - "type": "filelist", - "url": "https://ftp.gnu.org/pub/gnu/ncurses/", - "regex": "/href=\"(?ncurses-(?[^\"]+)\\.tar\\.gz)\"/" - } - }, - "net-snmp": { - "source": { - "type": "ghtagtar", - "repo": "net-snmp/net-snmp" - } - }, - "nghttp2": { - "source": { - "type": "ghrel", - "repo": "nghttp2/nghttp2", - "match": "nghttp2.+\\.tar\\.xz", - "prefer-stable": true - } - }, - "nghttp3": { - "source": { - "type": "ghrel", - "repo": "ngtcp2/nghttp3", - "match": "nghttp3.+\\.tar\\.xz", - "prefer-stable": true - } - }, - "ngtcp2": { - "source": { - "type": "ghrel", - "repo": "ngtcp2/ngtcp2", - "match": "ngtcp2.+\\.tar\\.xz", - "prefer-stable": true - } - }, - "onig": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "kkos/oniguruma", - "match": "onig-.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "openssl": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "openssl/openssl", - "match": "openssl.+\\.tar\\.gz", - "prefer-stable": true - }, - "source-mirror": { - "type": "filelist", - "url": "https://www.openssl.org/source/", - "regex": "/href=\"(?openssl-(?[^\"]+)\\.tar\\.gz)\"/" - } - }, - "opentelemetry": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/opentelemetry", - "filename": "opentelemetry.tgz", - "extract": "php-src/ext/opentelemetry" - } - }, - "parallel": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/parallel", - "filename": "parallel.tgz", - "extract": "php-src/ext/parallel" - } - }, - "pdo_sqlsrv": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/pdo_sqlsrv", - "filename": "pdo_sqlsrv.tgz", - "extract": "php-src/ext/pdo_sqlsrv" - } - }, - "php-sdk-binary-tools": { - "binary": { - "windows-x86_64": { - "type": "git", - "rev": "master", - "url": "https://github.com/php/php-sdk-binary-tools.git", - "extract": "{php_sdk_path}" - } - } - }, - "php-src": { - "source": { - "type": "php-release" - } - }, - "pkg-config": { - "binary": { - "linux-x86_64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", - "extract": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "linux-aarch64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", - "extract": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "macos-x86_64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-darwin.txz", - "extract": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "macos-aarch64": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-darwin.txz", - "extract": "{pkg_root_path}" - } - }, - "source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz" - }, - "postgresql": { - "source": { - "type": "ghtagtar", - "repo": "postgres/postgres", - "match": "REL_18_\\d+" - } - }, - "postgresql-win": { - "source": { - "type": "url", - "url": "https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip" - } - }, - "protobuf": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/protobuf", - "filename": "protobuf.tgz", - "extract": "php-src/ext/protobuf" - } - }, - "pthreads4w": { - "source": { - "type": "git", - "rev": "master", - "url": "https://git.code.sf.net/p/pthreads4w/code" - } - }, - "qdbm": { - "source": { - "type": "git", - "url": "https://github.com/static-php/qdbm.git", - "rev": "main" - } - }, - "rar": { - "source": { - "type": "git", - "url": "https://github.com/static-php/php-rar.git", - "rev": "issue-php82", - "extract": "php-src/ext/rar" - } - }, - "re2c": { - "source": { - "type": "ghrel", - "repo": "skvadrik/re2c", - "match": "re2c.+\\.tar\\.xz", - "prefer-stable": true - }, - "source-mirror": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/re2c/re2c-4.3.tar.xz" - } - }, - "readline": { - "binary": "hosted", - "source": { - "type": "filelist", - "url": "https://ftp.gnu.org/pub/gnu/readline/", - "regex": "/href=\"(?readline-(?[^\"]+)\\.tar\\.gz)\"/" - } - }, - "redis": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/redis", - "filename": "redis.tgz", - "extract": "php-src/ext/redis" - } - }, - "snappy": { - "source": { - "type": "git", - "rev": "main", - "url": "https://github.com/google/snappy" - } - }, - "spx": { - "source": { - "type": "pie", - "repo": "noisebynorthwest/php-spx", - "extract": "php-src/ext/spx" - } - }, - "sqlite": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://www.sqlite.org/2024/sqlite-autoconf-3450200.tar.gz" - } - }, - "sqlsrv": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/sqlsrv", - "filename": "sqlsrv.tgz", - "extract": "php-src/ext/sqlsrv" - } - }, - "strawberry-perl": { - "binary": { - "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" - } - } - }, - "swoole": { - "source": { - "type": "ghtar", - "repo": "swoole/swoole-src", - "match": "v6\\.+", - "prefer-stable": true, - "extract": "php-src/ext/swoole" - } - }, - "swow": { - "source": { - "type": "ghtar", - "repo": "swow/swow", - "prefer-stable": true, - "extract": "php-src/ext/swow-src" - } - }, - "tidy": { - "source": { - "type": "ghtar", - "repo": "htacg/tidy-html5", - "prefer-stable": true - } - }, - "unixodbc": { - "binary": "hosted", - "source": { - "type": "url", - "url": "https://www.unixodbc.org/unixODBC-2.3.12.tar.gz", - "version": "2.3.12" - } - }, - "upx": { - "binary": { - "linux-x86_64": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-amd64_linux\\.tar\\.xz", - "extract": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "linux-aarch64": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-arm64_linux\\.tar\\.xz", - "extract": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "windows-x86_64": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-win64\\.zip", - "extract": { - "upx.exe": "{pkg_root_path}/bin/upx.exe" - } - } - } - }, - "vswhere": { - "binary": { - "windows-x86_64": { - "type": "url", - "url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe", - "extract": "{pkg_root_path}/bin/vswhere.exe" - } - } - }, - "watcher": { - "source": { - "type": "ghtar", - "repo": "e-dant/watcher", - "prefer-stable": true - } - }, - "xdebug": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/xdebug", - "filename": "xdebug.tgz" - } - }, - "xhprof": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/xhprof", - "filename": "xhprof.tgz", - "extract": "php-src/ext/xhprof-src" - } - }, - "xlswriter": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/xlswriter", - "filename": "xlswriter.tgz", - "extract": "php-src/ext/xlswriter" - } - }, - "xz": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "tukaani-project/xz", - "match": "xz.+\\.tar\\.xz", - "prefer-stable": true - }, - "source-mirror": { - "type": "url", - "url": "https://github.com/tukaani-project/xz/releases/download/v5.8.1/xz-5.8.1.tar.gz" - } - }, - "yac": { - "source": { - "type": "url", - "url": "https://pecl.php.net/get/yac", - "filename": "yac.tgz", - "extract": "php-src/ext/yac" - } - }, - "yaml": { - "source": { - "type": "git", - "rev": "php7", - "url": "https://github.com/php/pecl-file_formats-yaml", - "extract": "php-src/ext/yaml" - } - }, - "zig": { - "binary": "custom" - }, - "zlib": { - "binary": "hosted", - "source": { - "type": "ghrel", - "repo": "madler/zlib", - "match": "zlib.+\\.tar\\.gz", - "prefer-stable": true - } - }, - "zstd": { - "source": { - "type": "ghrel", - "repo": "facebook/zstd", - "match": "zstd.+\\.tar\\.gz", - "prefer-stable": true - } - } -} diff --git a/config/ext.json b/config/ext.json deleted file mode 100644 index 16a71c212..000000000 --- a/config/ext.json +++ /dev/null @@ -1,1293 +0,0 @@ -{ - "amqp": { - "support": { - "BSD": "wip" - }, - "type": "external", - "arg-type": "custom", - "source": "amqp", - "lib-depends": [ - "librabbitmq" - ], - "ext-depends-windows": [ - "openssl" - ] - }, - "apcu": { - "type": "external", - "source": "apcu" - }, - "ast": { - "type": "external", - "source": "ast" - }, - "bcmath": { - "type": "builtin" - }, - "brotli": { - "type": "external", - "source": "ext-brotli", - "arg-type": "enable", - "lib-depends": [ - "brotli" - ] - }, - "bz2": { - "type": "builtin", - "arg-type-unix": "with-path", - "arg-type-windows": "with", - "lib-depends": [ - "bzip2" - ] - }, - "calendar": { - "type": "builtin" - }, - "com_dotnet": { - "support": { - "BSD": "no", - "Linux": "no", - "Darwin": "no" - }, - "type": "builtin" - }, - "ctype": { - "type": "builtin" - }, - "curl": { - "notes": true, - "type": "builtin", - "arg-type": "with", - "lib-depends": [ - "curl" - ], - "ext-depends-windows": [ - "zlib", - "openssl", - "brotli" - ] - }, - "dba": { - "type": "builtin", - "arg-type": "custom", - "lib-suggests": [ - "qdbm" - ] - }, - "dio": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "dio" - }, - "dom": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "custom", - "arg-type-windows": "with", - "lib-depends": [ - "libxml2", - "zlib" - ], - "ext-depends-windows": [ - "xml" - ] - }, - "ds": { - "type": "external", - "source": "ext-ds" - }, - "enchant": { - "support": { - "Windows": "wip", - "BSD": "wip", - "Darwin": "wip", - "Linux": "wip" - }, - "type": "wip" - }, - "ev": { - "type": "external", - "source": "ev", - "arg-type-windows": "with", - "ext-depends": [ - "sockets" - ] - }, - "event": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "ext-event", - "arg-type": "custom", - "lib-depends": [ - "libevent" - ], - "ext-depends": [ - "openssl" - ], - "ext-suggests": [ - "sockets" - ] - }, - "excimer": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-excimer" - }, - "exif": { - "type": "builtin" - }, - "ffi": { - "support": { - "Linux": "partial", - "BSD": "wip" - }, - "notes": true, - "arg-type": "custom", - "type": "builtin", - "lib-depends-unix": [ - "libffi" - ], - "lib-depends-windows": [ - "libffi-win" - ] - }, - "fileinfo": { - "type": "builtin" - }, - "filter": { - "type": "builtin" - }, - "ftp": { - "type": "builtin", - "lib-suggests": [ - "openssl" - ] - }, - "gd": { - "support": { - "BSD": "wip" - }, - "notes": true, - "type": "builtin", - "arg-type": "custom", - "arg-type-windows": "with", - "lib-depends": [ - "zlib", - "libpng" - ], - "ext-depends": [ - "zlib" - ], - "lib-suggests": [ - "libavif", - "libwebp", - "libjpeg", - "freetype" - ] - }, - "gettext": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "lib-depends": [ - "gettext" - ] - }, - "glfw": { - "support": { - "Windows": "wip", - "BSD": "no", - "Linux": "no" - }, - "notes": true, - "type": "external", - "arg-type": "custom", - "source": "ext-glfw", - "lib-depends": [ - "glfw" - ], - "lib-depends-windows": [] - }, - "gmp": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "lib-depends": [ - "gmp" - ] - }, - "gmssl": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "ext-gmssl", - "lib-depends": [ - "gmssl" - ] - }, - "grpc": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-grpc", - "arg-type-unix": "enable-path", - "cpp-extension": true, - "lib-depends": [ - "grpc", - "zlib", - "openssl", - "libcares" - ] - }, - "iconv": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "arg-type-windows": "with", - "lib-depends-unix": [ - "libiconv" - ], - "lib-depends-windows": [ - "libiconv-win" - ] - }, - "igbinary": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "igbinary", - "ext-suggests": [ - "session", - "apcu" - ] - }, - "imagick": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "ext-imagick", - "arg-type": "custom", - "lib-depends": [ - "imagemagick" - ] - }, - "imap": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "ext-imap", - "arg-type": "custom", - "lib-depends": [ - "imap" - ], - "ext-suggests": [ - "openssl" - ] - }, - "inotify": { - "support": { - "Windows": "no", - "BSD": "wip", - "Darwin": "no" - }, - "type": "external", - "source": "inotify" - }, - "intl": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "lib-depends-unix": [ - "icu" - ], - "lib-depends-windows": [ - "icu-static-win" - ] - }, - "ldap": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "lib-depends": [ - "ldap" - ], - "lib-suggests": [ - "gmp", - "libsodium" - ], - "ext-suggests": [ - "openssl" - ] - }, - "libxml": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "none", - "ext-depends": [ - "xml" - ], - "build-with-php": true, - "target": [ - "static" - ] - }, - "lz4": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-lz4", - "arg-type": "custom", - "lib-depends": [ - "liblz4" - ] - }, - "maxminddb": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "type": "external", - "source": "ext-maxminddb", - "arg-type": "with", - "lib-depends": [ - "libmaxminddb" - ] - }, - "mbregex": { - "type": "builtin", - "arg-type": "custom", - "target": [ - "static" - ], - "ext-depends": [ - "mbstring" - ], - "lib-depends": [ - "onig" - ] - }, - "mbstring": { - "type": "builtin", - "arg-type": "custom" - }, - "mcrypt": { - "type": "wip", - "support": { - "Windows": "no", - "BSD": "no", - "Darwin": "no", - "Linux": "no" - }, - "notes": true - }, - "memcache": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-memcache", - "arg-type": "custom", - "ext-depends": [ - "zlib", - "session" - ] - }, - "memcached": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "memcached", - "arg-type": "custom", - "cpp-extension": true, - "lib-depends": [ - "libmemcached" - ], - "lib-depends-unix": [ - "libmemcached", - "fastlz" - ], - "lib-suggests": [ - "zstd" - ], - "ext-depends": [ - "session", - "zlib" - ], - "ext-suggests": [ - "igbinary", - "msgpack", - "session" - ] - }, - "mongodb": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "type": "external", - "source": "mongodb", - "arg-type": "custom", - "lib-suggests": [ - "icu", - "openssl", - "zstd", - "zlib" - ], - "frameworks": [ - "CoreFoundation", - "Security" - ] - }, - "msgpack": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "msgpack", - "arg-type-unix": "with", - "arg-type-windows": "enable", - "ext-depends": [ - "session" - ] - }, - "mysqli": { - "type": "builtin", - "arg-type": "with", - "build-with-php": true, - "ext-depends": [ - "mysqlnd" - ] - }, - "mysqlnd": { - "type": "builtin", - "arg-type-windows": "with", - "build-with-php": true, - "lib-depends": [ - "zlib" - ] - }, - "mysqlnd_ed25519": { - "type": "external", - "source": "mysqlnd_ed25519", - "arg-type": "enable", - "target": [ - "shared" - ], - "ext-depends": [ - "mysqlnd" - ], - "lib-depends": [ - "libsodium" - ], - "lib-suggests": [ - "openssl" - ] - }, - "mysqlnd_parsec": { - "type": "external", - "source": "mysqlnd_parsec", - "arg-type": "enable", - "target": [ - "shared" - ], - "ext-depends": [ - "mysqlnd" - ], - "lib-depends": [ - "libsodium" - ], - "lib-suggests": [ - "openssl" - ] - }, - "oci8": { - "type": "wip", - "support": { - "Windows": "wip", - "BSD": "no", - "Darwin": "no", - "Linux": "no" - }, - "notes": true - }, - "odbc": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "type": "builtin", - "arg-type-unix": "custom", - "lib-depends-unix": [ - "unixodbc" - ] - }, - "opcache": { - "type": "builtin", - "arg-type-unix": "custom", - "arg-type-windows": "enable", - "zend-extension": true - }, - "openssl": { - "notes": true, - "type": "builtin", - "arg-type": "custom", - "arg-type-windows": "with", - "build-with-php": true, - "lib-depends": [ - "openssl", - "zlib" - ], - "ext-depends": [ - "zlib" - ] - }, - "opentelemetry": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "opentelemetry" - }, - "parallel": { - "support": { - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "parallel", - "arg-type-windows": "with", - "lib-depends-windows": [ - "pthreads4w" - ] - }, - "password-argon2": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "notes": true, - "type": "builtin", - "arg-type": "custom", - "lib-depends": [ - "libargon2", - "openssl" - ] - }, - "pcntl": { - "support": { - "Windows": "no" - }, - "type": "builtin", - "unix-only": true - }, - "pcov": { - "type": "external", - "source": "pcov", - "target": [ - "shared" - ] - }, - "pdo": { - "type": "builtin" - }, - "pdo_mysql": { - "type": "builtin", - "arg-type": "with", - "ext-depends": [ - "pdo", - "mysqlnd" - ] - }, - "pdo_odbc": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "custom", - "lib-depends-unix": [ - "unixodbc" - ], - "ext-depends": [ - "pdo", - "odbc" - ] - }, - "pdo_pgsql": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "arg-type-windows": "custom", - "ext-depends": [ - "pdo", - "pgsql" - ], - "lib-depends-unix": [ - "postgresql" - ], - "lib-depends-windows": [ - "postgresql-win" - ] - }, - "pdo_sqlite": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with", - "ext-depends": [ - "pdo", - "sqlite3" - ], - "lib-depends": [ - "sqlite" - ] - }, - "pdo_sqlsrv": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "pdo_sqlsrv", - "arg-type": "with", - "ext-depends": [ - "pdo", - "sqlsrv" - ] - }, - "pgsql": { - "support": { - "BSD": "wip" - }, - "notes": true, - "type": "builtin", - "arg-type": "custom", - "lib-depends-unix": [ - "postgresql" - ], - "lib-depends-windows": [ - "postgresql-win" - ] - }, - "phar": { - "type": "builtin", - "ext-depends": [ - "zlib" - ] - }, - "posix": { - "support": { - "Windows": "no" - }, - "type": "builtin", - "unix-only": true - }, - "protobuf": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "protobuf" - }, - "rar": { - "support": { - "BSD": "wip", - "Darwin": "partial" - }, - "notes": true, - "type": "external", - "source": "rar", - "cpp-extension": true - }, - "rdkafka": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "type": "external", - "source": "ext-rdkafka", - "arg-type": "custom", - "cpp-extension": true, - "lib-depends": [ - "librdkafka" - ] - }, - "readline": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "lib-depends": [ - "libedit" - ], - "target": [ - "static" - ] - }, - "redis": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "redis", - "arg-type": "custom", - "ext-suggests": [ - "session", - "igbinary", - "msgpack" - ], - "lib-suggests-unix": [ - "zstd", - "liblz4" - ] - }, - "session": { - "type": "builtin", - "build-with-php": true - }, - "shmop": { - "type": "builtin", - "build-with-php": true - }, - "simdjson": { - "type": "external", - "source": "ext-simdjson", - "cpp-extension": true - }, - "simplexml": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "custom", - "lib-depends": [ - "libxml2" - ], - "ext-depends-windows": [ - "xml" - ], - "build-with-php": true - }, - "snappy": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-snappy", - "cpp-extension": true, - "arg-type": "custom", - "lib-depends": [ - "snappy" - ], - "ext-suggests": [ - "apcu" - ] - }, - "snmp": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type-unix": "with", - "arg-type-windows": "with", - "lib-depends": [ - "net-snmp" - ] - }, - "soap": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "custom", - "ext-depends": [ - "libxml", - "session" - ] - }, - "sockets": { - "type": "builtin" - }, - "sodium": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with", - "lib-depends": [ - "libsodium" - ] - }, - "spx": { - "support": { - "BSD": "wip", - "Windows": "no" - }, - "notes": true, - "type": "external", - "source": "spx", - "arg-type": "custom", - "lib-depends": [ - "zlib" - ] - }, - "sqlite3": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "arg-type-windows": "with", - "build-with-php": true, - "lib-depends": [ - "sqlite" - ] - }, - "sqlsrv": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "sqlsrv", - "lib-depends-unix": [ - "unixodbc" - ], - "ext-depends-linux": [ - "pcntl" - ], - "cpp-extension": true - }, - "ssh2": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "ext-ssh2", - "arg-type": "with-path", - "arg-type-windows": "with", - "lib-depends": [ - "libssh2" - ], - "ext-depends": [ - "openssl", - "zlib" - ] - }, - "swoole": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "swoole", - "arg-type": "custom", - "cpp-extension": true, - "unix-only": true, - "lib-depends": [ - "libcares", - "brotli", - "nghttp2", - "zlib" - ], - "lib-suggests": [ - "zstd" - ], - "lib-suggests-linux": [ - "zstd", - "liburing" - ], - "ext-depends": [ - "openssl", - "curl" - ], - "ext-suggests": [ - "sockets", - "swoole-hook-pgsql", - "swoole-hook-mysql", - "swoole-hook-sqlite", - "swoole-hook-odbc" - ] - }, - "swoole-hook-mysql": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "notes": true, - "type": "addon", - "arg-type": "none", - "ext-depends": [ - "mysqlnd", - "pdo", - "pdo_mysql", - "swoole" - ], - "ext-suggests": [ - "mysqli" - ] - }, - "swoole-hook-odbc": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "notes": true, - "type": "addon", - "arg-type": "none", - "ext-depends": [ - "pdo", - "swoole" - ], - "lib-depends": [ - "unixodbc" - ] - }, - "swoole-hook-pgsql": { - "support": { - "Windows": "no", - "BSD": "wip", - "Darwin": "partial" - }, - "notes": true, - "type": "addon", - "arg-type": "none", - "ext-depends": [ - "pgsql", - "pdo", - "swoole" - ] - }, - "swoole-hook-sqlite": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "notes": true, - "type": "addon", - "arg-type": "none", - "ext-depends": [ - "sqlite3", - "pdo", - "swoole" - ] - }, - "swow": { - "support": { - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "swow", - "arg-type": "custom", - "lib-suggests": [ - "openssl", - "curl" - ], - "ext-suggests": [ - "openssl", - "curl" - ] - }, - "sysvmsg": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "type": "builtin", - "unix-only": true - }, - "sysvsem": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "type": "builtin", - "unix-only": true - }, - "sysvshm": { - "support": { - "BSD": "wip" - }, - "type": "builtin" - }, - "tidy": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "lib-depends": [ - "tidy" - ] - }, - "tokenizer": { - "type": "builtin", - "build-with-php": true - }, - "trader": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "type": "external", - "source": "ext-trader" - }, - "uuid": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-uuid", - "arg-type": "with-path", - "lib-depends": [ - "libuuid" - ] - }, - "uv": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-uv", - "arg-type": "with-path", - "lib-depends": [ - "libuv" - ], - "ext-depends": [ - "sockets" - ] - }, - "xdebug": { - "type": "external", - "source": "xdebug", - "target": [ - "shared" - ], - "support": { - "Windows": "wip", - "BSD": "no", - "Darwin": "partial", - "Linux": "partial" - }, - "notes": true, - "zend-extension": true - }, - "xhprof": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "notes": true, - "type": "external", - "source": "xhprof", - "ext-depends": [ - "ctype" - ], - "build-with-php": true - }, - "xlswriter": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "xlswriter", - "arg-type": "custom", - "ext-depends": [ - "zlib", - "zip" - ], - "lib-suggests": [ - "openssl" - ] - }, - "xml": { - "support": { - "BSD": "wip" - }, - "notes": true, - "type": "builtin", - "arg-type": "custom", - "arg-type-windows": "with", - "lib-depends": [ - "libxml2" - ], - "ext-depends-windows": [ - "iconv" - ], - "build-with-php": true - }, - "xmlreader": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "custom", - "lib-depends": [ - "libxml2" - ], - "ext-depends-windows": [ - "xml", - "dom" - ], - "build-with-php": true - }, - "xmlwriter": { - "support": { - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "custom", - "lib-depends": [ - "libxml2" - ], - "ext-depends-windows": [ - "xml" - ], - "build-with-php": true - }, - "xsl": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "builtin", - "arg-type": "with-path", - "lib-depends": [ - "libxslt" - ], - "ext-depends": [ - "xml", - "dom" - ] - }, - "xz": { - "type": "external", - "source": "ext-xz", - "arg-type": "with", - "lib-depends": [ - "xz" - ] - }, - "yac": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "yac", - "arg-type-unix": "custom", - "lib-depends-unix": [ - "fastlz" - ], - "ext-depends-unix": [ - "igbinary" - ] - }, - "yaml": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "yaml", - "arg-type-unix": "with-path", - "arg-type-windows": "with", - "lib-depends": [ - "libyaml" - ] - }, - "zip": { - "support": { - "BSD": "wip" - }, - "type": "external", - "source": "ext-zip", - "arg-type": "custom", - "arg-type-windows": "enable", - "lib-depends-unix": [ - "libzip" - ], - "ext-depends-windows": [ - "zlib", - "bz2" - ], - "lib-depends-windows": [ - "libzip", - "zlib", - "bzip2", - "xz" - ] - }, - "zlib": { - "type": "builtin", - "arg-type": "custom", - "arg-type-windows": "enable", - "lib-depends": [ - "zlib" - ], - "build-with-php": true, - "target": [ - "static" - ] - }, - "zstd": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "type": "external", - "source": "ext-zstd", - "arg-type": "custom", - "lib-depends": [ - "zstd" - ] - } -} diff --git a/config/lib.json b/config/lib.json deleted file mode 100644 index 4792a9329..000000000 --- a/config/lib.json +++ /dev/null @@ -1,1066 +0,0 @@ -{ - "lib-base": { - "type": "root" - }, - "php": { - "type": "root", - "source": "php-src", - "lib-depends": [ - "lib-base", - "micro", - "frankenphp" - ], - "lib-depends-macos": [ - "lib-base", - "micro", - "libxml2", - "frankenphp" - ], - "lib-suggests-linux": [ - "libacl", - "brotli", - "watcher" - ], - "lib-suggests-macos": [ - "brotli", - "watcher" - ] - }, - "frankenphp": { - "source": "frankenphp", - "type": "target" - }, - "micro": { - "type": "target", - "source": "micro" - }, - "pkg-config": { - "type": "package", - "source": "pkg-config", - "bin-unix": [ - "pkg-config" - ] - }, - "attr": { - "source": "attr", - "static-libs-unix": [ - "libattr.a" - ] - }, - "brotli": { - "source": "brotli", - "pkg-configs": [ - "libbrotlicommon", - "libbrotlidec", - "libbrotlienc" - ], - "static-libs-windows": [ - "brotlicommon.lib", - "brotlienc.lib", - "brotlidec.lib" - ], - "headers": [ - "brotli" - ] - }, - "bzip2": { - "source": "bzip2", - "static-libs-unix": [ - "libbz2.a" - ], - "static-libs-windows": [ - "libbz2.lib", - "libbz2_a.lib" - ], - "headers": [ - "bzlib.h" - ] - }, - "curl": { - "source": "curl", - "static-libs-unix": [ - "libcurl.a" - ], - "static-libs-windows": [ - "libcurl_a.lib" - ], - "headers": [ - "curl" - ], - "lib-depends-unix": [ - "openssl", - "zlib" - ], - "lib-depends-windows": [ - "zlib", - "libssh2", - "nghttp2" - ], - "lib-suggests-unix": [ - "libssh2", - "brotli", - "nghttp2", - "nghttp3", - "ngtcp2", - "zstd", - "libcares", - "ldap", - "idn2", - "krb5" - ], - "lib-suggests-windows": [ - "brotli" - ], - "frameworks": [ - "CoreFoundation", - "CoreServices", - "SystemConfiguration" - ] - }, - "fastlz": { - "source": "fastlz", - "static-libs-unix": [ - "libfastlz.a" - ], - "headers": [ - "fastlz/fastlz.h" - ] - }, - "freetype": { - "source": "freetype", - "static-libs-unix": [ - "libfreetype.a" - ], - "static-libs-windows": [ - "libfreetype_a.lib" - ], - "headers-unix": [ - "freetype2/freetype/freetype.h", - "freetype2/ft2build.h" - ], - "lib-depends": [ - "zlib" - ], - "lib-suggests": [ - "libpng" - ] - }, - "gettext": { - "source": "gettext", - "static-libs-unix": [ - "libintl.a" - ], - "lib-depends": [ - "libiconv" - ], - "lib-suggests": [ - "ncurses", - "libxml2" - ], - "frameworks": [ - "CoreFoundation" - ] - }, - "glfw": { - "source": "ext-glfw", - "static-libs-unix": [ - "libglfw3.a" - ], - "frameworks": [ - "CoreVideo", - "OpenGL", - "Cocoa", - "IOKit" - ] - }, - "gmp": { - "source": "gmp", - "static-libs-unix": [ - "libgmp.a" - ], - "static-libs-windows": [ - "libgmp.lib" - ], - "headers": [ - "gmp.h" - ] - }, - "gmssl": { - "source": "gmssl", - "static-libs-unix": [ - "libgmssl.a" - ], - "static-libs-windows": [ - "gmssl.lib" - ], - "frameworks": [ - "Security" - ] - }, - "grpc": { - "source": "grpc", - "pkg-configs": [ - "grpc" - ], - "lib-depends": [ - "zlib", - "openssl", - "libcares" - ], - "cpp-library": true, - "frameworks": [ - "CoreFoundation" - ] - }, - "icu": { - "source": "icu", - "cpp-library": true, - "pkg-configs": [ - "icu-uc", - "icu-i18n", - "icu-io" - ] - }, - "icu-static-win": { - "source": "icu-static-win", - "static-libs-windows": [ - "icudt.lib", - "icuin.lib", - "icuio.lib", - "icuuc.lib" - ], - "headers-windows": [ - "unicode" - ] - }, - "idn2": { - "source": "libidn2", - "pkg-configs": [ - "libidn2" - ], - "headers": [ - "idn2.h" - ], - "lib-suggests-unix": [ - "libiconv", - "gettext", - "libunistring" - ], - "lib-depends-macos": [ - "libiconv", - "gettext" - ] - }, - "imagemagick": { - "source": "imagemagick", - "cpp-library": true, - "pkg-configs": [ - "Magick++-7.Q16HDRI", - "MagickCore-7.Q16HDRI", - "MagickWand-7.Q16HDRI" - ], - "lib-depends": [ - "zlib", - "libjpeg", - "libjxl", - "libpng", - "libwebp", - "freetype", - "libtiff", - "libheif", - "bzip2" - ], - "lib-suggests": [ - "zstd", - "xz", - "libzip", - "libxml2" - ] - }, - "imap": { - "source": "imap", - "static-libs-unix": [ - "libc-client.a" - ], - "lib-suggests": [ - "openssl" - ] - }, - "jbig": { - "source": "jbig", - "static-libs-unix": [ - "libjbig.a", - "libjbig85.a" - ], - "headers": [ - "jbig.h", - "jbig85.h", - "jbig_ar.h" - ] - }, - "krb5": { - "source": "krb5", - "pkg-configs": [ - "krb5-gssapi" - ], - "headers": [ - "krb5.h", - "gssapi/gssapi.h" - ], - "lib-depends": [ - "openssl" - ], - "lib-suggests": [ - "ldap", - "libedit" - ], - "frameworks": [ - "Kerberos" - ] - }, - "ldap": { - "source": "ldap", - "pkg-configs": [ - "ldap", - "lber" - ], - "lib-depends": [ - "openssl", - "zlib", - "gmp", - "libsodium" - ] - }, - "lerc": { - "source": "lerc", - "static-libs-unix": [ - "libLerc.a" - ], - "cpp-library": true - }, - "libacl": { - "source": "libacl", - "static-libs-unix": [ - "libacl.a" - ], - "lib-depends": [ - "attr" - ] - }, - "libaom": { - "source": "libaom", - "static-libs-unix": [ - "libaom.a" - ], - "static-libs-windows": [ - "aom.lib" - ], - "cpp-library": true - }, - "libargon2": { - "source": "libargon2", - "static-libs-unix": [ - "libargon2.a" - ], - "lib-suggests": [ - "libsodium" - ] - }, - "libavif": { - "source": "libavif", - "static-libs-unix": [ - "libavif.a" - ], - "static-libs-windows": [ - "avif.lib" - ], - "lib-depends": [ - "libaom" - ], - "lib-suggests": [ - "libwebp", - "libjpeg", - "libxml2", - "libpng" - ] - }, - "libcares": { - "source": "libcares", - "static-libs-unix": [ - "libcares.a" - ], - "headers-unix": [ - "ares.h", - "ares_dns.h", - "ares_nameser.h" - ] - }, - "libde265": { - "source": "libde265", - "static-libs-unix": [ - "libde265.a" - ], - "cpp-library": true - }, - "libedit": { - "source": "libedit", - "static-libs-unix": [ - "libedit.a" - ], - "lib-depends": [ - "ncurses" - ] - }, - "libevent": { - "source": "libevent", - "static-libs-unix": [ - "libevent.a", - "libevent_core.a", - "libevent_extra.a", - "libevent_openssl.a" - ], - "lib-depends": [ - "openssl" - ] - }, - "libffi": { - "source": "libffi", - "static-libs-unix": [ - "libffi.a" - ], - "static-libs-windows": [ - "libffi.lib" - ], - "headers-unix": [ - "ffi.h", - "ffitarget.h" - ], - "headers-windows": [ - "ffi.h", - "fficonfig.h", - "ffitarget.h" - ] - }, - "libffi-win": { - "source": "libffi-win", - "static-libs-windows": [ - "libffi.lib" - ], - "headers-windows": [ - "ffi.h", - "ffitarget.h", - "fficonfig.h" - ] - }, - "libheif": { - "source": "libheif", - "static-libs-unix": [ - "libheif.a" - ], - "lib-depends": [ - "libde265", - "libwebp", - "libaom", - "zlib", - "brotli" - ] - }, - "libiconv": { - "source": "libiconv", - "static-libs-unix": [ - "libiconv.a", - "libcharset.a" - ], - "headers": [ - "iconv.h", - "libcharset.h", - "localcharset.h" - ] - }, - "libiconv-win": { - "source": "libiconv-win", - "static-libs-windows": [ - "libiconv.lib", - "libiconv_a.lib" - ] - }, - "libjpeg": { - "source": "libjpeg", - "static-libs-unix": [ - "libjpeg.a", - "libturbojpeg.a" - ], - "static-libs-windows": [ - "libjpeg_a.lib" - ], - "lib-depends": [ - "zlib" - ] - }, - "libjxl": { - "source": "libjxl", - "pkg-configs": [ - "libjxl", - "libjxl_cms", - "libjxl_threads", - "libhwy" - ], - "lib-depends": [ - "brotli", - "libjpeg", - "libpng", - "libwebp" - ] - }, - "liblz4": { - "source": "liblz4", - "static-libs-unix": [ - "liblz4.a" - ] - }, - "libmaxminddb": { - "source": "libmaxminddb", - "static-libs-unix": [ - "libmaxminddb.a" - ], - "headers": [ - "maxminddb.h", - "maxminddb_config.h" - ] - }, - "libmemcached": { - "source": "libmemcached", - "cpp-library": true, - "static-libs-unix": [ - "libmemcached.a", - "libmemcachedprotocol.a", - "libmemcachedutil.a", - "libhashkit.a" - ] - }, - "libpng": { - "source": "libpng", - "static-libs-unix": [ - "libpng16.a" - ], - "static-libs-windows": [ - "libpng16_static.lib", - "libpng_a.lib" - ], - "headers-unix": [ - "png.h", - "pngconf.h", - "pnglibconf.h" - ], - "headers-windows": [ - "png.h", - "pngconf.h" - ], - "lib-depends": [ - "zlib" - ] - }, - "librabbitmq": { - "source": "librabbitmq", - "static-libs-unix": [ - "librabbitmq.a" - ], - "static-libs-windows": [ - "rabbitmq.4.lib" - ], - "lib-depends": [ - "openssl" - ] - }, - "librdkafka": { - "source": "librdkafka", - "pkg-configs": [ - "rdkafka++-static", - "rdkafka-static" - ], - "cpp-library": true, - "lib-suggests": [ - "curl", - "liblz4", - "openssl", - "zlib", - "zstd" - ] - }, - "libsodium": { - "source": "libsodium", - "static-libs-unix": [ - "libsodium.a" - ], - "static-libs-windows": [ - "libsodium.lib" - ] - }, - "libssh2": { - "source": "libssh2", - "static-libs-unix": [ - "libssh2.a" - ], - "static-libs-windows": [ - "libssh2.lib" - ], - "headers": [ - "libssh2.h", - "libssh2_publickey.h", - "libssh2_sftp.h" - ], - "lib-depends": [ - "openssl" - ] - }, - "libtiff": { - "source": "libtiff", - "static-libs-unix": [ - "libtiff.a" - ], - "lib-depends": [ - "zlib", - "libjpeg" - ], - "lib-suggests-unix": [ - "lerc", - "libwebp", - "jbig", - "xz", - "zstd" - ] - }, - "libunistring": { - "source": "libunistring", - "static-libs-unix": [ - "libunistring.a" - ], - "headers": [ - "unistr.h", - "unistring/" - ] - }, - "liburing": { - "source": "liburing", - "pkg-configs": [ - "liburing", - "liburing-ffi" - ], - "static-libs-linux": [ - "liburing.a", - "liburing-ffi.a" - ], - "headers-linux": [ - "liburing/", - "liburing.h" - ] - }, - "libuuid": { - "source": "libuuid", - "static-libs-unix": [ - "libuuid.a" - ], - "headers": [ - "uuid/uuid.h" - ] - }, - "libuv": { - "source": "libuv", - "static-libs-unix": [ - "libuv.a" - ] - }, - "libwebp": { - "source": "libwebp", - "pkg-configs": [ - "libwebp", - "libwebpdecoder", - "libwebpdemux", - "libwebpmux", - "libsharpyuv" - ], - "static-libs-windows": [ - "libwebp.lib", - "libwebpdecoder.lib", - "libwebpdemux.lib", - "libsharpyuv.lib" - ] - }, - "libxml2": { - "source": "libxml2", - "pkg-configs": [ - "libxml-2.0" - ], - "static-libs-windows": [ - "libxml2s.lib", - "libxml2_a.lib" - ], - "headers": [ - "libxml2" - ], - "lib-depends-unix": [ - "libiconv" - ], - "lib-suggests-unix": [ - "xz", - "zlib" - ], - "lib-depends-windows": [ - "libiconv-win" - ], - "lib-suggests-windows": [ - "zlib" - ] - }, - "libxslt": { - "source": "libxslt", - "static-libs-unix": [ - "libxslt.a", - "libexslt.a" - ], - "lib-depends": [ - "libxml2" - ] - }, - "libyaml": { - "source": "libyaml", - "static-libs-unix": [ - "libyaml.a" - ], - "static-libs-windows": [ - "yaml.lib" - ], - "headers": [ - "yaml.h" - ] - }, - "libzip": { - "source": "libzip", - "static-libs-unix": [ - "libzip.a" - ], - "static-libs-windows": [ - "zip.lib", - "libzip_a.lib" - ], - "headers": [ - "zip.h", - "zipconf.h" - ], - "lib-depends-unix": [ - "zlib" - ], - "lib-suggests-unix": [ - "bzip2", - "xz", - "zstd", - "openssl" - ], - "lib-depends-windows": [ - "zlib", - "bzip2", - "xz" - ], - "lib-suggests-windows": [ - "openssl" - ] - }, - "mimalloc": { - "source": "mimalloc", - "static-libs-unix": [ - "libmimalloc.a" - ] - }, - "ncurses": { - "source": "ncurses", - "static-libs-unix": [ - "libncurses.a" - ] - }, - "net-snmp": { - "source": "net-snmp", - "pkg-configs": [ - "netsnmp", - "netsnmp-agent" - ], - "lib-depends": [ - "openssl", - "zlib" - ] - }, - "nghttp2": { - "source": "nghttp2", - "static-libs-unix": [ - "libnghttp2.a" - ], - "static-libs-windows": [ - "nghttp2.lib" - ], - "headers": [ - "nghttp2" - ], - "lib-depends": [ - "zlib", - "openssl" - ], - "lib-suggests": [ - "libxml2", - "nghttp3", - "ngtcp2" - ] - }, - "nghttp3": { - "source": "nghttp3", - "static-libs-unix": [ - "libnghttp3.a" - ], - "static-libs-windows": [ - "nghttp3.lib" - ], - "headers": [ - "nghttp3" - ], - "lib-depends": [ - "openssl" - ] - }, - "ngtcp2": { - "source": "ngtcp2", - "static-libs-unix": [ - "libngtcp2.a", - "libngtcp2_crypto_ossl.a" - ], - "static-libs-windows": [ - "ngtcp2.lib", - "ngtcp2_crypto_ossl.lib" - ], - "headers": [ - "ngtcp2" - ], - "lib-depends": [ - "openssl" - ], - "lib-suggests": [ - "nghttp3", - "brotli" - ] - }, - "onig": { - "source": "onig", - "static-libs-unix": [ - "libonig.a" - ], - "static-libs-windows": [ - "onig.lib", - "onig_a.lib" - ], - "headers": [ - "oniggnu.h", - "oniguruma.h" - ] - }, - "openssl": { - "source": "openssl", - "pkg-configs": [ - "openssl" - ], - "static-libs-unix": [ - "libssl.a", - "libcrypto.a" - ], - "static-libs-windows": [ - "libssl.lib", - "libcrypto.lib" - ], - "headers": [ - "openssl" - ], - "lib-depends": [ - "zlib" - ] - }, - "postgresql": { - "source": "postgresql", - "pkg-configs": [ - "libpq" - ], - "lib-depends": [ - "libiconv", - "libxml2", - "openssl", - "zlib", - "libedit" - ], - "lib-suggests": [ - "icu", - "libxslt", - "ldap", - "zstd" - ] - }, - "postgresql-win": { - "source": "postgresql-win", - "static-libs": [ - "libpq.lib", - "libpgport.lib", - "libpgcommon.lib" - ] - }, - "pthreads4w": { - "source": "pthreads4w", - "static-libs-windows": [ - "libpthreadVC3.lib" - ] - }, - "qdbm": { - "source": "qdbm", - "static-libs-unix": [ - "libqdbm.a" - ], - "static-libs-windows": [ - "qdbm_a.lib" - ], - "headers-windows": [ - "depot.h" - ] - }, - "re2c": { - "source": "re2c", - "bin-unix": [ - "re2c" - ] - }, - "readline": { - "source": "readline", - "static-libs-unix": [ - "libreadline.a" - ], - "lib-depends": [ - "ncurses" - ] - }, - "snappy": { - "source": "snappy", - "cpp-library": true, - "static-libs-unix": [ - "libsnappy.a" - ], - "headers-unix": [ - "snappy.h", - "snappy-c.h", - "snappy-sinksource.h", - "snappy-stubs-public.h" - ], - "lib-depends": [ - "zlib" - ] - }, - "sqlite": { - "source": "sqlite", - "static-libs-unix": [ - "libsqlite3.a" - ], - "static-libs-windows": [ - "libsqlite3_a.lib" - ], - "headers": [ - "sqlite3.h", - "sqlite3ext.h" - ] - }, - "tidy": { - "source": "tidy", - "static-libs-unix": [ - "libtidy.a" - ] - }, - "unixodbc": { - "source": "unixodbc", - "pkg-configs": [ - "odbc", - "odbccr", - "odbcinst" - ], - "static-libs-unix": [ - "libodbc.a", - "libodbccr.a", - "libodbcinst.a" - ], - "lib-depends": [ - "libiconv" - ] - }, - "watcher": { - "source": "watcher", - "cpp-library": true, - "static-libs-unix": [ - "libwatcher-c.a" - ], - "headers": [ - "wtr/watcher-c.h" - ], - "frameworks": [ - "CoreServices" - ] - }, - "xz": { - "source": "xz", - "static-libs-unix": [ - "liblzma.a" - ], - "static-libs-windows": [ - "lzma.lib", - "liblzma_a.lib" - ], - "headers-unix": [ - "lzma" - ], - "headers-windows": [ - "lzma", - "lzma.h" - ], - "lib-depends-unix": [ - "libiconv" - ] - }, - "zlib": { - "source": "zlib", - "pkg-configs": [ - "zlib" - ], - "static-libs-unix": [ - "libz.a" - ], - "static-libs-windows": [ - "zlib_a.lib" - ], - "headers": [ - "zlib.h", - "zconf.h" - ] - }, - "zstd": { - "source": "zstd", - "pkg-configs": [ - "libzstd" - ], - "static-libs-unix": [ - "libzstd.a" - ], - "static-libs-windows": [ - [ - "zstd.lib", - "zstd_static.lib" - ] - ], - "headers-unix": [ - "zdict.h", - "zstd.h", - "zstd_errors.h" - ], - "headers-windows": [ - "zstd.h", - "zstd_errors.h" - ] - } -} diff --git a/config/pkg.ext.json b/config/pkg.ext.json deleted file mode 100644 index cbcf5ad58..000000000 --- a/config/pkg.ext.json +++ /dev/null @@ -1,1520 +0,0 @@ -{ - "ext-amqp": { - "type": "php-extension", - "artifact": "amqp", - "depends": [ - "librabbitmq" - ], - "depends@windows": [ - "ext-openssl" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-apcu": { - "type": "php-extension", - "artifact": "apcu", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-ast": { - "type": "php-extension", - "artifact": "ast", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-bcmath": { - "type": "php-extension" - }, - "ext-brotli": { - "type": "php-extension", - "artifact": "ext-brotli", - "depends": [ - "brotli" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "arg-type": "enable" - } - }, - "ext-bz2": { - "type": "php-extension", - "depends": [ - "bzip2" - ], - "php-extension": { - "arg-type@windows": "with", - "arg-type": "with-path" - } - }, - "ext-calendar": { - "type": "php-extension" - }, - "ext-ctype": { - "type": "php-extension" - }, - "ext-curl": { - "type": "php-extension", - "depends": [ - "curl" - ], - "depends@windows": [ - "ext-zlib", - "ext-openssl" - ], - "php-extension": { - "arg-type": "with", - "notes": true - } - }, - "ext-dba": { - "type": "php-extension", - "suggests": [ - "qdbm" - ], - "php-extension": { - "arg-type": "custom" - } - }, - "ext-dio": { - "type": "php-extension", - "artifact": "dio", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-dom": { - "type": "php-extension", - "depends": [ - "libxml2", - "zlib" - ], - "depends@windows": [ - "ext-xml" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "arg-type@windows": "with" - } - }, - "ext-ds": { - "type": "php-extension", - "artifact": "ext-ds", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-enchant": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip", - "Darwin": "wip", - "Linux": "wip" - } - } - }, - "ext-ev": { - "type": "php-extension", - "artifact": "ev", - "depends": [ - "ext-sockets" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "arg-type@windows": "with" - } - }, - "ext-event": { - "type": "php-extension", - "artifact": "ext-event", - "depends": [ - "libevent", - "ext-openssl" - ], - "suggests": [ - "ext-sockets" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-exif": { - "type": "php-extension" - }, - "ext-ffi": { - "type": "php-extension", - "depends": [ - "libffi" - ], - "depends@windows": [ - "libffi-win" - ], - "php-extension": { - "support": { - "Linux": "partial", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-fileinfo": { - "type": "php-extension" - }, - "ext-filter": { - "type": "php-extension" - }, - "ext-ftp": { - "type": "php-extension", - "suggests": [ - "openssl" - ] - }, - "ext-gd": { - "type": "php-extension", - "depends": [ - "zlib", - "libpng", - "ext-zlib" - ], - "suggests": [ - "libavif", - "libwebp", - "libjpeg", - "freetype" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "arg-type@windows": "with", - "notes": true - } - }, - "ext-gettext": { - "type": "php-extension", - "depends": [ - "gettext" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-gmp": { - "type": "php-extension", - "depends": [ - "gmp" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-gmssl": { - "type": "php-extension", - "artifact": "ext-gmssl", - "depends": [ - "gmssl" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-grpc": { - "type": "php-extension", - "artifact": "grpc", - "depends": [ - "grpc" - ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "enable-path" - } - }, - "ext-iconv": { - "type": "php-extension", - "depends": [ - "libiconv" - ], - "depends@windows": [ - "libiconv-win" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with-path", - "arg-type@windows": "with" - } - }, - "ext-igbinary": { - "type": "php-extension", - "artifact": "igbinary", - "suggests": [ - "ext-session", - "ext-apcu" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-imagick": { - "type": "php-extension", - "artifact": "ext-imagick", - "depends": [ - "imagemagick" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-imap": { - "type": "php-extension", - "artifact": "ext-imap", - "depends": [ - "imap" - ], - "suggests": [ - "ext-openssl" - ], - "license": { - "type": "file", - "path": [ - "LICENSE" - ] - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-inotify": { - "type": "php-extension", - "artifact": "inotify", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip", - "Darwin": "no" - } - } - }, - "ext-intl": { - "type": "php-extension", - "depends": [ - "icu" - ], - "depends@windows": [ - "icu-static-win" - ], - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-ldap": { - "type": "php-extension", - "depends": [ - "ldap" - ], - "suggests": [ - "gmp", - "libsodium", - "ext-openssl" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-libxml": { - "type": "php-extension", - "depends": [ - "ext-xml" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "none", - "build-shared": false, - "build-static": true, - "build-with-php": true - } - }, - "ext-lz4": { - "type": "php-extension", - "artifact": "ext-lz4", - "depends": [ - "liblz4" - ], - "license": { - "type": "file", - "path": [ - "LICENSE" - ] - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-mbregex": { - "type": "php-extension", - "depends": [ - "onig", - "ext-mbstring" - ], - "php-extension": { - "arg-type": "custom", - "build-shared": false, - "build-static": true - } - }, - "ext-mbstring": { - "type": "php-extension", - "php-extension": { - "arg-type": "custom" - } - }, - "ext-mcrypt": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "no", - "BSD": "no", - "Darwin": "no", - "Linux": "no" - }, - "notes": true - } - }, - "ext-memcache": { - "type": "php-extension", - "artifact": "ext-memcache", - "depends": [ - "ext-zlib", - "ext-session" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom", - "build-with-php": true - } - }, - "ext-memcached": { - "type": "php-extension", - "artifact": "memcached", - "depends": [ - "libmemcached", - "fastlz", - "ext-session", - "ext-zlib" - ], - "suggests": [ - "zstd", - "ext-igbinary", - "ext-msgpack", - "ext-session" - ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-mongodb": { - "type": "php-extension", - "artifact": "mongodb", - "suggests": [ - "icu", - "openssl", - "zstd", - "zlib" - ], - "frameworks": [ - "CoreFoundation", - "Security" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "arg-type": "custom" - } - }, - "ext-msgpack": { - "type": "php-extension", - "artifact": "msgpack", - "depends": [ - "ext-session" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type@windows": "enable", - "arg-type": "with" - } - }, - "ext-mysqli": { - "type": "php-extension", - "depends": [ - "ext-mysqlnd" - ], - "php-extension": { - "arg-type": "with", - "build-with-php": true - } - }, - "ext-mysqlnd": { - "type": "php-extension", - "depends": [ - "zlib" - ], - "php-extension": { - "arg-type@windows": "with", - "build-with-php": true - } - }, - "ext-oci8": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "no", - "Darwin": "no", - "Linux": "no" - }, - "notes": true - } - }, - "ext-odbc": { - "type": "php-extension", - "depends": [ - "unixodbc" - ], - "php-extension": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "arg-type": "custom" - } - }, - "ext-opcache": { - "type": "php-extension", - "php-extension": { - "arg-type@windows": "enable", - "arg-type": "custom", - "zend-extension": true - } - }, - "ext-openssl": { - "type": "php-extension", - "depends": [ - "openssl", - "zlib", - "ext-zlib" - ], - "php-extension": { - "arg-type": "custom", - "arg-type@windows": "with", - "build-with-php": true, - "notes": true - } - }, - "ext-opentelemetry": { - "type": "php-extension", - "artifact": "opentelemetry", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-parallel": { - "type": "php-extension", - "artifact": "parallel", - "depends@windows": [ - "pthreads4w" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type@windows": "with", - "notes": true - } - }, - "ext-password-argon2": { - "type": "php-extension", - "depends": [ - "libargon2", - "openssl" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-pcntl": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "no" - } - } - }, - "ext-pdo": { - "type": "php-extension" - }, - "ext-pdo_mysql": { - "type": "php-extension", - "depends": [ - "ext-pdo", - "ext-mysqlnd" - ], - "php-extension": { - "arg-type": "with" - } - }, - "ext-pdo_odbc": { - "type": "php-extension", - "depends": [ - "unixodbc", - "ext-pdo", - "ext-odbc" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-pdo_pgsql": { - "type": "php-extension", - "depends": [ - "postgresql", - "ext-pdo", - "ext-pgsql" - ], - "depends@windows": [ - "postgresql-win" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with-path", - "arg-type@windows": "custom" - } - }, - "ext-pdo_sqlite": { - "type": "php-extension", - "depends": [ - "sqlite", - "ext-pdo", - "ext-sqlite3" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with" - } - }, - "ext-pdo_sqlsrv": { - "type": "php-extension", - "artifact": "pdo_sqlsrv", - "depends": [ - "ext-pdo", - "ext-sqlsrv" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with" - } - }, - "ext-pgsql": { - "type": "php-extension", - "depends": [ - "postgresql" - ], - "depends@windows": [ - "postgresql-win" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-phar": { - "type": "php-extension", - "depends": [ - "ext-zlib" - ] - }, - "ext-posix": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "no" - } - } - }, - "ext-protobuf": { - "type": "php-extension", - "artifact": "protobuf", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - } - } - }, - "ext-rar": { - "type": "php-extension", - "artifact": "rar", - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip", - "Darwin": "partial" - }, - "notes": true - } - }, - "ext-rdkafka": { - "type": "php-extension", - "artifact": "ext-rdkafka", - "depends": [ - "librdkafka" - ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip", - "Windows": "wip" - }, - "arg-type": "custom" - } - }, - "ext-readline": { - "type": "php-extension", - "depends": [ - "libedit" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path", - "build-shared": false, - "build-static": true - } - }, - "ext-redis": { - "type": "php-extension", - "artifact": "redis", - "suggests": [ - "zstd", - "liblz4", - "ext-session", - "ext-igbinary", - "ext-msgpack" - ], - "license": { - "type": "file", - "path": [ - "LICENSE", - "COPYING" - ] - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-session": { - "type": "php-extension", - "php-extension": { - "build-with-php": true - } - }, - "ext-shmop": { - "type": "php-extension", - "php-extension": { - "build-with-php": true - } - }, - "ext-simdjson": { - "type": "php-extension", - "artifact": "ext-simdjson", - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-simplexml": { - "type": "php-extension", - "depends": [ - "libxml2" - ], - "depends@windows": [ - "ext-xml" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "build-with-php": true - } - }, - "ext-snappy": { - "type": "php-extension", - "artifact": "ext-snappy", - "depends": [ - "snappy" - ], - "suggests": [ - "ext-apcu" - ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-snmp": { - "type": "php-extension", - "depends": [ - "net-snmp" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type@windows": "with", - "arg-type": "with" - } - }, - "ext-soap": { - "type": "php-extension", - "depends": [ - "ext-libxml", - "ext-session" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-sockets": { - "type": "php-extension" - }, - "ext-sodium": { - "type": "php-extension", - "depends": [ - "libsodium" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with" - } - }, - "ext-spx": { - "type": "php-extension", - "artifact": "spx", - "depends": [ - "zlib" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip", - "Windows": "no" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-sqlite3": { - "type": "php-extension", - "depends": [ - "sqlite" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with-path", - "arg-type@windows": "with", - "build-with-php": true - } - }, - "ext-sqlsrv": { - "type": "php-extension", - "artifact": "sqlsrv", - "depends": [ - "unixodbc" - ], - "depends@linux": [ - "ext-pcntl" - ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-ssh2": { - "type": "php-extension", - "artifact": "ext-ssh2", - "depends": [ - "libssh2", - "ext-openssl", - "ext-zlib" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "with-path", - "arg-type@windows": "with" - } - }, - "ext-swoole": { - "type": "php-extension", - "artifact": "swoole", - "depends": [ - "libcares", - "brotli", - "nghttp2", - "zlib", - "ext-openssl", - "ext-curl" - ], - "suggests": [ - "zstd", - "ext-sockets", - "ext-swoole-hook-pgsql", - "ext-swoole-hook-mysql", - "ext-swoole-hook-sqlite", - "ext-swoole-hook-odbc" - ], - "suggests@linux": [ - "zstd", - "liburing" - ], - "lang": "cpp", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-swoole-hook-mysql": { - "type": "php-extension", - "depends": [ - "ext-mysqlnd", - "ext-pdo", - "ext-pdo_mysql", - "ext-swoole" - ], - "suggests": [ - "ext-mysqli" - ], - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "arg-type": "none", - "notes": true - } - }, - "ext-swoole-hook-odbc": { - "type": "php-extension", - "depends": [ - "unixodbc", - "ext-pdo", - "ext-swoole" - ], - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "arg-type": "none", - "notes": true - } - }, - "ext-swoole-hook-pgsql": { - "type": "php-extension", - "depends": [ - "ext-pgsql", - "ext-pdo", - "ext-swoole" - ], - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip", - "Darwin": "partial" - }, - "arg-type": "none", - "notes": true - } - }, - "ext-swoole-hook-sqlite": { - "type": "php-extension", - "depends": [ - "ext-sqlite3", - "ext-pdo", - "ext-swoole" - ], - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "arg-type": "none", - "notes": true - } - }, - "ext-swow": { - "type": "php-extension", - "artifact": "swow", - "suggests": [ - "openssl", - "curl", - "ext-openssl", - "ext-curl" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "notes": true - } - }, - "ext-sysvmsg": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - } - } - }, - "ext-sysvsem": { - "type": "php-extension", - "php-extension": { - "support": { - "Windows": "no", - "BSD": "wip" - } - } - }, - "ext-sysvshm": { - "type": "php-extension", - "php-extension": { - "support": { - "BSD": "wip" - } - } - }, - "ext-tidy": { - "type": "php-extension", - "depends": [ - "tidy" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-tokenizer": { - "type": "php-extension", - "php-extension": { - "build-with-php": true - } - }, - "ext-trader": { - "type": "php-extension", - "artifact": "ext-trader", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip", - "Windows": "wip" - } - } - }, - "ext-uuid": { - "type": "php-extension", - "artifact": "ext-uuid", - "depends": [ - "libuuid" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-uv": { - "type": "php-extension", - "artifact": "ext-uv", - "depends": [ - "libuv", - "ext-sockets" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-xdebug": { - "type": "php-extension", - "artifact": "xdebug", - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "no", - "Darwin": "partial", - "Linux": "partial" - }, - "build-shared": true, - "build-static": false, - "notes": true, - "zend-extension": true - } - }, - "ext-xhprof": { - "type": "php-extension", - "artifact": "xhprof", - "depends": [ - "ext-ctype" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "build-with-php": true, - "notes": true - } - }, - "ext-xlswriter": { - "type": "php-extension", - "artifact": "xlswriter", - "depends": [ - "ext-zlib", - "ext-zip" - ], - "suggests": [ - "openssl" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-xml": { - "type": "php-extension", - "depends": [ - "libxml2" - ], - "depends@windows": [ - "ext-iconv" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "arg-type@windows": "with", - "build-with-php": true, - "notes": true - } - }, - "ext-xmlreader": { - "type": "php-extension", - "depends": [ - "libxml2" - ], - "depends@windows": [ - "ext-xml", - "ext-dom" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "build-with-php": true - } - }, - "ext-xmlwriter": { - "type": "php-extension", - "depends": [ - "libxml2" - ], - "depends@windows": [ - "ext-xml" - ], - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "build-with-php": true - } - }, - "ext-xsl": { - "type": "php-extension", - "depends": [ - "libxslt", - "ext-xml", - "ext-dom" - ], - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "with-path" - } - }, - "ext-xz": { - "type": "php-extension", - "artifact": "ext-xz", - "depends": [ - "xz" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "arg-type": "with" - } - }, - "ext-yac": { - "type": "php-extension", - "artifact": "yac", - "depends": [ - "fastlz", - "ext-igbinary" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom" - } - }, - "ext-yaml": { - "type": "php-extension", - "artifact": "yaml", - "depends": [ - "libyaml" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type@windows": "with", - "arg-type": "with-path" - } - }, - "ext-zip": { - "type": "php-extension", - "artifact": "ext-zip", - "depends": [ - "libzip" - ], - "depends@windows": [ - "libzip", - "zlib", - "bzip2", - "xz", - "ext-zlib", - "ext-bz2" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "BSD": "wip" - }, - "arg-type": "custom", - "arg-type@windows": "enable" - } - }, - "ext-zlib": { - "type": "php-extension", - "depends": [ - "zlib" - ], - "php-extension": { - "arg-type": "custom", - "arg-type@windows": "enable", - "build-shared": false, - "build-static": true, - "build-with-php": true - } - }, - "ext-zstd": { - "type": "php-extension", - "artifact": "ext-zstd", - "depends": [ - "zstd" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "php-extension": { - "support": { - "Windows": "wip", - "BSD": "wip" - }, - "arg-type": "custom" - } - } -} diff --git a/config/pkg.lib.json b/config/pkg.lib.json deleted file mode 100644 index 6cade9b7f..000000000 --- a/config/pkg.lib.json +++ /dev/null @@ -1,992 +0,0 @@ -{ - "attr": { - "artifact": "attr", - "license": { - "type": "file", - "path": "doc/COPYING.LGPL" - }, - "type": "library" - }, - "brotli": { - "artifact": "brotli", - "headers": [ - "brotli" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "libbrotlicommon", - "libbrotlidec", - "libbrotlienc" - ], - "type": "library" - }, - "bzip2": { - "artifact": "bzip2", - "headers": [ - "bzlib.h" - ], - "license": { - "type": "text", - "text": "This program, \"bzip2\", the associated library \"libbzip2\", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\n 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\n 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nJulian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010\n\nPATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement." - }, - "type": "library" - }, - "curl": { - "artifact": "curl", - "depends": [ - "openssl", - "zlib" - ], - "depends@windows": [ - "zlib", - "libssh2", - "nghttp2" - ], - "frameworks": [ - "CoreFoundation", - "CoreServices", - "SystemConfiguration" - ], - "headers": [ - "curl" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "suggests": [ - "libssh2", - "brotli", - "nghttp2", - "nghttp3", - "ngtcp2", - "zstd", - "libcares", - "ldap" - ], - "suggests@windows": [ - "brotli", - "zstd" - ], - "type": "library" - }, - "fastlz": { - "artifact": "fastlz", - "headers": [ - "fastlz/fastlz.h" - ], - "license": { - "type": "file", - "path": "LICENSE.MIT" - }, - "type": "library" - }, - "freetype": { - "artifact": "freetype", - "depends": [ - "zlib" - ], - "headers": [ - "freetype2/freetype/freetype.h", - "freetype2/ft2build.h" - ], - "license": { - "type": "file", - "path": "LICENSE.TXT" - }, - "suggests": [ - "libpng", - "bzip2", - "brotli" - ], - "type": "library" - }, - "gettext": { - "artifact": "gettext", - "depends": [ - "libiconv" - ], - "frameworks": [ - "CoreFoundation" - ], - "license": { - "type": "file", - "path": "gettext-runtime/intl/COPYING.LIB" - }, - "suggests": [ - "ncurses", - "libxml2" - ], - "type": "library" - }, - "glfw": { - "artifact": "ext-glfw", - "frameworks": [ - "CoreVideo", - "OpenGL", - "Cocoa", - "IOKit" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "gmp": { - "artifact": "gmp", - "headers": [ - "gmp.h" - ], - "license": { - "type": "text", - "text": "Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs." - }, - "type": "library" - }, - "gmssl": { - "artifact": "gmssl", - "frameworks": [ - "Security" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "grpc": { - "artifact": "grpc", - "depends": [ - "zlib", - "openssl", - "libcares" - ], - "frameworks": [ - "CoreFoundation" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "grpc" - ], - "type": "library" - }, - "icu": { - "artifact": "icu", - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "icu-uc", - "icu-i18n", - "icu-io" - ], - "type": "library" - }, - "icu-static-win": { - "artifact": "icu-static-win", - "headers@windows": [ - "unicode" - ], - "license": { - "type": "text", - "text": "none" - }, - "type": "library" - }, - "imagemagick": { - "artifact": "imagemagick", - "depends": [ - "zlib", - "libjpeg", - "libjxl", - "libpng", - "libwebp", - "freetype", - "libtiff", - "libheif", - "bzip2" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "Magick++-7.Q16HDRI", - "MagickCore-7.Q16HDRI", - "MagickWand-7.Q16HDRI" - ], - "suggests": [ - "zstd", - "xz", - "libzip", - "libxml2" - ], - "type": "library" - }, - "imap": { - "artifact": "imap", - "license": { - "type": "file", - "path": "LICENSE" - }, - "suggests": [ - "openssl" - ], - "type": "library" - }, - "jbig": { - "artifact": "jbig", - "headers": [ - "jbig.h", - "jbig85.h", - "jbig_ar.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "ldap": { - "artifact": "ldap", - "depends": [ - "openssl", - "zlib", - "gmp", - "libsodium" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "ldap", - "lber" - ], - "type": "library" - }, - "lerc": { - "artifact": "lerc", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libacl": { - "artifact": "libacl", - "depends": [ - "attr" - ], - "license": { - "type": "file", - "path": "doc/COPYING.LGPL" - }, - "type": "library" - }, - "libaom": { - "artifact": "libaom", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libargon2": { - "artifact": "libargon2", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libavif": { - "artifact": "libavif", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libcares": { - "artifact": "libcares", - "headers": [ - "ares.h", - "ares_dns.h", - "ares_nameser.h" - ], - "license": { - "type": "file", - "path": "LICENSE.md" - }, - "type": "library" - }, - "libde265": { - "artifact": "libde265", - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "libedit": { - "artifact": "libedit", - "depends": [ - "ncurses" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "libevent": { - "artifact": "libevent", - "depends": [ - "openssl" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libffi": { - "artifact": "libffi", - "headers": [ - "ffi.h", - "ffitarget.h" - ], - "headers@windows": [ - "ffi.h", - "fficonfig.h", - "ffitarget.h" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libffi-win": { - "artifact": "libffi-win", - "headers@windows": [ - "ffi.h", - "ffitarget.h", - "fficonfig.h" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libheif": { - "artifact": "libheif", - "depends": [ - "libde265", - "libwebp", - "libaom", - "zlib", - "brotli" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "libiconv": { - "artifact": "libiconv", - "headers": [ - "iconv.h", - "libcharset.h", - "localcharset.h" - ], - "license": { - "type": "file", - "path": "COPYING.LIB" - }, - "type": "library" - }, - "libiconv-win": { - "artifact": "libiconv-win", - "license": { - "type": "file", - "path": "source/COPYING" - }, - "type": "library" - }, - "libjpeg": { - "artifact": "libjpeg", - "license": { - "type": "file", - "path": "LICENSE.md" - }, - "suggests@windows": [ - "zlib" - ], - "type": "library" - }, - "libjxl": { - "artifact": "libjxl", - "depends": [ - "brotli", - "libjpeg", - "libpng", - "libwebp" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "libjxl", - "libjxl_cms", - "libjxl_threads", - "libhwy" - ], - "type": "library" - }, - "liblz4": { - "artifact": "liblz4", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libmemcached": { - "artifact": "libmemcached", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libpng": { - "artifact": "libpng", - "depends": [ - "zlib" - ], - "headers": [ - "png.h", - "pngconf.h", - "pnglibconf.h" - ], - "headers@windows": [ - "png.h", - "pngconf.h" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "librabbitmq": { - "artifact": "librabbitmq", - "depends": [ - "openssl" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "librdkafka": { - "artifact": "librdkafka", - "license": { - "type": "file", - "path": "LICENSE" - }, - "pkg-configs": [ - "rdkafka++-static", - "rdkafka-static" - ], - "suggests": [ - "curl", - "liblz4", - "openssl", - "zlib", - "zstd" - ], - "type": "library" - }, - "libsodium": { - "artifact": "libsodium", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "libssh2": { - "artifact": "libssh2", - "depends": [ - "openssl" - ], - "headers": [ - "libssh2.h", - "libssh2_publickey.h", - "libssh2_sftp.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "libtiff": { - "artifact": "libtiff", - "depends": [ - "zlib", - "libjpeg" - ], - "license": { - "type": "file", - "path": "LICENSE.md" - }, - "suggests": [ - "lerc", - "libwebp", - "jbig", - "xz", - "zstd" - ], - "type": "library" - }, - "liburing": { - "artifact": "liburing", - "headers@linux": [ - "liburing/", - "liburing.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "pkg-configs": [ - "liburing", - "liburing-ffi" - ], - "type": "library" - }, - "libuuid": { - "artifact": "libuuid", - "headers": [ - "uuid/uuid.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "libuv": { - "artifact": "libuv", - "license": [ - { - "type": "file", - "path": "LICENSE" - }, - { - "type": "file", - "path": "LICENSE-extra" - } - ], - "type": "library" - }, - "libwebp": { - "artifact": "libwebp", - "license": { - "type": "file", - "path": "COPYING" - }, - "pkg-configs": [ - "libwebp", - "libwebpdecoder", - "libwebpdemux", - "libwebpmux", - "libsharpyuv" - ], - "type": "library" - }, - "libxml2": { - "artifact": "libxml2", - "depends": [ - "libiconv" - ], - "depends@windows": [ - "libiconv-win" - ], - "headers": [ - "libxml2" - ], - "license": { - "type": "file", - "path": "Copyright" - }, - "pkg-configs": [ - "libxml-2.0" - ], - "suggests": [ - "xz", - "zlib" - ], - "suggests@windows": [ - "zlib" - ], - "type": "library" - }, - "libxslt": { - "artifact": "libxslt", - "depends": [ - "libxml2" - ], - "license": { - "type": "file", - "path": "Copyright" - }, - "type": "library" - }, - "libyaml": { - "artifact": "libyaml", - "headers": [ - "yaml.h" - ], - "license": { - "type": "file", - "path": "License" - }, - "type": "library" - }, - "libzip": { - "artifact": "libzip", - "depends": [ - "zlib" - ], - "depends@windows": [ - "zlib", - "bzip2", - "xz" - ], - "headers": [ - "zip.h", - "zipconf.h" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "suggests": [ - "bzip2", - "xz", - "zstd", - "openssl" - ], - "suggests@windows": [ - "zstd", - "openssl" - ], - "type": "library" - }, - "mimalloc": { - "artifact": "mimalloc", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "ncurses": { - "artifact": "ncurses", - "license": { - "type": "file", - "path": "COPYING" - }, - "static-libs@unix": [ - "libncurses.a" - ], - "type": "library" - }, - "net-snmp": { - "artifact": "net-snmp", - "depends": [ - "openssl", - "zlib" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "pkg-configs": [ - "netsnmp", - "netsnmp-agent" - ], - "type": "library" - }, - "nghttp2": { - "artifact": "nghttp2", - "depends": [ - "zlib", - "openssl" - ], - "headers": [ - "nghttp2" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "suggests": [ - "libxml2", - "nghttp3", - "ngtcp2" - ], - "type": "library" - }, - "nghttp3": { - "artifact": "nghttp3", - "depends": [ - "openssl" - ], - "headers": [ - "nghttp3" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "ngtcp2": { - "artifact": "ngtcp2", - "depends": [ - "openssl" - ], - "headers": [ - "ngtcp2" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "suggests": [ - "nghttp3", - "brotli" - ], - "type": "library" - }, - "onig": { - "artifact": "onig", - "headers": [ - "oniggnu.h", - "oniguruma.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "openssl": { - "artifact": "openssl", - "depends": [ - "zlib" - ], - "headers": [ - "openssl" - ], - "license": { - "type": "file", - "path": "LICENSE.txt" - }, - "type": "library" - }, - "postgresql": { - "artifact": "postgresql", - "depends": [ - "libiconv", - "libxml2", - "openssl", - "zlib", - "libedit" - ], - "license": { - "type": "file", - "path": "COPYRIGHT" - }, - "pkg-configs": [ - "libpq" - ], - "suggests": [ - "icu", - "libxslt", - "ldap", - "zstd" - ], - "type": "library" - }, - "postgresql-win": { - "artifact": "postgresql-win", - "license": { - "type": "text", - "text": "PostgreSQL Database Management System\n(also known as Postgres, formerly as Postgres95)\n\nPortions Copyright (c) 1996-2025, The PostgreSQL Global Development Group\n\nPortions Copyright (c) 1994, The Regents of the University of California\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written\nagreement is hereby granted, provided that the above copyright notice\nand this paragraph and the following two paragraphs appear in all\ncopies.\n\nIN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY\nFOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nINCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\nDOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\nTHE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS\nTO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS." - }, - "type": "library" - }, - "pthreads4w": { - "artifact": "pthreads4w", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "qdbm": { - "artifact": "qdbm", - "headers@windows": [ - "depot.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "re2c": { - "artifact": "re2c", - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - }, - "readline": { - "artifact": "readline", - "depends": [ - "ncurses" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "snappy": { - "artifact": "snappy", - "depends": [ - "zlib" - ], - "headers": [ - "snappy.h", - "snappy-c.h", - "snappy-sinksource.h", - "snappy-stubs-public.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "sqlite": { - "artifact": "sqlite", - "headers": [ - "sqlite3.h", - "sqlite3ext.h" - ], - "license": { - "type": "text", - "text": "The author disclaims copyright to this source code. In place of\na legal notice, here is a blessing:\n\n * May you do good and not evil.\n * May you find forgiveness for yourself and forgive others.\n * May you share freely, never taking more than you give." - }, - "type": "library" - }, - "tidy": { - "artifact": "tidy", - "license": { - "type": "file", - "path": "README/LICENSE.md" - }, - "type": "library" - }, - "unixodbc": { - "artifact": "unixodbc", - "depends": [ - "libiconv" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "watcher": { - "artifact": "watcher", - "headers": [ - "wtr/watcher-c.h" - ], - "license": { - "type": "file", - "path": "license" - }, - "type": "library" - }, - "xz": { - "artifact": "xz", - "depends": [ - "libiconv" - ], - "headers": [ - "lzma" - ], - "headers@windows": [ - "lzma", - "lzma.h" - ], - "license": { - "type": "file", - "path": "COPYING" - }, - "type": "library" - }, - "zlib": { - "artifact": "zlib", - "headers": [ - "zlib.h", - "zconf.h" - ], - "license": { - "type": "text", - "text": "(C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly Mark Adler\njloup@gzip.org madler@alumni.caltech.edu" - }, - "type": "library" - }, - "zstd": { - "artifact": "zstd", - "headers": [ - "zdict.h", - "zstd.h", - "zstd_errors.h" - ], - "headers@windows": [ - "zstd.h", - "zstd_errors.h" - ], - "license": { - "type": "file", - "path": "LICENSE" - }, - "type": "library" - } -} diff --git a/config/pre-built.json b/config/pre-built.json deleted file mode 100644 index af8496e22..000000000 --- a/config/pre-built.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "repo": "static-php/static-php-cli-hosted", - "prefer-stable": true, - "match-pattern-linux": "{name}-{arch}-{os}-{libc}-{libcver}.txz", - "match-pattern-macos": "{name}-{arch}-{os}.txz", - "match-pattern-windows": "{name}-{arch}-{os}.tgz" -} diff --git a/config/source.json b/config/source.json deleted file mode 100644 index 18bca217b..000000000 --- a/config/source.json +++ /dev/null @@ -1,1294 +0,0 @@ -{ - "php-src": { - "type": "custom", - "license": { - "type": "file", - "path": "LICENSE" - }, - "alt": false - }, - "amqp": { - "type": "url", - "url": "https://pecl.php.net/get/amqp", - "path": "php-src/ext/amqp", - "filename": "amqp.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "apcu": { - "type": "url", - "url": "https://pecl.php.net/get/APCu", - "path": "php-src/ext/apcu", - "filename": "apcu.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ast": { - "type": "url", - "url": "https://pecl.php.net/get/ast", - "path": "php-src/ext/ast", - "filename": "ast.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "attr": { - "alt": { - "type": "url", - "url": "https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz" - }, - "type": "url", - "url": "https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "doc/COPYING.LGPL" - } - }, - "brotli": { - "type": "ghtagtar", - "repo": "google/brotli", - "match": "v1\\.\\d.*", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "bzip2": { - "alt": { - "type": "filelist", - "url": "https://sourceware.org/pub/bzip2/", - "regex": "/href=\"(?bzip2-(?[^\"]+)\\.tar\\.gz)\"/" - }, - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz", - "provide-pre-built": true, - "license": { - "type": "text", - "text": "This program, \"bzip2\", the associated library \"libbzip2\", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\n 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\n 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nJulian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010\n\nPATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement." - } - }, - "curl": { - "type": "ghrel", - "repo": "curl/curl", - "match": "curl.+\\.tar\\.xz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "dio": { - "type": "url", - "url": "https://pecl.php.net/get/dio", - "path": "php-src/ext/dio", - "filename": "dio.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ev": { - "type": "url", - "url": "https://pecl.php.net/get/ev", - "path": "php-src/ext/ev", - "filename": "ev.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-brotli": { - "type": "git", - "path": "php-src/ext/brotli", - "rev": "master", - "url": "https://github.com/kjdev/php-ext-brotli", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-ds": { - "type": "url", - "url": "https://pecl.php.net/get/ds", - "path": "php-src/ext/ds", - "filename": "ds.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-event": { - "type": "url", - "url": "https://bitbucket.org/osmanov/pecl-event/get/3.1.4.tar.gz", - "path": "php-src/ext/event", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-excimer": { - "type": "url", - "url": "https://pecl.php.net/get/excimer", - "path": "php-src/ext/excimer", - "filename": "excimer.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-glfw": { - "type": "git", - "url": "https://github.com/mario-deluna/php-glfw", - "rev": "master", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-gmssl": { - "type": "ghtar", - "repo": "gmssl/GmSSL-PHP", - "path": "php-src/ext/gmssl", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-grpc": { - "type": "url", - "url": "https://pecl.php.net/get/grpc", - "path": "php-src/ext/grpc", - "filename": "grpc.tgz", - "license": { - "type": "file", - "path": [ - "LICENSE" - ] - } - }, - "ext-imagick": { - "type": "url", - "url": "https://pecl.php.net/get/imagick", - "path": "php-src/ext/imagick", - "filename": "imagick.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-imap": { - "type": "url", - "url": "https://pecl.php.net/get/imap", - "path": "php-src/ext/imap", - "filename": "imap.tgz", - "license": { - "type": "file", - "path": [ - "LICENSE" - ] - } - }, - "ext-lz4": { - "type": "ghtagtar", - "repo": "kjdev/php-ext-lz4", - "path": "php-src/ext/lz4", - "license": { - "type": "file", - "path": [ - "LICENSE" - ] - } - }, - "ext-maxminddb": { - "type": "url", - "url": "https://pecl.php.net/get/maxminddb", - "filename": "ext-maxminddb.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-memcache": { - "type": "url", - "url": "https://pecl.php.net/get/memcache", - "path": "php-src/ext/memcache", - "filename": "memcache.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-rdkafka": { - "type": "ghtar", - "repo": "arnaud-lb/php-rdkafka", - "path": "php-src/ext/rdkafka", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-simdjson": { - "type": "url", - "url": "https://pecl.php.net/get/simdjson", - "path": "php-src/ext/simdjson", - "filename": "simdjson.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-snappy": { - "type": "git", - "path": "php-src/ext/snappy", - "rev": "master", - "url": "https://github.com/kjdev/php-ext-snappy", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-ssh2": { - "type": "url", - "url": "https://pecl.php.net/get/ssh2", - "path": "php-src/ext/ssh2", - "filename": "ssh2.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-trader": { - "type": "url", - "url": "https://pecl.php.net/get/trader", - "path": "php-src/ext/trader", - "filename": "trader.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-uuid": { - "type": "url", - "url": "https://pecl.php.net/get/uuid", - "path": "php-src/ext/uuid", - "filename": "uuid.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-uv": { - "type": "url", - "url": "https://pecl.php.net/get/uv", - "path": "php-src/ext/uv", - "filename": "uv.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-xz": { - "type": "git", - "path": "php-src/ext/xz", - "rev": "main", - "url": "https://github.com/codemasher/php-ext-xz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-zip": { - "type": "url", - "url": "https://pecl.php.net/get/zip", - "filename": "ext-zip.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ext-zstd": { - "type": "git", - "path": "php-src/ext/zstd", - "rev": "master", - "url": "https://github.com/kjdev/php-ext-zstd", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "fastlz": { - "type": "git", - "url": "https://github.com/ariya/FastLZ.git", - "rev": "master", - "license": { - "type": "file", - "path": "LICENSE.MIT" - } - }, - "frankenphp": { - "type": "ghtar", - "repo": "php/frankenphp", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "freetype": { - "type": "ghtagtar", - "repo": "freetype/freetype", - "match": "VER-2-\\d+-\\d+", - "license": { - "type": "file", - "path": "LICENSE.TXT" - } - }, - "gettext": { - "type": "filelist", - "url": "https://ftp.gnu.org/pub/gnu/gettext/", - "regex": "/href=\"(?gettext-(?[^\"]+)\\.tar\\.xz)\"/", - "license": { - "type": "file", - "path": "gettext-runtime/intl/COPYING.LIB" - } - }, - "gmp": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/gmp/", - "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/", - "provide-pre-built": true, - "alt": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz" - }, - "license": { - "type": "text", - "text": "Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs." - } - }, - "gmssl": { - "type": "ghtar", - "repo": "guanzhi/GmSSL", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "grpc": { - "type": "git", - "rev": "v1.75.x", - "url": "https://github.com/grpc/grpc.git", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "icu": { - "type": "ghrel", - "repo": "unicode-org/icu", - "match": "icu4c.+-src\\.tgz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "icu-static-win": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/icu-static-windows-x64/icu-static-windows-x64.zip", - "license": { - "type": "text", - "text": "none" - } - }, - "igbinary": { - "type": "url", - "url": "https://pecl.php.net/get/igbinary", - "path": "php-src/ext/igbinary", - "filename": "igbinary.tgz", - "license": { - "type": "file", - "path": "COPYING" - } - }, - "imagemagick": { - "type": "ghtar", - "repo": "ImageMagick/ImageMagick", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "imap": { - "type": "git", - "url": "https://github.com/static-php/imap.git", - "rev": "master", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "inotify": { - "type": "url", - "url": "https://pecl.php.net/get/inotify", - "path": "php-src/ext/inotify", - "filename": "inotify.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "jbig": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/jbig/jbigkit-2.1.tar.gz", - "provide-pre-built": true, - "alt": { - "type": "url", - "url": "https://www.cl.cam.ac.uk/~mgk25/jbigkit/download/jbigkit-2.1.tar.gz" - }, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "krb5": { - "type": "url", - "url": "https://web.mit.edu/kerberos/dist/krb5/1.22/krb5-1.22.2.tar.gz", - "license": { - "type": "file", - "path": "NOTICE" - } - }, - "ldap": { - "type": "filelist", - "url": "https://www.openldap.org/software/download/OpenLDAP/openldap-release/", - "regex": "/href=\"(?openldap-(?[^\"]+)\\.tgz)\"/", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "lerc": { - "type": "ghtar", - "repo": "Esri/lerc", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libacl": { - "alt": { - "type": "url", - "url": "https://mirror.souseiseki.middlendian.com/nongnu/acl/acl-2.3.2.tar.gz" - }, - "type": "url", - "url": "https://download.savannah.nongnu.org/releases/acl/acl-2.3.2.tar.gz", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "doc/COPYING.LGPL" - } - }, - "libaom": { - "type": "git", - "rev": "main", - "url": "https://aomedia.googlesource.com/aom", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libargon2": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/phc-winner-argon2", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libavif": { - "type": "ghtar", - "repo": "AOMediaCodec/libavif", - "provide-pre-built": false, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libcares": { - "type": "ghrel", - "repo": "c-ares/c-ares", - "match": "c-ares-.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "alt": { - "type": "filelist", - "url": "https://c-ares.org/download/", - "regex": "/href=\"\\/download\\/(?c-ares-(?[^\"]+)\\.tar\\.gz)\"/" - }, - "license": { - "type": "file", - "path": "LICENSE.md" - } - }, - "libde265": { - "type": "ghrel", - "repo": "strukturag/libde265", - "match": "libde265-.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libedit": { - "type": "filelist", - "url": "https://thrysoee.dk/editline/", - "regex": "/href=\"(?libedit-(?[^\"]+)\\.tar\\.gz)\"/", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libevent": { - "type": "ghrel", - "repo": "libevent/libevent", - "match": "libevent.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libffi": { - "type": "ghrel", - "repo": "libffi/libffi", - "match": "libffi.+\\.tar\\.gz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libffi-win": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/libffi-win.git", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libheif": { - "type": "ghrel", - "repo": "strukturag/libheif", - "match": "libheif-.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libiconv": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/libiconv/", - "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING.LIB" - } - }, - "libiconv-win": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/libiconv-win.git", - "license": { - "type": "file", - "path": "source/COPYING" - } - }, - "libidn2": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/libidn/", - "regex": "/href=\"(?libidn2-(?[^\"]+)\\.tar\\.gz)\"/", - "license": { - "type": "file", - "path": "COPYING.LESSERv3" - } - }, - "libjpeg": { - "type": "ghtar", - "repo": "libjpeg-turbo/libjpeg-turbo", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE.md" - } - }, - "libjxl": { - "type": "git", - "url": "https://github.com/libjxl/libjxl", - "rev": "main", - "submodules": [ - "third_party/highway", - "third_party/libjpeg-turbo", - "third_party/sjpeg", - "third_party/skcms" - ], - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "liblz4": { - "type": "ghrel", - "repo": "lz4/lz4", - "match": "lz4-.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libmaxminddb": { - "type": "ghrel", - "repo": "maxmind/libmaxminddb", - "match": "libmaxminddb-.+\\.tar\\.gz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libmemcached": { - "type": "ghtagtar", - "repo": "awesomized/libmemcached", - "match": "1.\\d.\\d", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libpng": { - "type": "ghtagtar", - "repo": "pnggroup/libpng", - "match": "v1\\.6\\.\\d+", - "query": "?per_page=150", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "librabbitmq": { - "type": "ghtar", - "repo": "alanxz/rabbitmq-c", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "librdkafka": { - "type": "ghtar", - "repo": "confluentinc/librdkafka", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libsodium": { - "type": "ghrel", - "repo": "jedisct1/libsodium", - "match": "libsodium-(?!1\\.0\\.21)\\d+(\\.\\d+)*\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "libssh2": { - "type": "ghrel", - "repo": "libssh2/libssh2", - "match": "libssh2.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libtiff": { - "type": "filelist", - "url": "https://download.osgeo.org/libtiff/", - "regex": "/href=\"(?tiff-(?[^\"]+)\\.tar\\.xz)\"/", - "license": { - "type": "file", - "path": "LICENSE.md" - } - }, - "libunistring": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/libunistring/", - "regex": "/href=\"(?libunistring-(?[^\"]+)\\.tar\\.gz)\"/", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING.LIB" - } - }, - "liburing": { - "type": "ghtar", - "repo": "axboe/liburing", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libuuid": { - "type": "git", - "url": "https://github.com/static-php/libuuid.git", - "rev": "master", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libuv": { - "type": "ghtar", - "repo": "libuv/libuv", - "license": [ - { - "type": "file", - "path": "LICENSE" - }, - { - "type": "file", - "path": "LICENSE-extra" - } - ] - }, - "libwebp": { - "type": "ghtagtar", - "repo": "webmproject/libwebp", - "match": "v1\\.\\d+\\.\\d+$", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "libxml2": { - "type": "ghtagtar", - "repo": "GNOME/libxml2", - "match": "v2\\.\\d+\\.\\d+$", - "provide-pre-built": false, - "license": { - "type": "file", - "path": "Copyright" - } - }, - "libxslt": { - "type": "filelist", - "url": "https://download.gnome.org/sources/libxslt/1.1/", - "regex": "/href=\"(?libxslt-(?[^\"]+)\\.tar\\.xz)\"/", - "license": { - "type": "file", - "path": "Copyright" - } - }, - "libyaml": { - "type": "ghrel", - "repo": "yaml/libyaml", - "match": "yaml-.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "License" - } - }, - "libzip": { - "type": "ghrel", - "repo": "nih-at/libzip", - "match": "libzip.+\\.tar\\.xz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "memcached": { - "type": "url", - "url": "https://pecl.php.net/get/memcached", - "path": "php-src/ext/memcached", - "filename": "memcached.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "micro": { - "type": "git", - "path": "php-src/sapi/micro", - "rev": "master", - "url": "https://github.com/static-php/phpmicro", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "mimalloc": { - "type": "ghtagtar", - "repo": "microsoft/mimalloc", - "match": "v2\\.\\d\\.[^3].*", - "provide-pre-built": false, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "mongodb": { - "type": "ghrel", - "repo": "mongodb/mongo-php-driver", - "path": "php-src/ext/mongodb", - "match": "mongodb.+\\.tgz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "msgpack": { - "type": "url", - "url": "https://pecl.php.net/get/msgpack", - "path": "php-src/ext/msgpack", - "filename": "msgpack.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "mysqlnd_ed25519": { - "type": "pie", - "repo": "mariadb/mysqlnd_ed25519", - "path": "php-src/ext/mysqlnd_ed25519", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "mysqlnd_parsec": { - "type": "pie", - "repo": "mariadb/mysqlnd_parsec", - "path": "php-src/ext/mysqlnd_parsec", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "ncurses": { - "type": "filelist", - "url": "https://ftp.gnu.org/pub/gnu/ncurses/", - "regex": "/href=\"(?ncurses-(?[^\"]+)\\.tar\\.gz)\"/", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "net-snmp": { - "type": "ghtagtar", - "repo": "net-snmp/net-snmp", - "license": { - "type": "file", - "path": "COPYING" - } - }, - "nghttp2": { - "type": "ghrel", - "repo": "nghttp2/nghttp2", - "match": "nghttp2.+\\.tar\\.xz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "nghttp3": { - "type": "ghrel", - "repo": "ngtcp2/nghttp3", - "match": "nghttp3.+\\.tar\\.xz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "ngtcp2": { - "type": "ghrel", - "repo": "ngtcp2/ngtcp2", - "match": "ngtcp2.+\\.tar\\.xz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "onig": { - "type": "ghrel", - "repo": "kkos/oniguruma", - "match": "onig-.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "openssl": { - "type": "ghrel", - "repo": "openssl/openssl", - "match": "openssl.+\\.tar\\.gz", - "prefer-stable": true, - "alt": { - "type": "filelist", - "url": "https://www.openssl.org/source/", - "regex": "/href=\"(?openssl-(?[^\"]+)\\.tar\\.gz)\"/" - }, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE.txt" - } - }, - "opentelemetry": { - "type": "url", - "url": "https://pecl.php.net/get/opentelemetry", - "path": "php-src/ext/opentelemetry", - "filename": "opentelemetry.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "parallel": { - "type": "url", - "url": "https://pecl.php.net/get/parallel", - "path": "php-src/ext/parallel", - "filename": "parallel.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "pcov": { - "type": "url", - "url": "https://pecl.php.net/get/pcov", - "filename": "pcov.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "pdo_sqlsrv": { - "type": "url", - "url": "https://pecl.php.net/get/pdo_sqlsrv", - "path": "php-src/ext/pdo_sqlsrv", - "filename": "pdo_sqlsrv.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "pkg-config": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "postgresql": { - "type": "ghtagtar", - "repo": "postgres/postgres", - "match": "REL_18_\\d+", - "license": { - "type": "file", - "path": "COPYRIGHT" - } - }, - "postgresql-win": { - "type": "url", - "url": "https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip", - "license": { - "type": "text", - "text": "PostgreSQL Database Management System\n(also known as Postgres, formerly as Postgres95)\n\nPortions Copyright (c) 1996-2025, The PostgreSQL Global Development Group\n\nPortions Copyright (c) 1994, The Regents of the University of California\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written\nagreement is hereby granted, provided that the above copyright notice\nand this paragraph and the following two paragraphs appear in all\ncopies.\n\nIN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY\nFOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nINCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\nDOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\nTHE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS\nTO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS." - } - }, - "protobuf": { - "type": "url", - "url": "https://pecl.php.net/get/protobuf-5.34.1.tgz", - "path": "php-src/ext/protobuf", - "filename": "protobuf.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "pthreads4w": { - "type": "git", - "rev": "master", - "url": "https://git.code.sf.net/p/pthreads4w/code", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "qdbm": { - "type": "git", - "url": "https://github.com/static-php/qdbm.git", - "rev": "main", - "license": { - "type": "file", - "path": "COPYING" - } - }, - "rar": { - "type": "git", - "url": "https://github.com/static-php/php-rar.git", - "path": "php-src/ext/rar", - "rev": "issue-php82", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "re2c": { - "type": "ghrel", - "repo": "skvadrik/re2c", - "match": "re2c.+\\.tar\\.xz", - "prefer-stable": true, - "alt": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/re2c/re2c-4.3.tar.xz" - }, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "readline": { - "type": "filelist", - "url": "https://ftp.gnu.org/pub/gnu/readline/", - "regex": "/href=\"(?readline-(?[^\"]+)\\.tar\\.gz)\"/", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "redis": { - "type": "url", - "url": "https://pecl.php.net/get/redis", - "path": "php-src/ext/redis", - "filename": "redis.tgz", - "license": { - "type": "file", - "path": [ - "LICENSE", - "COPYING" - ] - } - }, - "snappy": { - "type": "git", - "rev": "main", - "url": "https://github.com/google/snappy", - "license": { - "type": "file", - "path": "COPYING" - } - }, - "spx": { - "type": "pie", - "repo": "noisebynorthwest/php-spx", - "path": "php-src/ext/spx", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "sqlite": { - "type": "url", - "url": "https://www.sqlite.org/2024/sqlite-autoconf-3450200.tar.gz", - "provide-pre-built": true, - "license": { - "type": "text", - "text": "The author disclaims copyright to this source code. In place of\na legal notice, here is a blessing:\n\n * May you do good and not evil.\n * May you find forgiveness for yourself and forgive others.\n * May you share freely, never taking more than you give." - } - }, - "sqlsrv": { - "type": "url", - "url": "https://pecl.php.net/get/sqlsrv", - "path": "php-src/ext/sqlsrv", - "filename": "sqlsrv.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "swoole": { - "path": "php-src/ext/swoole", - "type": "ghtar", - "repo": "swoole/swoole-src", - "match": "v6\\.+", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "swow": { - "path": "php-src/ext/swow-src", - "type": "ghtar", - "repo": "swow/swow", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "tidy": { - "type": "ghtar", - "repo": "htacg/tidy-html5", - "prefer-stable": true, - "license": { - "type": "file", - "path": "README/LICENSE.md" - } - }, - "unixodbc": { - "type": "url", - "url": "https://www.unixodbc.org/unixODBC-2.3.12.tar.gz", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "watcher": { - "type": "ghtar", - "repo": "e-dant/watcher", - "prefer-stable": true, - "license": { - "type": "file", - "path": "license" - } - }, - "xdebug": { - "type": "pie", - "repo": "xdebug/xdebug", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "xhprof": { - "type": "url", - "url": "https://pecl.php.net/get/xhprof", - "path": "php-src/ext/xhprof-src", - "filename": "xhprof.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "xlswriter": { - "type": "url", - "url": "https://pecl.php.net/get/xlswriter", - "path": "php-src/ext/xlswriter", - "filename": "xlswriter.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "xz": { - "type": "ghrel", - "repo": "tukaani-project/xz", - "match": "xz.+\\.tar\\.xz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "file", - "path": "COPYING" - } - }, - "yac": { - "type": "url", - "url": "https://pecl.php.net/get/yac", - "path": "php-src/ext/yac", - "filename": "yac.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "yaml": { - "type": "git", - "path": "php-src/ext/yaml", - "rev": "php7", - "url": "https://github.com/php/pecl-file_formats-yaml", - "license": { - "type": "file", - "path": "LICENSE" - } - }, - "zlib": { - "type": "ghrel", - "repo": "madler/zlib", - "match": "zlib.+\\.tar\\.gz", - "prefer-stable": true, - "provide-pre-built": true, - "license": { - "type": "text", - "text": "(C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly Mark Adler\njloup@gzip.org madler@alumni.caltech.edu" - } - }, - "zstd": { - "type": "ghrel", - "repo": "facebook/zstd", - "match": "zstd.+\\.tar\\.gz", - "prefer-stable": true, - "license": { - "type": "file", - "path": "LICENSE" - } - } -} diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php deleted file mode 100644 index 9685a1d88..000000000 --- a/src/SPC/ConsoleApplication.php +++ /dev/null @@ -1,78 +0,0 @@ -addCommands( - [ - // Craft command - new CraftCommand(), - // Common commands - new BuildPHPCommand(), - new BuildLibsCommand(), - new DoctorCommand(), - new DownloadCommand(), - new InstallPkgCommand(), - new DeleteDownloadCommand(), - new DumpLicenseCommand(), - new ExtractCommand(), - new MicroCombineCommand(), - new SwitchPhpVersionCommand(), - new SPCConfigCommand(), - new DumpExtensionsCommand(), - - // Dev commands - new AllExtCommand(), - new PhpVerCommand(), - new LibVerCommand(), - new ExtVerCommand(), - new SortConfigCommand(), - new GenerateExtDocCommand(), - new GenerateExtDepDocsCommand(), - new GenerateLibDepDocsCommand(), - new PackLibCommand(), - new EnvCommand(), - ] - ); - } -} diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php deleted file mode 100644 index fdba936d7..000000000 --- a/src/SPC/builder/BuilderBase.php +++ /dev/null @@ -1,543 +0,0 @@ - libraries */ - protected array $libs = []; - - /** @var array extensions */ - protected array $exts = []; - - /** @var array extension names */ - protected array $ext_list = []; - - /** @var array library names */ - protected array $lib_list = []; - - /** @var bool compile libs only (just mark it) */ - protected bool $libs_only = false; - - /** @var array compile options */ - protected array $options = []; - - /** @var string patch point name */ - protected string $patch_point = ''; - - /** - * Convert libraries to class - * - * @param array $sorted_libraries Libraries to build (if not empty, must sort first) - * - * @internal - */ - abstract public function proveLibs(array $sorted_libraries); - - /** - * Set-Up libraries - */ - public function setupLibs(): void - { - // build all libs - foreach ($this->libs as $lib) { - $starttime = microtime(true); - $status = $lib->setup($this->getOption('rebuild', false)); - match ($status) { - LIB_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] setup success, took ' . round(microtime(true) - $starttime, 2) . ' s'), - LIB_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'), - LIB_STATUS_INSTALL_FAILED => logger()->error('lib [' . $lib::NAME . '] install failed'), - default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'), - }; - if (in_array($status, [LIB_STATUS_BUILD_FAILED, LIB_STATUS_INSTALL_FAILED])) { - throw new BuildFailureException('Library [' . $lib::NAME . '] setup failed.'); - } - } - } - - /** - * Add library to build. - * - * @param LibraryBase $library Library object - */ - public function addLib(LibraryBase $library): void - { - $this->libs[$library::NAME] = $library; - } - - /** - * Get library object by name. - */ - public function getLib(string $name): ?LibraryBase - { - return $this->libs[$name] ?? null; - } - - /** - * Get all library objects. - * - * @return LibraryBase[] - */ - public function getLibs(): array - { - return $this->libs; - } - - /** - * Add extension to build. - */ - public function addExt(Extension $extension): void - { - $this->exts[$extension->getName()] = $extension; - } - - /** - * Get extension object by name. - */ - public function getExt(string $name): ?Extension - { - return $this->exts[$name] ?? null; - } - - /** - * Get all extension objects. - * - * @return Extension[] - */ - public function getExts(bool $including_shared = true): array - { - if ($including_shared) { - return $this->exts; - } - return array_filter($this->exts, fn ($ext) => $ext->isBuildStatic()); - } - - /** - * Set libs only mode. - * - * @internal - */ - public function setLibsOnly(bool $status = true): void - { - $this->libs_only = $status; - } - - /** - * Verify the list of "ext" extensions for validity and declare an Extension object to check the dependencies of the extensions. - * - * @internal - */ - public function proveExts(array $static_extensions, array $shared_extensions = [], bool $skip_check_deps = false, bool $skip_extract = false): void - { - // judge ext - foreach ($static_extensions as $ext) { - // if extension does not support static build, throw exception - if (!in_array('static', Config::getExtTarget($ext))) { - throw new WrongUsageException('Extension [' . $ext . '] does not support static build!'); - } - } - foreach ($shared_extensions as $ext) { - // if extension does not support shared build, throw exception - if (!in_array('shared', Config::getExtTarget($ext)) && !in_array($ext, $shared_extensions)) { - throw new WrongUsageException('Extension [' . $ext . '] does not support shared build!'); - } - } - if (!$skip_extract) { - $this->emitPatchPoint('before-php-extract'); - SourceManager::initSource(sources: ['php-src'], source_only: true); - $this->emitPatchPoint('after-php-extract'); - if ($this->getPHPVersionID() >= 80000) { - $this->emitPatchPoint('before-micro-extract'); - SourceManager::initSource(sources: ['micro'], source_only: true); - $this->emitPatchPoint('after-micro-extract'); - } - $this->emitPatchPoint('before-exts-extract'); - SourceManager::initSource(exts: [...$static_extensions, ...$shared_extensions]); - $this->emitPatchPoint('after-exts-extract'); - // patch micro - SourcePatcher::patchMicro(); - } - - foreach ([...$static_extensions, ...$shared_extensions] as $extension) { - $class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class; - /** @var Extension $ext */ - $ext = new $class($extension, $this); - if (in_array($extension, $static_extensions)) { - $ext->setBuildStatic(); - } - if (in_array($extension, $shared_extensions)) { - $ext->setBuildShared(); - } - $this->addExt($ext); - } - - if ($skip_check_deps) { - return; - } - - foreach ($this->getExts() as $ext) { - $ext->checkDependency(); - } - $this->ext_list = [...$static_extensions, ...$shared_extensions]; - } - - /** - * Start to build PHP - * - * @param int $build_target Build target, see BUILD_TARGET_* - */ - abstract public function buildPHP(int $build_target = BUILD_TARGET_NONE); - - /** - * Test PHP - */ - abstract public function testPHP(int $build_target = BUILD_TARGET_NONE); - - /** - * Build shared extensions. - */ - public function buildSharedExts(): void - { - $lines = file(BUILD_BIN_PATH . '/php-config'); - $extension_dir_line = null; - foreach ($lines as $key => $value) { - if (str_starts_with($value, 'extension_dir=')) { - $lines[$key] = 'extension_dir="' . BUILD_MODULES_PATH . '"' . PHP_EOL; - $extension_dir_line = $value; - break; - } - } - file_put_contents(BUILD_BIN_PATH . '/php-config', implode('', $lines)); - FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes'); - FileSystem::createDir(BUILD_MODULES_PATH); - try { - foreach ($this->getExts() as $ext) { - if (!$ext->isBuildShared()) { - continue; - } - $ext->buildShared(); - } - } finally { - FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line); - } - FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line); - FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', '# test "[$]$1" = "no" && $1=yes', 'test "[$]$1" = "no" && $1=yes'); - } - - /** - * Generate extension enable arguments for configure. - * e.g. --enable-mbstring - */ - public function makeStaticExtensionArgs(): string - { - $ret = []; - foreach ($this->getExts() as $ext) { - $arg = null; - if ($ext->isBuildShared() && !$ext->isBuildStatic()) { - if ( - (Config::getExt($ext->getName(), 'type') === 'builtin' && - !file_exists(SOURCE_PATH . '/php-src/ext/' . $ext->getName() . '/config.m4')) || - Config::getExt($ext->getName(), 'build-with-php') === true - ) { - $arg = $ext->getConfigureArg(true); - } else { - continue; - } - } - $arg ??= $ext->getConfigureArg(); - logger()->info($ext->getName() . ' is using ' . $arg); - $ret[] = trim($arg); - } - logger()->debug('Using configure: ' . implode(' ', $ret)); - return implode(' ', $ret); - } - - /** - * Get libs only mode. - */ - public function isLibsOnly(): bool - { - return $this->libs_only; - } - - /** - * Get PHP Version ID from php-src/main/php_version.h - */ - public function getPHPVersionID(): int - { - if (!file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - throw new WrongUsageException('PHP source files are not available, you need to download them first'); - } - - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { - return intval($match[1]); - } - - throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); - } - - public function getPHPVersion(bool $exception_on_failure = true): string - { - if (!file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - if (!$exception_on_failure) { - return 'unknown'; - } - throw new WrongUsageException('PHP source files are not available, you need to download them first'); - } - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION "(.*)"/', $file, $match) !== 0) { - return $match[1]; - } - if (!$exception_on_failure) { - return 'unknown'; - } - throw new WrongUsageException('PHP version file format is malformed, please remove it and download again'); - } - - /** - * Get PHP version from archive file name. - * - * @param null|string $file php-*.*.*.tar.gz filename, read from lockfile if empty - */ - public function getPHPVersionFromArchive(?string $file = null): false|string - { - if ($file === null) { - $lock = LockFile::get('php-src'); - if ($lock === null) { - return false; - } - $file = LockFile::getLockFullPath($lock); - } - if (preg_match('/php-(\d+\.\d+\.\d+(?:RC\d+|alpha\d+|beta\d+)?)\.tar\.(?:gz|bz2|xz)/', $file, $match)) { - return $match[1]; - } - return false; - } - - public function getMicroVersion(): false|string - { - $file = FileSystem::convertPath(SOURCE_PATH . '/php-src/sapi/micro/php_micro.h'); - if (!file_exists($file)) { - return false; - } - - $content = file_get_contents($file); - $ver = ''; - preg_match('/#define PHP_MICRO_VER_MAJ (\d)/m', $content, $match); - $ver .= $match[1] . '.'; - preg_match('/#define PHP_MICRO_VER_MIN (\d)/m', $content, $match); - $ver .= $match[1] . '.'; - preg_match('/#define PHP_MICRO_VER_PAT (\d)/m', $content, $match); - $ver .= $match[1]; - return $ver; - } - - /** - * Get build type name string to display. - * - * @param int $type Build target type - */ - public function getBuildTypeName(int $type): string - { - $ls = []; - if (($type & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { - $ls[] = 'cli'; - } - if (($type & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - $ls[] = 'micro'; - } - if (($type & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) { - $ls[] = 'fpm'; - } - if (($type & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) { - $ls[] = 'embed'; - } - if (($type & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { - $ls[] = 'frankenphp'; - } - if (($type & BUILD_TARGET_CGI) === BUILD_TARGET_CGI) { - $ls[] = 'cgi'; - } - return implode(', ', $ls); - } - - /** - * Get builder options (maybe changed by user) - * - * @param string $key Option key - * @param mixed $default If not exists, return this value - */ - public function getOption(string $key, mixed $default = null): mixed - { - return $this->options[$key] ?? $default; - } - - /** - * Get all builder options - */ - public function getOptions(): array - { - return $this->options; - } - - /** - * Set builder options if not exists. - */ - public function setOptionIfNotExist(string $key, mixed $value): void - { - if (!isset($this->options[$key])) { - $this->options[$key] = $value; - } - } - - /** - * Set builder options. - */ - public function setOption(string $key, mixed $value): void - { - $this->options[$key] = $value; - } - - public function getEnvString(array $vars = ['cc', 'cxx', 'ar', 'ld']): string - { - $env = []; - foreach ($vars as $var) { - $var = strtoupper($var); - if (getenv($var) !== false) { - $env[] = "{$var}=" . getenv($var); - } - } - return implode(' ', $env); - } - - /** - * Get builder patch point name. - */ - public function getPatchPoint(): string - { - return $this->patch_point; - } - - /** - * Validate libs and exts can be compiled successfully in current environment - */ - public function validateLibsAndExts(): void - { - foreach ($this->libs as $lib) { - $lib->validate(); - } - foreach ($this->getExts() as $ext) { - $ext->validate(); - } - } - - public function emitPatchPoint(string $point_name): void - { - $this->patch_point = $point_name; - if (($patches = $this->getOption('with-added-patch', [])) === []) { - return; - } - - foreach ($patches as $patch) { - try { - if (!file_exists($patch)) { - throw new WrongUsageException("Additional patch script file {$patch} not found!"); - } - logger()->debug('Running additional patch script: ' . $patch); - require $patch; - } catch (InterruptException $e) { - if ($e->getCode() === 0) { - logger()->notice('Patch script ' . $patch . ' interrupted' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.')); - } else { - logger()->error('Patch script ' . $patch . ' interrupted with error code [' . $e->getCode() . ']' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.')); - } - exit($e->getCode()); - } catch (\Throwable $e) { - logger()->critical('Patch script ' . $patch . ' failed to run.'); - throw $e; - } - } - } - - public function checkBeforeBuildPHP(int $rule): void - { - if (($rule & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { - if (!$this->getOption('enable-zts')) { - throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); - } - // frankenphp doesn't support windows, BSD is currently not supported by static-php-cli - if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { - throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); - } - // frankenphp needs package go-xcaddy installed - if (!GoXcaddy::isInstalled()) { - global $argv; - throw new WrongUsageException("FrankenPHP SAPI requires the go-xcaddy package, please install it first: {$argv[0]} install-pkg go-xcaddy"); - } - // frankenphp needs libxml2 lib on macos, see: https://github.com/php/frankenphp/blob/main/frankenphp.go#L17 - if (PHP_OS_FAMILY === 'Darwin' && !$this->getLib('libxml2')) { - throw new WrongUsageException('FrankenPHP SAPI for macOS requires libxml2 library, please include the `xml` extension in your build.'); - } - } - } - - /** - * Generate micro extension test php code. - */ - protected function generateMicroExtTests(): string - { - $php = "getExts(false) as $ext) { - $ext_name = $ext->getDistName(); - if (!empty($ext_name)) { - $php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n"; - $php .= "assert(extension_loaded('{$ext_name}'));\n\n"; - } - } - $php .= "echo '[micro-test-end]';\n"; - return $php; - } - - protected function getMicroTestTasks(): array - { - return [ - 'micro_ext_test' => [ - 'content' => ($this->getOption('without-micro-ext-test') ? 'generateMicroExtTests()), - 'conditions' => [ - // program success - function ($ret) { return $ret === 0; }, - // program returns expected output - function ($ret, $out) { - $raw_out = trim(implode('', $out)); - return str_starts_with($raw_out, '[micro-test-start]') && str_ends_with($raw_out, '[micro-test-end]'); - }, - ], - ], - 'micro_zend_bug_test' => [ - 'content' => ($this->getOption('without-micro-ext-test') ? ' [ - // program success - function ($ret) { return $ret === 0; }, - ], - ], - ]; - } -} diff --git a/src/SPC/builder/BuilderProvider.php b/src/SPC/builder/BuilderProvider.php deleted file mode 100644 index ec6141e64..000000000 --- a/src/SPC/builder/BuilderProvider.php +++ /dev/null @@ -1,47 +0,0 @@ - new WindowsBuilder($input->getOptions()), - 'Darwin' => new MacOSBuilder($input->getOptions()), - 'Linux' => new LinuxBuilder($input->getOptions()), - 'BSD' => new BSDBuilder($input->getOptions()), - default => throw new WrongUsageException('Current OS "' . PHP_OS_FAMILY . '" is not supported yet'), - }; - - // bind the builder to ExceptionHandler - ExceptionHandler::bindBuilder(self::$builder); - - return self::$builder; - } - - public static function getBuilder(): BuilderBase - { - if (self::$builder === null) { - throw new WrongUsageException('Builder has not been initialized'); - } - return self::$builder; - } -} diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php deleted file mode 100644 index 9077269e5..000000000 --- a/src/SPC/builder/Extension.php +++ /dev/null @@ -1,617 +0,0 @@ -name, 'type'); - $unix_only = Config::getExt($this->name, 'unix-only', false); - $windows_only = Config::getExt($this->name, 'windows-only', false); - if (PHP_OS_FAMILY !== 'Windows' && $windows_only) { - throw new EnvironmentException("{$ext_type} extension {$name} is not supported on Linux and macOS platform"); - } - if (PHP_OS_FAMILY === 'Windows' && $unix_only) { - throw new EnvironmentException("{$ext_type} extension {$name} is not supported on Windows platform"); - } - // set source_dir for builtin - if ($ext_type === 'builtin') { - $this->source_dir = SOURCE_PATH . '/php-src/ext/' . $this->name; - } elseif ($ext_type === 'external') { - $source = Config::getExt($this->name, 'source'); - if ($source === null) { - throw new ValidationException("{$ext_type} extension {$name} source not found", validation_module: "Extension [{$name}] loader"); - } - $source_path = Config::getSource($source)['path'] ?? null; - $source_path = $source_path === null ? SOURCE_PATH . '/' . $source : SOURCE_PATH . '/' . $source_path; - $this->source_dir = $source_path; - } else { - $this->source_dir = SOURCE_PATH . '/php-src'; - } - } - - public function getFrameworks(): array - { - return Config::getExt($this->getName(), 'frameworks', []); - } - - /** - * 获取开启该扩展的 PHP 编译添加的参数 - */ - public function getConfigureArg(bool $shared = false): string - { - return match (PHP_OS_FAMILY) { - 'Windows' => $this->getWindowsConfigureArg($shared), - 'Darwin', - 'Linux', - 'BSD' => $this->getUnixConfigureArg($shared), - default => throw new WrongUsageException(PHP_OS_FAMILY . ' build is not supported yet'), - }; - } - - /** - * 根据 ext 的 arg-type 获取对应开启的参数,一般都是 --enable-xxx 和 --with-xxx - */ - public function getEnableArg(bool $shared = false): string - { - $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $_name = str_replace('_', '-', $this->name); - return match ($arg_type = Config::getExt($this->name, 'arg-type', 'enable')) { - 'enable' => '--enable-' . $_name . ($shared ? '=shared' : '') . ' ', - 'enable-path' => '--enable-' . $_name . '=' . ($shared ? 'shared,' : '') . $escapedPath . ' ', - 'with' => '--with-' . $_name . ($shared ? '=shared' : '') . ' ', - 'with-path' => '--with-' . $_name . '=' . ($shared ? 'shared,' : '') . $escapedPath . ' ', - 'none', 'custom' => '', - default => throw new WrongUsageException("argType does not accept {$arg_type}, use [enable/with/with-path] ."), - }; - } - - /** - * 导出当前扩展依赖的所有 lib 库生成的 .a 静态编译库文件,以字符串形式导出,用空格分割 - */ - public function getLibFilesString(): string - { - $ret = array_map( - fn ($x) => $x->getStaticLibFiles(), - $this->getLibraryDependencies(recursive: true) - ); - $libs = implode(' ', $ret); - return deduplicate_flags($libs); - } - - /** - * 检查下依赖就行了,作用是导入依赖给 Extension 对象,今后可以对库依赖进行选择性处理 - */ - public function checkDependency(): static - { - foreach (Config::getExt($this->name, 'lib-depends', []) as $name) { - $this->addLibraryDependency($name); - } - foreach (Config::getExt($this->name, 'lib-suggests', []) as $name) { - $this->addLibraryDependency($name, true); - } - foreach (Config::getExt($this->name, 'ext-depends', []) as $name) { - $this->addExtensionDependency($name); - } - foreach (Config::getExt($this->name, 'ext-suggests', []) as $name) { - $this->addExtensionDependency($name, true); - } - return $this; - } - - public function getExtensionDependency(): array - { - return array_filter($this->dependencies, fn ($x) => $x instanceof Extension); - } - - public function getName(): string - { - return $this->name; - } - - /** - * returns extension dist name - */ - public function getDistName(): string - { - return $this->name; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - return $this->getEnableArg(); - // Windows is not supported yet - } - - public function getUnixConfigureArg(bool $shared = false): string - { - return $this->getEnableArg($shared); - } - - /** - * Patch code before ./buildconf - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeBuildconf(): bool - { - return false; - } - - /** - * Patch code before ./configure - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeConfigure(): bool - { - return false; - } - - /** - * Patch code before ./configure.bat for Windows - */ - public function patchBeforeWindowsConfigure(): bool - { - return false; - } - - /** - * Patch code before make - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeMake(): bool - { - if (SPCTarget::getTargetOS() === 'Linux' && $this->isBuildShared() && ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS'))) { - FileSystem::replaceFileRegex( - SOURCE_PATH . '/php-src/Makefile', - "/^(shared_objects_{$this->getName()}\\s*=.*)$/m", - "$1 {$objs}", - ); - return true; - } - return false; - } - - /** - * Patch code before shared extension phpize - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeSharedPhpize(): bool - { - return false; - } - - /** - * Patch code before shared extension ./configure - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeSharedConfigure(): bool - { - return false; - } - - /** - * Patch code before shared extension make - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeSharedMake(): bool - { - $config = (new SPCConfigUtil($this->builder))->getExtensionConfig($this); - [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); - $lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a') ? '-l:libstdc++.a' : null; - $lstdcpp ??= str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : ''; - - $makefileContent = file_get_contents($this->source_dir . '/Makefile'); - if (preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $makefileContent, $matches)) { - $prefix = $matches[1]; - $currentLibs = trim($matches[2]); - $newLibs = trim("{$currentLibs} {$staticLibs} {$lstdcpp}"); - $deduplicatedLibs = deduplicate_flags($newLibs); - - FileSystem::replaceFileRegex( - $this->source_dir . '/Makefile', - '/^(.*_SHARED_LIBADD\s*=.*)$/m', - $prefix . $deduplicatedLibs - ); - } - - if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) { - FileSystem::replaceFileRegex( - $this->source_dir . '/Makefile', - "/^(shared_objects_{$this->getName()}\\s*=.*)$/m", - "$1 {$objs}", - ); - } - return true; - } - - /** - * @return string - * returns a command line string with all required shared extensions to load - * i.e.; pdo_pgsql would return: - * - * `-d "extension=pgsql" -d "extension=pdo_pgsql"` - */ - public function getSharedExtensionLoadString(): string - { - $loaded = []; - $order = []; - - $resolve = function ($extension) use (&$resolve, &$loaded, &$order) { - if (!$extension instanceof Extension) { - return; - } - if (isset($loaded[$extension->getName()])) { - return; - } - $loaded[$extension->getName()] = true; - - foreach ($extension->dependencies as $dependency) { - $resolve($dependency); - } - - $order[] = $extension; - }; - - $resolve($this); - - $ret = ''; - foreach ($order as $ext) { - if ($ext instanceof self && $ext->isBuildShared()) { - if (Config::getExt($ext->getName(), 'type', false) === 'addon') { - continue; - } - if (Config::getExt($ext->getName(), 'zend-extension', false) === true) { - $ret .= " -d \"zend_extension={$ext->getName()}\""; - } else { - $ret .= " -d \"extension={$ext->getName()}\""; - } - } - } - - if ($ret !== '') { - $ret = ' -d "extension_dir=' . BUILD_MODULES_PATH . '"' . $ret; - } - - return $ret; - } - - public function runCliCheckUnix(): void - { - // Run compile check if build target is cli - // If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php - $sharedExtensions = $this->getSharedExtensionLoadString(); - [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"'); - if ($ret !== 0) { - throw new ValidationException( - "extension {$this->getName()} failed compile check: php-cli returned {$ret}", - validation_module: 'Extension ' . $this->getName() . ' sanity check' - ); - } - - if (file_exists(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php')) { - // Trim additional content & escape special characters to allow inline usage - $test = str_replace( - ['getName() . '.php') - ); - - [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' -r "' . trim($test) . '"'); - if ($ret !== 0) { - throw new ValidationException( - "extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out), - validation_module: 'Extension ' . $this->getName() . ' function check' - ); - } - } - } - - public function runCliCheckWindows(): void - { - // Run compile check if build target is cli - // If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php - [$ret] = cmd()->execWithResult(BUILD_BIN_PATH . '/php.exe -n --ri "' . $this->getDistName() . '"', false); - if ($ret !== 0) { - throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check"); - } - - if (file_exists(FileSystem::convertPath(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php'))) { - // Trim additional content & escape special characters to allow inline usage - $test = str_replace( - ['getName() . '.php')) - ); - - [$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n -r "' . trim($test) . '"'); - if ($ret !== 0) { - throw new ValidationException( - "extension {$this->getName()} failed function check", - validation_module: "Extension {$this->getName()} function check" - ); - } - } - } - - public function validate(): void - { - // do nothing, just throw wrong usage exception if not valid - } - - /** - * Build shared extension - */ - public function buildShared(array $visited = []): void - { - try { - if (Config::getExt($this->getName(), 'type') === 'builtin' || Config::getExt($this->getName(), 'build-with-php') === true) { - if (file_exists(BUILD_MODULES_PATH . '/' . $this->getName() . '.so')) { - logger()->info('Shared extension [' . $this->getName() . '] was already built by php-src/configure (' . $this->getName() . '.so)'); - return; - } - if (Config::getExt($this->getName(), 'build-with-php') === true) { - logger()->warning('Shared extension [' . $this->getName() . '] did not build with php-src/configure (' . $this->getName() . '.so)'); - logger()->warning('Try deleting your build and source folders and running `spc build`` again.'); - return; - } - } - if (file_exists(BUILD_MODULES_PATH . '/' . $this->getName() . '.so')) { - logger()->info('Shared extension [' . $this->getName() . '] was already built, skipping (' . $this->getName() . '.so)'); - return; - } - if ((string) Config::getExt($this->getName(), 'type') === 'addon') { - return; - } - logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)'); - foreach ($this->dependencies as $dependency) { - if (!$dependency instanceof Extension) { - continue; - } - if (!$dependency->isBuildStatic() && !in_array($dependency->getName(), $visited)) { - logger()->info('extension ' . $this->getName() . ' requires extension ' . $dependency->getName()); - $dependency->buildShared([...$visited, $this->getName()]); - } - } - $this->builder->emitPatchPoint('before-shared-ext[' . $this->getName() . ']-build'); - match (PHP_OS_FAMILY) { - 'Darwin', 'Linux' => $this->buildUnixShared(), - default => throw new WrongUsageException(PHP_OS_FAMILY . ' build shared extensions is not supported yet'), - }; - $this->builder->emitPatchPoint('after-shared-ext[' . $this->getName() . ']-build'); - } catch (SPCException $e) { - $e->bindExtensionInfo(['extension_name' => $this->getName()]); - throw $e; - } - } - - /** - * Build shared extension for Unix - */ - public function buildUnixShared(): void - { - $env = $this->getSharedExtensionEnv(); - if ($this->patchBeforeSharedPhpize()) { - logger()->info("Extension [{$this->getName()}] patched before shared phpize"); - } - - // prepare configure args - shell()->cd($this->source_dir) - ->setEnv($env) - ->appendEnv($this->getExtraEnv()) - ->exec(BUILD_BIN_PATH . '/phpize'); - - if ($this->patchBeforeSharedConfigure()) { - logger()->info("Extension [{$this->getName()}] patched before shared configure"); - } - - $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; - - shell()->cd($this->source_dir) - ->setEnv($env) - ->appendEnv($this->getExtraEnv()) - ->exec( - './configure ' . $this->getUnixConfigureArg(true) . - ' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' . - "--enable-shared --disable-static {$phpvars}" - ); - - if ($this->patchBeforeSharedMake()) { - logger()->info("Extension [{$this->getName()}] patched before shared make"); - } - - shell()->cd($this->source_dir) - ->setEnv($env) - ->appendEnv($this->getExtraEnv()) - ->exec('make clean') - ->exec('make -j' . $this->builder->concurrency) - ->exec('make install'); - - // process *.so file - $soFile = BUILD_MODULES_PATH . '/' . $this->getName() . '.so'; - $soDest = $soFile; - preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); - if (!empty($matches[1])) { - $soDest = str_replace('.so', '-' . $matches[1] . '.so', $soFile); - } - if (!file_exists($soFile)) { - throw new ValidationException("extension {$this->getName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getName()} build"); - } - /** @var UnixBuilderBase $builder */ - $builder = $this->builder; - $builder->deployBinary($soFile, $soDest, false); - } - - /** - * Get current extension version - * - * @return null|string Version string or null - */ - public function getExtVersion(): ?string - { - return null; - } - - public function setBuildStatic(): void - { - if (!in_array('static', Config::getExtTarget($this->name))) { - throw new WrongUsageException("Extension [{$this->name}] does not support static build!"); - } - $this->build_static = true; - } - - public function setBuildShared(): void - { - if (!in_array('shared', Config::getExtTarget($this->name))) { - throw new WrongUsageException("Extension [{$this->name}] does not support shared build!"); - } - $this->build_shared = true; - } - - public function isBuildShared(): bool - { - return $this->build_shared; - } - - public function isBuildStatic(): bool - { - return $this->build_static; - } - - /** - * Get the library dependencies that current extension depends on. - * - * @param bool $recursive Whether it includes dependencies recursively - */ - public function getLibraryDependencies(bool $recursive = false): array - { - $ret = array_filter($this->dependencies, fn ($x) => $x instanceof LibraryBase); - if (!$recursive) { - return $ret; - } - - $deps = []; - - $added = 1; - while ($added !== 0) { - $added = 0; - foreach ($ret as $depName => $dep) { - foreach ($dep->getDependencies(true) as $depdepName => $depdep) { - if (!array_key_exists($depdepName, $deps)) { - $deps[$depdepName] = $depdep; - ++$added; - } - } - if (!array_key_exists($depName, $deps)) { - $deps[$depName] = $dep; - } - } - } - - return $deps; - } - - /** - * Returns the environment variables a shared extension needs to be built. - * CFLAGS, CXXFLAGS, LDFLAGS and so on. - */ - protected function getSharedExtensionEnv(): array - { - $config = (new SPCConfigUtil($this->builder, ['no_php' => true]))->getExtensionConfig($this); - [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); - $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; - $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; - return [ - 'CFLAGS' => $config['cflags'], - 'CXXFLAGS' => $config['cflags'], - 'LDFLAGS' => $config['ldflags'], - 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), - 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), - 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, - ]; - } - - protected function addLibraryDependency(string $name, bool $optional = false): void - { - $depLib = $this->builder->getLib($name); - if (!$depLib) { - if (!$optional) { - throw new WrongUsageException("extension {$this->name} requires library {$name}"); - } - logger()->info("enabling {$this->name} without library {$name}"); - } else { - $this->dependencies[$name] = $depLib; - } - } - - protected function addExtensionDependency(string $name, bool $optional = false): void - { - $depExt = $this->builder->getExt($name); - if (!$depExt) { - if (!$optional) { - throw new WrongUsageException("{$this->name} requires extension {$name} which is not included"); - } - logger()->info("enabling {$this->name} without extension {$name}"); - } else { - $this->dependencies[$name] = $depExt; - } - } - - protected function getExtraEnv(): array - { - return []; - } - - /** - * Splits a given string of library flags into static and shared libraries. - * - * @param string $allLibs A space-separated string of library flags (e.g., -lxyz). - * @return array an array containing two elements: the first is a space-separated string - * of static library flags, and the second is a space-separated string - * of shared library flags - */ - protected function splitLibsIntoStaticAndShared(string $allLibs): array - { - $staticLibString = ''; - $sharedLibString = ''; - $libs = explode(' ', $allLibs); - foreach ($libs as $lib) { - $staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a'; - if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) { - $staticLib = $lib; - } - if ($lib === '-lphp' || !file_exists($staticLib)) { - $sharedLibString .= " {$lib}"; - } else { - $staticLibString .= " {$lib}"; - } - } - return [trim($staticLibString), trim($sharedLibString)]; - } -} diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php deleted file mode 100644 index 73b1f9ed9..000000000 --- a/src/SPC/builder/LibraryBase.php +++ /dev/null @@ -1,420 +0,0 @@ -source_dir = $source_dir ?? (SOURCE_PATH . DIRECTORY_SEPARATOR . Config::getLib(static::NAME, 'source')); - } - - /** - * Try to install or build this library. - * @param bool $force If true, force install or build - */ - public function setup(bool $force = false): int - { - $source = Config::getLib(static::NAME, 'source'); - // if source is locked as pre-built, we just tryInstall it - $pre_built_name = Downloader::getPreBuiltLockName($source); - if (($lock = LockFile::get($pre_built_name)) && $lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT) { - return $this->tryInstall($lock, $force); - } - return $this->tryBuild($force); - } - - /** - * Get library name. - */ - public function getName(): string - { - return static::NAME; - } - - /** - * Get current lib source root dir. - */ - public function getSourceDir(): string - { - return $this->source_dir; - } - - /** - * Get current lib dependencies. - * - * @return array - */ - public function getDependencies(bool $recursive = false): array - { - // 非递归情况下直接返回通过 addLibraryDependency 方法添加的依赖 - if (!$recursive) { - return $this->dependencies; - } - - $deps = []; - - $added = 1; - while ($added !== 0) { - $added = 0; - foreach ($this->dependencies as $depName => $dep) { - foreach ($dep->getDependencies(true) as $depdepName => $depdep) { - if (!in_array($depdepName, array_keys($deps), true)) { - $deps[$depdepName] = $depdep; - ++$added; - } - } - if (!in_array($depName, array_keys($deps), true)) { - $deps[$depName] = $dep; - } - } - } - - return $deps; - } - - /** - * Calculate dependencies for current library. - */ - public function calcDependency(): void - { - // Add dependencies from the configuration file. Here, choose different metadata based on the operating system. - /* - Rules: - If it is a Windows system, try the following dependencies in order: lib-depends-windows, lib-depends-win, lib-depends. - If it is a macOS system, try the following dependencies in order: lib-depends-macos, lib-depends-unix, lib-depends. - If it is a Linux system, try the following dependencies in order: lib-depends-linux, lib-depends-unix, lib-depends. - */ - foreach (Config::getLib(static::NAME, 'lib-depends', []) as $dep_name) { - $this->addLibraryDependency($dep_name); - } - foreach (Config::getLib(static::NAME, 'lib-suggests', []) as $dep_name) { - $this->addLibraryDependency($dep_name, true); - } - } - - /** - * Get config static libs. - */ - public function getStaticLibs(): array - { - return Config::getLib(static::NAME, 'static-libs', []); - } - - /** - * Get config headers. - */ - public function getHeaders(): array - { - return Config::getLib(static::NAME, 'headers', []); - } - - /** - * Get binary files. - */ - public function getBinaryFiles(): array - { - return Config::getLib(static::NAME, 'bin', []); - } - - public function tryInstall(array $lock, bool $force_install = false): int - { - $install_file = $lock['filename']; - if ($force_install) { - logger()->info('Installing required library [' . static::NAME . '] from pre-built binaries'); - - // Extract files - try { - FileSystem::extractPackage($install_file, $lock['source_type'], DOWNLOAD_PATH . '/' . $install_file, BUILD_ROOT_PATH); - $this->install(); - return LIB_STATUS_OK; - } catch (SPCException $e) { - logger()->error('Failed to extract pre-built library [' . static::NAME . ']: ' . $e->getMessage()); - return LIB_STATUS_INSTALL_FAILED; - } - } - if (!$this->isLibraryInstalled()) { - return $this->tryInstall($lock, true); - } - return LIB_STATUS_ALREADY; - } - - /** - * Try to build this library, before build, we check first. - * - * BUILD_STATUS_OK if build success - * BUILD_STATUS_ALREADY if already built - * BUILD_STATUS_FAILED if build failed - */ - public function tryBuild(bool $force_build = false): int - { - if (file_exists($this->source_dir . '/.spc.patched')) { - $this->patched = true; - } - // force means just build - if ($force_build) { - $type = Config::getLib(static::NAME, 'type', 'lib'); - logger()->info('Building required ' . $type . ' [' . static::NAME . ']'); - - // extract first if not exists - if (!is_dir($this->source_dir)) { - $this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-extract'); - SourceManager::initSource(libs: [static::NAME], source_only: true); - $this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-extract'); - } - - if (!$this->patched && $this->patchBeforeBuild()) { - file_put_contents($this->source_dir . '/.spc.patched', 'PATCHED!!!'); - } - $this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-build'); - $this->build(); - $this->installLicense(); - $this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-build'); - return LIB_STATUS_OK; - } - - if (!$this->isLibraryInstalled()) { - return $this->tryBuild(true); - } - // if all the files exist at this point, skip the compilation process - return LIB_STATUS_ALREADY; - } - - public function validate(): void - { - // do nothing, just throw wrong usage exception if not valid - } - - /** - * Get current lib version - * - * @return null|string Version string or null - */ - public function getLibVersion(): ?string - { - return null; - } - - /** - * Get current builder object. - */ - abstract public function getBuilder(): BuilderBase; - - public function beforePack(): void - { - // do something before pack, default do nothing. overwrite this method to do something (e.g. modify pkg-config file) - } - - /** - * Patch code before build - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeBuild(): bool - { - return false; - } - - /** - * Patch code before ./buildconf - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeBuildconf(): bool - { - return false; - } - - /** - * Patch code before ./configure - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeConfigure(): bool - { - return false; - } - - /** - * Patch code before windows configure.bat - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeWindowsConfigure(): bool - { - return false; - } - - /** - * Patch code before make - * If you need to patch some code, overwrite this - * return true if you patched something, false if not - */ - public function patchBeforeMake(): bool - { - return false; - } - - /** - * Patch php-config after embed was built - * Example: imap requires -lcrypt - */ - public function patchPhpConfig(): bool - { - return false; - } - - /** - * Build this library. - */ - abstract protected function build(); - - protected function install(): void - { - // replace placeholders if BUILD_ROOT_PATH/.spc-extract-placeholder.json exists - $replace_item_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; - if (!file_exists($replace_item_file)) { - return; - } - $replace_items = json_decode(file_get_contents($replace_item_file), true); - if (!is_array($replace_items)) { - throw new SPCInternalException("Invalid placeholder file: {$replace_item_file}"); - } - $placeholders = get_pack_replace(); - // replace placeholders in BUILD_ROOT_PATH - foreach ($replace_items as $item) { - $filepath = BUILD_ROOT_PATH . "/{$item}"; - FileSystem::replaceFileStr( - $filepath, - array_values($placeholders), - array_keys($placeholders), - ); - } - // remove placeholder file - unlink($replace_item_file); - } - - /** - * Add lib dependency - */ - protected function addLibraryDependency(string $name, bool $optional = false): void - { - $dep_lib = $this->getBuilder()->getLib($name); - if ($dep_lib) { - $this->dependencies[$name] = $dep_lib; - return; - } - if (!$optional) { - throw new WrongUsageException(static::NAME . " requires library {$name} but it is not included"); - } - logger()->debug('enabling ' . static::NAME . " without {$name}"); - } - - protected function getSnakeCaseName(): string - { - return str_replace('-', '_', static::NAME); - } - - /** - * Install license files in buildroot directory - */ - protected function installLicense(): void - { - $source = Config::getLib($this->getName(), 'source'); - FileSystem::createDir(BUILD_ROOT_PATH . "/source-licenses/{$source}"); - $license_files = Config::getSource($source)['license'] ?? []; - if (is_assoc_array($license_files)) { - $license_files = [$license_files]; - } - foreach ($license_files as $index => $license) { - if ($license['type'] === 'text') { - FileSystem::writeFile(BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt", $license['text']); - continue; - } - if ($license['type'] === 'file') { - copy($this->source_dir . '/' . $license['path'], BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt"); - } - } - } - - protected function isLibraryInstalled(): bool - { - if ($pkg_configs = Config::getLib(static::NAME, 'pkg-configs', [])) { - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_unique(array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path))); - - foreach ($pkg_configs as $name) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$name}.pc")) { - $found = true; - break; - } - } - if (!$found) { - return false; - } - } - // allow using system dependencies if pkg_config_path is explicitly defined - if (count($search_paths) > 1) { - return true; - } - } - foreach (Config::getLib(static::NAME, 'static-libs', []) as $name) { - if (!file_exists(BUILD_LIB_PATH . "/{$name}")) { - return false; - } - } - foreach (Config::getLib(static::NAME, 'headers', []) as $name) { - if (!file_exists(BUILD_INCLUDE_PATH . "/{$name}")) { - return false; - } - } - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)); - foreach (Config::getLib(static::NAME, 'pkg-configs', []) as $name) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$name}.pc")) { - $found = true; - break; - } - } - if (!$found) { - return false; - } - } - foreach (Config::getLib(static::NAME, 'bin', []) as $name) { - if (!file_exists(BUILD_BIN_PATH . "/{$name}")) { - return false; - } - } - return true; - } -} diff --git a/src/SPC/builder/LibraryInterface.php b/src/SPC/builder/LibraryInterface.php deleted file mode 100644 index 5368555e5..000000000 --- a/src/SPC/builder/LibraryInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -builder instanceof MacOSBuilder ? ' ' . $this->builder->getFrameworks(true) . ' ' : ''; - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/configure', '/-lbz2/', $this->getLibFilesString() . $frameworks); - return true; - } -} diff --git a/src/SPC/builder/extension/com_dotnet.php b/src/SPC/builder/extension/com_dotnet.php deleted file mode 100644 index 7a8f6a4e4..000000000 --- a/src/SPC/builder/extension/com_dotnet.php +++ /dev/null @@ -1,17 +0,0 @@ -info('patching before-configure for curl checks'); - $file1 = "AC_DEFUN([PHP_CHECK_LIBRARY], [\n $3\n])"; - $files = FileSystem::readFile($this->source_dir . '/config.m4'); - $file2 = 'AC_DEFUN([PHP_CHECK_LIBRARY], [ - save_old_LDFLAGS=$LDFLAGS - ac_stuff="$5" - - save_ext_shared=$ext_shared - ext_shared=yes - PHP_EVAL_LIBLINE([$]ac_stuff, LDFLAGS) - AC_CHECK_LIB([$1],[$2],[ - LDFLAGS=$save_old_LDFLAGS - ext_shared=$save_ext_shared - $3 - ],[ - LDFLAGS=$save_old_LDFLAGS - ext_shared=$save_ext_shared - unset ac_cv_lib_$1[]_$2 - $4 - ])dnl -])'; - file_put_contents($this->source_dir . '/config.m4', $file1 . "\n" . $files . "\n" . $file2); - return true; - } - - public function patchBeforeConfigure(): bool - { - $frameworks = $this->builder instanceof MacOSBuilder ? ' ' . $this->builder->getFrameworks(true) . ' ' : ''; - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/configure', '/-lcurl/', $this->getLibFilesString() . $frameworks); - $this->patchBeforeSharedConfigure(); - return true; - } - - public function patchBeforeMake(): bool - { - $patched = parent::patchBeforeMake(); - $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; - if ($this->builder instanceof WindowsBuilder && !str_contains($extra_libs, 'secur32.lib')) { - $extra_libs .= ' secur32.lib'; - putenv('SPC_EXTRA_LIBS=' . trim($extra_libs)); - return true; - } - return $patched; - } - - public function patchBeforeSharedConfigure(): bool - { - $file = $this->source_dir . '/config.m4'; - $content = FileSystem::readFile($file); - - // Inject patch before it - $patch = ' save_LIBS="$LIBS" - LIBS="$LIBS $CURL_LIBS" -'; - // Check if already patched - if (str_contains($content, $patch)) { - return false; // Already patched - } - - // Match the line containing PHP_CHECK_LIBRARY for curl - $pattern = '/(PHP_CHECK_LIBRARY\(\[curl],\s*\[curl_easy_perform],)/'; - - // Restore LIBS after the check — append this just after the macro block - $restore = ' - LIBS="$save_LIBS"'; - - // Apply patch - $patched = preg_replace_callback($pattern, function ($matches) use ($patch) { - return $patch . $matches[1]; - }, $content, 1); - - // Inject restore after the matching PHP_CHECK_LIBRARY block - $patched = preg_replace( - '/(PHP_CHECK_LIBRARY\(\[curl],\s*\[curl_easy_perform],.*?\)\n)/s', - "$1{$restore}\n", - $patched, - 1 - ); - - if ($patched === null) { - throw new PatchException('shared extension curl patcher', 'Failed to patch config.m4 due to a regex error'); - } - - FileSystem::writeFile($file, $patched); - return true; - } - - public function buildUnixShared(): void - { - if (!$this->builder instanceof LinuxBuilder) { - parent::buildUnixShared(); - return; - } - - FileSystem::replaceFileStr( - $this->source_dir . '/config.m4', - ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'], - ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'] - ); - try { - parent::buildUnixShared(); - } finally { - FileSystem::replaceFileStr( - $this->source_dir . '/config.m4', - ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'], - ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'] - ); - } - } -} diff --git a/src/SPC/builder/extension/dba.php b/src/SPC/builder/extension/dba.php deleted file mode 100644 index d1f8cda3a..000000000 --- a/src/SPC/builder/extension/dba.php +++ /dev/null @@ -1,24 +0,0 @@ -builder->getLib('qdbm') ? (' --with-qdbm=' . BUILD_ROOT_PATH) : ''; - return '--enable-dba' . ($shared ? '=shared' : '') . $qdbm; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - $qdbm = $this->builder->getLib('qdbm') ? ' --with-qdbm' : ''; - return '--with-dba' . $qdbm; - } -} diff --git a/src/SPC/builder/extension/dio.php b/src/SPC/builder/extension/dio.php deleted file mode 100644 index 55d08480d..000000000 --- a/src/SPC/builder/extension/dio.php +++ /dev/null @@ -1,22 +0,0 @@ -getLibFilesString() . '"'; - $arg .= ' GLIB_CFLAGS=-I"' . BUILD_INCLUDE_PATH . '"'; - $arg .= ' GLIB_LIBS="' . implode(' ', $glibs) . '"'; - return $arg; - } -} diff --git a/src/SPC/builder/extension/ev.php b/src/SPC/builder/extension/ev.php deleted file mode 100644 index 8e391244b..000000000 --- a/src/SPC/builder/extension/ev.php +++ /dev/null @@ -1,27 +0,0 @@ -source_dir . '/config.w32', - 'EXTENSION(\'ev\'', - " EXTENSION('ev', php_ev_sources, PHP_EV_SHARED, ' /DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');" - ); - return true; - } -} diff --git a/src/SPC/builder/extension/event.php b/src/SPC/builder/extension/event.php deleted file mode 100644 index 0a981f91f..000000000 --- a/src/SPC/builder/extension/event.php +++ /dev/null @@ -1,45 +0,0 @@ -builder->getLib('openssl')) { - $arg .= ' --with-event-openssl=' . BUILD_ROOT_PATH; - } - if ($this->builder->getExt('sockets')) { - $arg .= ' --enable-event-sockets'; - } else { - $arg .= ' --disable-event-sockets'; - } - return $arg; - } - - public function patchBeforeConfigure(): bool - { - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/configure', '/-levent_openssl/', $this->getLibFilesString()); - return true; - } - - public function patchBeforeMake(): bool - { - $patched = parent::patchBeforeMake(); - // Prevent event extension compile error on macOS - if ($this->builder instanceof MacOSBuilder) { - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_OPENPTY 1$/m', ''); - return true; - } - return $patched; - } -} diff --git a/src/SPC/builder/extension/ffi.php b/src/SPC/builder/extension/ffi.php deleted file mode 100644 index 8e192beab..000000000 --- a/src/SPC/builder/extension/ffi.php +++ /dev/null @@ -1,32 +0,0 @@ -builder->getLib('freetype') ? ' --with-freetype' : ''; - $arg .= $this->builder->getLib('libjpeg') ? ' --with-jpeg' : ''; - $arg .= $this->builder->getLib('libwebp') ? ' --with-webp' : ''; - $arg .= $this->builder->getLib('libavif') ? ' --with-avif' : ''; - return $arg; - } -} diff --git a/src/SPC/builder/extension/gettext.php b/src/SPC/builder/extension/gettext.php deleted file mode 100644 index 303cc3892..000000000 --- a/src/SPC/builder/extension/gettext.php +++ /dev/null @@ -1,35 +0,0 @@ -builder instanceof MacOSBuilder) { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/gettext/config.m4', - ['AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB([$GETTEXT_CHECK_IN_LIB'], - ['AC_CHECK_LIB(intl', 'AC_CHECK_LIB([intl'] // new php versions use a bracket - ); - } - return true; - } - - public function patchBeforeConfigure(): bool - { - if ($this->builder instanceof MacOSBuilder) { - $frameworks = ' ' . $this->builder->getFrameworks(true) . ' '; - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/configure', '-lintl', $this->getLibFilesString() . $frameworks); - } - return true; - } -} diff --git a/src/SPC/builder/extension/glfw.php b/src/SPC/builder/extension/glfw.php deleted file mode 100644 index 7cfdaaaf6..000000000 --- a/src/SPC/builder/extension/glfw.php +++ /dev/null @@ -1,38 +0,0 @@ -builder instanceof WindowsBuilder) { - throw new ValidationException('grpc extension does not support windows yet'); - } - - // Fix deprecated PHP API usage in call.c - FileSystem::replaceFileStr( - "{$this->source_dir}/src/php/ext/grpc/call.c", - 'zend_exception_get_default(TSRMLS_C),', - 'zend_ce_exception,', - ); - - // Fix include path conflict with pdo_sqlsrv: grpc's PHP ext dir is added to the global include path via - $grpc_php_dir = "{$this->source_dir}/src/php/ext/grpc"; - if (file_exists("{$grpc_php_dir}/version.h")) { - copy("{$grpc_php_dir}/version.h", "{$grpc_php_dir}/php_grpc_version.h"); - unlink("{$grpc_php_dir}/version.h"); - FileSystem::replaceFileStr("{$grpc_php_dir}/php_grpc.h", '#include "version.h"', '#include "php_grpc_version.h"'); - FileSystem::replaceFileStr("{$grpc_php_dir}/php_grpc.c", '#include "version.h"', '#include "php_grpc_version.h"'); - } - - $config_m4 = <<<'M4' -PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])]) - -if test "$PHP_GRPC" != "no"; then - PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/include) - PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/src/php/ext/grpc) - GRPC_LIBDIR=@@build_lib_path@@ - PHP_ADD_LIBPATH($GRPC_LIBDIR) - PHP_ADD_LIBRARY(grpc,,GRPC_SHARED_LIBADD) - LIBS="-lpthread $LIBS" - PHP_ADD_LIBRARY(pthread) - - case $host in - *darwin*) - PHP_ADD_LIBRARY(c++,1,GRPC_SHARED_LIBADD) - ;; - *) - PHP_ADD_LIBRARY(stdc++,1,GRPC_SHARED_LIBADD) - PHP_ADD_LIBRARY(rt,,GRPC_SHARED_LIBADD) - PHP_ADD_LIBRARY(rt) - ;; - esac - - PHP_NEW_EXTENSION(grpc, @grpc_c_files@, $ext_shared, , -DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1) - PHP_SUBST(GRPC_SHARED_LIBADD) - PHP_INSTALL_HEADERS([ext/grpc], [php_grpc.h]) -fi -M4; - $replace = get_pack_replace(); - // load grpc c files from src/php/ext/grpc - $c_files = glob($this->source_dir . '/src/php/ext/grpc/*.c'); - $replace['@grpc_c_files@'] = implode(" \\\n ", array_map(fn ($f) => 'src/php/ext/grpc/' . basename($f), $c_files)); - $config_m4 = str_replace(array_keys($replace), array_values($replace), $config_m4); - file_put_contents($this->source_dir . '/config.m4', $config_m4); - - copy($this->source_dir . '/src/php/ext/grpc/php_grpc.h', $this->source_dir . '/php_grpc.h'); - return true; - } - - public function patchBeforeConfigure(): bool - { - $util = new SPCConfigUtil($this->builder, ['libs_only_deps' => true]); - $config = $util->getExtensionConfig($this); - $libs = $config['libs']; - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/configure', '-lgrpc', $libs); - return true; - } - - public function patchBeforeMake(): bool - { - parent::patchBeforeMake(); - GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes'); - return true; - } - - protected function getSharedExtensionEnv(): array - { - $env = parent::getSharedExtensionEnv(); - $env['CPPFLAGS'] = $env['CXXFLAGS'] . ' -Wno-attributes'; - return $env; - } -} diff --git a/src/SPC/builder/extension/iconv.php b/src/SPC/builder/extension/iconv.php deleted file mode 100644 index e3178144b..000000000 --- a/src/SPC/builder/extension/iconv.php +++ /dev/null @@ -1,27 +0,0 @@ -builder instanceof MacOSBuilder) { - return false; - } - $extra_libs = $this->builder->getOption('extra-libs', ''); - if (!str_contains($extra_libs, '-liconv')) { - $extra_libs .= ' -liconv'; - } - $this->builder->setOption('extra-libs', $extra_libs); - return true; - } -} diff --git a/src/SPC/builder/extension/imagick.php b/src/SPC/builder/extension/imagick.php deleted file mode 100644 index a548a8a3d..000000000 --- a/src/SPC/builder/extension/imagick.php +++ /dev/null @@ -1,32 +0,0 @@ -builder->getLib('openssl')) { - // sometimes imap with openssl does not contain zlib (required by openssl) - // we need to add it manually - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/imap/config.m4', 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD"', 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD -lz"'); - return true; - } - return false; - } - - public function validate(): void - { - if ($this->builder->getOption('enable-zts')) { - throw new WrongUsageException('ext-imap is not thread safe, do not build it with ZTS builds'); - } - } - - public function getUnixConfigureArg(bool $shared = false): string - { - $arg = '--with-imap=' . BUILD_ROOT_PATH; - if ($this->builder->getLib('openssl') !== null) { - $arg .= ' --with-imap-ssl=' . BUILD_ROOT_PATH; - } - return $arg; - } - - public function patchBeforeMake(): bool - { - $patched = parent::patchBeforeMake(); - if (PHP_OS_FAMILY !== 'Linux' || SystemUtil::isMuslDist()) { - return $patched; - } - $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' -lcrypt'); - f_putenv('SPC_EXTRA_LIBS=' . $extra_libs); - return true; - } -} diff --git a/src/SPC/builder/extension/intl.php b/src/SPC/builder/extension/intl.php deleted file mode 100644 index 0c1e323dd..000000000 --- a/src/SPC/builder/extension/intl.php +++ /dev/null @@ -1,32 +0,0 @@ -builder instanceof WindowsBuilder) { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/intl/config.w32', - 'EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", true,', - 'EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", PHP_INTL_SHARED,' - ); - return true; - } - return false; - } - - public function patchBeforeSharedPhpize(): bool - { - return $this->patchBeforeBuildconf(); - } -} diff --git a/src/SPC/builder/extension/ldap.php b/src/SPC/builder/extension/ldap.php deleted file mode 100644 index 4616bea85..000000000 --- a/src/SPC/builder/extension/ldap.php +++ /dev/null @@ -1,23 +0,0 @@ -execWithResult('$PKG_CONFIG --libs-only-l --static ldap'); - if (!empty($output[1][0])) { - $libs = $output[1][0]; - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/configure', '-lldap ', $libs . ' '); - } - return true; - } -} diff --git a/src/SPC/builder/extension/lz4.php b/src/SPC/builder/extension/lz4.php deleted file mode 100644 index 5727a97d5..000000000 --- a/src/SPC/builder/extension/lz4.php +++ /dev/null @@ -1,22 +0,0 @@ -source_dir; - FileSystem::copyDir($original . '/ext', SOURCE_PATH . '/php-src/ext/maxminddb'); - $this->source_dir = SOURCE_PATH . '/php-src/ext/maxminddb'; - return true; - } - $this->source_dir = SOURCE_PATH . '/php-src/ext/maxminddb'; - return false; - } -} diff --git a/src/SPC/builder/extension/mbregex.php b/src/SPC/builder/extension/mbregex.php deleted file mode 100644 index 1132eeb23..000000000 --- a/src/SPC/builder/extension/mbregex.php +++ /dev/null @@ -1,42 +0,0 @@ -builder->getExt('mbstring')->isBuildShared() ? '-d "extension_dir=' . BUILD_MODULES_PATH . '" -d "extension=mbstring"' : ''; - [$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $sharedext . ' --ri "mbstring" | grep regex', false); - if ($ret !== 0) { - throw new ValidationException("Extension {$this->getName()} failed compile check: compiled php-cli mbstring extension does not contain regex !"); - } - } - - public function runCliCheckWindows(): void - { - [$ret, $out] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "mbstring"', false); - if ($ret !== 0) { - throw new ValidationException("extension {$this->getName()} failed compile check: compiled php-cli does not contain mbstring !"); - } - $out = implode("\n", $out); - if (!str_contains($out, 'regex')) { - throw new ValidationException("extension {$this->getName()} failed compile check: compiled php-cli mbstring extension does not contain regex !"); - } - } -} diff --git a/src/SPC/builder/extension/mbstring.php b/src/SPC/builder/extension/mbstring.php deleted file mode 100644 index 3576877f7..000000000 --- a/src/SPC/builder/extension/mbstring.php +++ /dev/null @@ -1,34 +0,0 @@ -builder->getExt('mbregex') === null) { - $arg .= ' --disable-mbregex'; - } else { - $arg .= ' --enable-mbregex'; - } - return $arg; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - $arg = '--enable-mbstring' . ($shared ? '=shared' : ''); - if ($this->builder->getExt('mbregex') === null) { - $arg .= ' --disable-mbregex'; - } else { - $arg .= ' --enable-mbregex'; - } - return $arg; - } -} diff --git a/src/SPC/builder/extension/memcache.php b/src/SPC/builder/extension/memcache.php deleted file mode 100644 index 32cb301c6..000000000 --- a/src/SPC/builder/extension/memcache.php +++ /dev/null @@ -1,72 +0,0 @@ -isBuildStatic()) { - return false; - } - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/memcache/config9.m4', - 'if test -d $abs_srcdir/src ; then', - 'if test -d $abs_srcdir/main ; then' - ); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/memcache/config9.m4', - 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', - 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"' - ); - // add for in-tree building - file_put_contents( - SOURCE_PATH . '/php-src/ext/memcache/php_memcache.h', - <<<'EOF' -#ifndef PHP_MEMCACHE_H -#define PHP_MEMCACHE_H - -extern zend_module_entry memcache_module_entry; -#define phpext_memcache_ptr &memcache_module_entry - -#endif -EOF - ); - return true; - } - - public function patchBeforeSharedConfigure(): bool - { - if (!$this->isBuildShared()) { - return false; - } - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/memcache/config9.m4', - 'if test -d $abs_srcdir/main ; then', - 'if test -d $abs_srcdir/src ; then', - ); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/memcache/config9.m4', - 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"', - 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', - ); - return true; - } - - protected function getExtraEnv(): array - { - return ['CFLAGS' => '-std=c17']; - } -} diff --git a/src/SPC/builder/extension/memcached.php b/src/SPC/builder/extension/memcached.php deleted file mode 100644 index 983b5b64d..000000000 --- a/src/SPC/builder/extension/memcached.php +++ /dev/null @@ -1,26 +0,0 @@ -builder->getLib('zstd') ? '--with-zstd ' : '') . - ($this->builder->getExt('igbinary') ? '--enable-memcached-igbinary ' : '') . - ($this->builder->getExt('session') ? '--enable-memcached-session ' : '') . - ($this->builder->getExt('msgpack') ? '--enable-memcached-msgpack ' : '') . - '--with-system-fastlz'; - } -} diff --git a/src/SPC/builder/extension/mongodb.php b/src/SPC/builder/extension/mongodb.php deleted file mode 100644 index 08861e4ef..000000000 --- a/src/SPC/builder/extension/mongodb.php +++ /dev/null @@ -1,32 +0,0 @@ -builder->getLib('openssl')) { - $arg .= '--with-mongodb-ssl=openssl'; - } - $arg .= $this->builder->getLib('icu') ? ' --with-mongodb-icu=yes ' : ' --with-mongodb-icu=no '; - $arg .= $this->builder->getLib('zstd') ? ' --with-mongodb-zstd=yes ' : ' --with-mongodb-zstd=no '; - // $arg .= $this->builder->getLib('snappy') ? ' --with-mongodb-snappy=yes ' : ' --with-mongodb-snappy=no '; - $arg .= $this->builder->getLib('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; - return clean_spaces($arg); - } - - public function getExtraEnv(): array - { - return ['CFLAGS' => '-std=c17']; - } -} diff --git a/src/SPC/builder/extension/mysqlnd_ed25519.php b/src/SPC/builder/extension/mysqlnd_ed25519.php deleted file mode 100644 index 7b2b4abcd..000000000 --- a/src/SPC/builder/extension/mysqlnd_ed25519.php +++ /dev/null @@ -1,22 +0,0 @@ -getConfigureArg(); - } -} diff --git a/src/SPC/builder/extension/mysqlnd_parsec.php b/src/SPC/builder/extension/mysqlnd_parsec.php deleted file mode 100644 index d044b1c52..000000000 --- a/src/SPC/builder/extension/mysqlnd_parsec.php +++ /dev/null @@ -1,22 +0,0 @@ -getConfigureArg(); - } -} diff --git a/src/SPC/builder/extension/odbc.php b/src/SPC/builder/extension/odbc.php deleted file mode 100644 index 278b5865a..000000000 --- a/src/SPC/builder/extension/odbc.php +++ /dev/null @@ -1,17 +0,0 @@ -builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') { - throw new WrongUsageException('Statically compiled PHP with Zend Opcache only available for PHP >= 8.0 !'); - } - } - - public function patchBeforeBuildconf(): bool - { - $version = $this->builder->getPHPVersion(); - if (file_exists(SOURCE_PATH . '/php-src/.opcache_patched')) { - return false; - } - // if 8.2.0 <= PHP_VERSION < 8.2.23, we need to patch from legacy patch file - if (version_compare($version, '8.2.0', '>=') && version_compare($version, '8.2.23', '<')) { - SourcePatcher::patchFile('spc_fix_static_opcache_before_80222.patch', SOURCE_PATH . '/php-src'); - } - // if 8.3.0 <= PHP_VERSION < 8.3.11, we need to patch from legacy patch file - elseif (version_compare($version, '8.3.0', '>=') && version_compare($version, '8.3.11', '<')) { - SourcePatcher::patchFile('spc_fix_static_opcache_before_80310.patch', SOURCE_PATH . '/php-src'); - } - // if 8.3.12 <= PHP_VERSION < 8.5.0-dev, we need to patch from legacy patch file - elseif (version_compare($version, '8.5.0-dev', '<')) { - SourcePatcher::patchMicro(items: ['static_opcache']); - } - // PHP 8.5.0-dev and later supports static opcache without patching - else { - return false; - } - return file_put_contents(SOURCE_PATH . '/php-src/.opcache_patched', '1') !== false; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - $phpVersionID = $this->builder->getPHPVersionID(); - $opcache_jit = ' --enable-opcache-jit'; - if ((SPCTarget::getTargetOS() === 'Linux' && - SPCTarget::getLibc() === 'musl' && - $this->builder->getOption('enable-zts') && - arch2gnu(php_uname('m')) === 'x86_64' && - $phpVersionID < 80500) || - $this->builder->getOption('disable-opcache-jit') - ) { - $opcache_jit = ' --disable-opcache-jit'; - } - return '--enable-opcache' . ($shared ? '=shared' : '') . $opcache_jit; - } - - public function getDistName(): string - { - return 'Zend Opcache'; - } -} diff --git a/src/SPC/builder/extension/openssl.php b/src/SPC/builder/extension/openssl.php deleted file mode 100644 index bf61fa371..000000000 --- a/src/SPC/builder/extension/openssl.php +++ /dev/null @@ -1,45 +0,0 @@ -builder->getPHPVersionID() < 80100) { - $openssl_c = file_get_contents(SOURCE_PATH . '/php-src/ext/openssl/openssl.c'); - $openssl_c = preg_replace('/REGISTER_LONG_CONSTANT\s*\(\s*"OPENSSL_SSLV23_PADDING"\s*.+;/', '', $openssl_c); - file_put_contents(SOURCE_PATH . '/php-src/ext/openssl/openssl.c', $openssl_c); - return true; - } - - return $patched; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - $openssl_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : ' --with-openssl-dir=' . BUILD_ROOT_PATH; - $args = '--with-openssl=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH . $openssl_dir; - if ($this->builder->getPHPVersionID() >= 80500 || ($this->builder->getPHPVersionID() >= 80400 && !$this->builder->getOption('enable-zts'))) { - $args .= ' --with-openssl-argon2 OPENSSL_LIBS="-lz"'; - } - return $args; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - $args = '--with-openssl'; - if ($this->builder->getPHPVersionID() >= 80500 || ($this->builder->getPHPVersionID() >= 80400 && !$this->builder->getOption('enable-zts'))) { - $args .= ' --with-openssl-argon2'; - } - return $args; - } -} diff --git a/src/SPC/builder/extension/opentelemetry.php b/src/SPC/builder/extension/opentelemetry.php deleted file mode 100644 index 924c9ea26..000000000 --- a/src/SPC/builder/extension/opentelemetry.php +++ /dev/null @@ -1,43 +0,0 @@ -builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') { - throw new ValidationException('The opentelemetry extension requires PHP 8.0 or later'); - } - } - - public function patchBeforeBuildconf(): bool - { - if (PHP_OS_FAMILY === 'Windows') { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/opentelemetry/config.w32', - "EXTENSION('opentelemetry', 'opentelemetry.c otel_observer.c', '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');", - "EXTENSION('opentelemetry', 'opentelemetry.c otel_observer.c', PHP_OPENTELEMETRY_SHARED, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');" - ); - return true; - } - return false; - } - - public function patchBeforeMake(): bool - { - parent::patchBeforeMake(); - // add -Wno-strict-prototypes - GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes'); - return true; - } -} diff --git a/src/SPC/builder/extension/parallel.php b/src/SPC/builder/extension/parallel.php deleted file mode 100644 index de4e5d88c..000000000 --- a/src/SPC/builder/extension/parallel.php +++ /dev/null @@ -1,27 +0,0 @@ -builder->getOption('enable-zts')) { - throw new WrongUsageException('ext-parallel must be built with ZTS builds. Use "--enable-zts" option!'); - } - } - - public function patchBeforeBuildconf(): bool - { - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/ext/parallel/config.m4', '/PHP_VERSION=.*/m', ''); - return true; - } -} diff --git a/src/SPC/builder/extension/password_argon2.php b/src/SPC/builder/extension/password_argon2.php deleted file mode 100644 index d42fe4e37..000000000 --- a/src/SPC/builder/extension/password_argon2.php +++ /dev/null @@ -1,36 +0,0 @@ -execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "assert(defined(\'PASSWORD_ARGON2I\'));"'); - if ($ret !== 0) { - throw new ValidationException('extension ' . $this->getName() . ' failed sanity check', validation_module: 'password_argon2 function check'); - } - } - - public function getConfigureArg(bool $shared = false): string - { - if ($this->builder->getLib('openssl') !== null) { - if ($this->builder->getPHPVersionID() >= 80500 || ($this->builder->getPHPVersionID() >= 80400 && !$this->builder->getOption('enable-zts'))) { - return '--without-password-argon2'; // use --with-openssl-argon2 in openssl extension instead - } - } - return '--with-password-argon2'; - } -} diff --git a/src/SPC/builder/extension/pdo_odbc.php b/src/SPC/builder/extension/pdo_odbc.php deleted file mode 100644 index c47144fe5..000000000 --- a/src/SPC/builder/extension/pdo_odbc.php +++ /dev/null @@ -1,29 +0,0 @@ -getLibFilesString() - ); - return true; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - if ($this->builder->getPHPVersionID() >= 80400) { - $libfiles = $this->getLibFilesString(); - $libfiles = str_replace(BUILD_LIB_PATH . '/lib', '-l', $libfiles); - $libfiles = str_replace('.a', '', $libfiles); - return '--with-pgsql' . ($shared ? '=shared' : '') . - ' PGSQL_CFLAGS=-I' . BUILD_INCLUDE_PATH . - ' PGSQL_LIBS="-L' . BUILD_LIB_PATH . ' ' . $libfiles . '"'; - } - return '--with-pgsql=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - if ($this->builder->getPHPVersionID() >= 80400) { - return '--with-pgsql'; - } - return '--with-pgsql=' . BUILD_ROOT_PATH; - } - - protected function getExtraEnv(): array - { - return [ - 'CFLAGS' => '-std=c17 -Wno-int-conversion', - ]; - } -} diff --git a/src/SPC/builder/extension/phar.php b/src/SPC/builder/extension/phar.php deleted file mode 100644 index 7396ba837..000000000 --- a/src/SPC/builder/extension/phar.php +++ /dev/null @@ -1,37 +0,0 @@ -builder instanceof LinuxBuilder) { - parent::buildUnixShared(); - return; - } - - FileSystem::replaceFileStr( - $this->source_dir . '/config.m4', - ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'], - ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'] - ); - try { - parent::buildUnixShared(); - } finally { - FileSystem::replaceFileStr( - $this->source_dir . '/config.m4', - ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'], - ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'] - ); - } - } -} diff --git a/src/SPC/builder/extension/protobuf.php b/src/SPC/builder/extension/protobuf.php deleted file mode 100644 index fd84dfec7..000000000 --- a/src/SPC/builder/extension/protobuf.php +++ /dev/null @@ -1,25 +0,0 @@ -builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') { - throw new ValidationException('The latest protobuf extension requires PHP 8.0 or later'); - } - $grpc = $this->builder->getExt('grpc'); - // protobuf conflicts with grpc - if ($grpc?->isBuildStatic()) { - throw new ValidationException('protobuf conflicts with grpc, please remove grpc or protobuf extension'); - } - } -} diff --git a/src/SPC/builder/extension/rar.php b/src/SPC/builder/extension/rar.php deleted file mode 100644 index 6db8c03c2..000000000 --- a/src/SPC/builder/extension/rar.php +++ /dev/null @@ -1,24 +0,0 @@ -= 15.0) - if ($this->builder instanceof MacOSBuilder) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/rar/config.m4', '-Wall -fvisibility=hidden', '-Wall -Wno-incompatible-function-pointer-types -fvisibility=hidden'); - return true; - } - return false; - } -} diff --git a/src/SPC/builder/extension/rdkafka.php b/src/SPC/builder/extension/rdkafka.php deleted file mode 100644 index 58af50ffe..000000000 --- a/src/SPC/builder/extension/rdkafka.php +++ /dev/null @@ -1,45 +0,0 @@ -source_dir}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\n", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm \$RDKAFKA_LIBS\n"); - FileSystem::replaceFileStr("{$this->source_dir}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\"\n", '-L$RDKAFKA_DIR/$PHP_LIBDIR -lm $RDKAFKA_LIBS"'); - FileSystem::replaceFileStr("{$this->source_dir}/config.m4", 'PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,', 'AC_CHECK_LIB([$LIBNAME], [$LIBSYMBOL],'); - return true; - } - - public function patchBeforeMake(): bool - { - parent::patchBeforeMake(); - // when compiling rdkafka with inline builds, it shows some errors, I don't know why. - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/rdkafka/rdkafka.c', - "#ifdef HAS_RD_KAFKA_TRANSACTIONS\n#include \"kafka_error_exception.h\"\n#endif", - '#include "kafka_error_exception.h"' - ); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/rdkafka/kafka_error_exception.h', - ['#ifdef HAS_RD_KAFKA_TRANSACTIONS', '#endif'], - '' - ); - return true; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - $pkgconf_libs = (new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]))->getExtensionConfig($this); - return '--with-rdkafka=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; - } -} diff --git a/src/SPC/builder/extension/readline.php b/src/SPC/builder/extension/readline.php deleted file mode 100644 index 035d7b438..000000000 --- a/src/SPC/builder/extension/readline.php +++ /dev/null @@ -1,47 +0,0 @@ -getLibFilesString() - ); - return true; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - return '--with-libedit --without-readline'; - } - - public function buildUnixShared(): void - { - if (!file_exists(BUILD_BIN_PATH . '/php') || !file_exists(BUILD_INCLUDE_PATH . '/php/sapi/cli/cli.h')) { - logger()->warning('CLI mode is not enabled, skipping readline build'); - return; - } - parent::buildUnixShared(); - } - - public function runCliCheckUnix(): void - { - parent::runCliCheckUnix(); - [$ret, $out] = shell()->execWithResult('printf "exit\n" | ' . BUILD_BIN_PATH . '/php -a'); - if ($ret !== 0 || !str_contains(implode("\n", $out), 'Interactive shell')) { - throw new ValidationException("readline extension failed sanity check. Code: {$ret}, output: " . implode("\n", $out)); - } - } -} diff --git a/src/SPC/builder/extension/redis.php b/src/SPC/builder/extension/redis.php deleted file mode 100644 index 4dd59565b..000000000 --- a/src/SPC/builder/extension/redis.php +++ /dev/null @@ -1,41 +0,0 @@ -isBuildStatic()) { - $arg .= $this->builder->getExt('session')?->isBuildStatic() ? ' --enable-redis-session' : ' --disable-redis-session'; - $arg .= $this->builder->getExt('igbinary')?->isBuildStatic() ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; - $arg .= $this->builder->getExt('msgpack')?->isBuildStatic() ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; - } else { - $arg .= $this->builder->getExt('session') ? ' --enable-redis-session' : ' --disable-redis-session'; - $arg .= $this->builder->getExt('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; - $arg .= $this->builder->getExt('msgpack') ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; - } - if ($this->builder->getLib('zstd')) { - $arg .= ' --enable-redis-zstd --with-libzstd="' . BUILD_ROOT_PATH . '"'; - } - if ($this->builder->getLib('liblz4')) { - $arg .= ' --enable-redis-lz4 --with-liblz4="' . BUILD_ROOT_PATH . '"'; - } - return $arg; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - $arg = '--enable-redis'; - $arg .= $this->builder->getExt('session') ? ' --enable-redis-session' : ' --disable-redis-session'; - $arg .= $this->builder->getExt('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; - return $arg; - } -} diff --git a/src/SPC/builder/extension/simdjson.php b/src/SPC/builder/extension/simdjson.php deleted file mode 100644 index 914fd674f..000000000 --- a/src/SPC/builder/extension/simdjson.php +++ /dev/null @@ -1,50 +0,0 @@ -builder->getPHPVersionID(); - FileSystem::replaceFileRegex( - SOURCE_PATH . '/php-src/ext/simdjson/config.m4', - '/php_version=(`.*`)$/m', - 'php_version=' . $php_ver - ); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/simdjson/config.m4', - 'if test -z "$PHP_CONFIG"; then', - 'if false; then' - ); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/simdjson/config.w32', - "'yes',", - 'PHP_SIMDJSON_SHARED,' - ); - return true; - } - - public function getSharedExtensionEnv(): array - { - $env = parent::getSharedExtensionEnv(); - if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { - $extra = getenv('SPC_COMPILER_EXTRA'); - if (!str_contains((string) $extra, '-lstdc++')) { - f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++')); - } - $env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; - $env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; - } - return $env; - } -} diff --git a/src/SPC/builder/extension/snappy.php b/src/SPC/builder/extension/snappy.php deleted file mode 100644 index 13644edf1..000000000 --- a/src/SPC/builder/extension/snappy.php +++ /dev/null @@ -1,29 +0,0 @@ -getLibFilesString() . ($this->builder instanceof MacOSBuilder ? ' -lc++' : ' -lstdc++') - ); - return true; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - return '--enable-snappy --with-snappy-includedir="' . BUILD_ROOT_PATH . '"'; - } -} diff --git a/src/SPC/builder/extension/snmp.php b/src/SPC/builder/extension/snmp.php deleted file mode 100644 index 488fc81ec..000000000 --- a/src/SPC/builder/extension/snmp.php +++ /dev/null @@ -1,29 +0,0 @@ -builder->getPHPVersionID() < 80400) { - FileSystem::copy(ROOT_DIR . '/src/globals/extra/snmp-ext-config-old.m4', "{$this->source_dir}/config.m4"); - } - $libs = implode(' ', PkgConfigUtil::getLibsArray('netsnmp')); - FileSystem::replaceFileStr( - "{$this->source_dir}/config.m4", - 'PHP_EVAL_LIBLINE([$SNMP_LIBS], [SNMP_SHARED_LIBADD])', - "SNMP_LIBS=\"{$libs}\"\nPHP_EVAL_LIBLINE([\$SNMP_LIBS], [SNMP_SHARED_LIBADD])" - ); - return true; - } -} diff --git a/src/SPC/builder/extension/spx.php b/src/SPC/builder/extension/spx.php deleted file mode 100644 index e77232934..000000000 --- a/src/SPC/builder/extension/spx.php +++ /dev/null @@ -1,55 +0,0 @@ -builder->getLib('zlib') !== null) { - $arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH; - } - return $arg; - } - - public function patchBeforeConfigure(): bool - { - FileSystem::replaceFileStr( - $this->source_dir . '/Makefile.frag', - '@cp -r assets/web-ui/*', - '@cp -r ' . $this->source_dir . '/assets/web-ui/*', - ); - return true; - } - - public function patchBeforeBuildconf(): bool - { - FileSystem::replaceFileStr( - $this->source_dir . '/config.m4', - 'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"', - 'CFLAGS="$CFLAGS -pthread"' - ); - FileSystem::replaceFileStr( - $this->source_dir . '/src/php_spx.h', - "extern zend_module_entry spx_module_entry;\n", - "extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n" - ); - FileSystem::copy($this->source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h'); - return true; - } - - public function getSharedExtensionEnv(): array - { - $env = parent::getSharedExtensionEnv(); - $env['SPX_SHARED_LIBADD'] = $env['LIBS']; - return $env; - } -} diff --git a/src/SPC/builder/extension/sqlsrv.php b/src/SPC/builder/extension/sqlsrv.php deleted file mode 100644 index fa55b9324..000000000 --- a/src/SPC/builder/extension/sqlsrv.php +++ /dev/null @@ -1,46 +0,0 @@ -builder->getExt('pdo_sqlsrv') === null) { - // support sqlsrv without pdo_sqlsrv - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/sqlsrv/config.w32', 'PHP_PDO_SQLSRV', '"no"'); - $this->pdo_sqlsrv_patched = true; - return true; - } - return false; - } - - public function patchBeforeWindowsConfigure(): bool - { - if ($this->pdo_sqlsrv_patched) { - // revert pdo_sqlsrv patch - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/sqlsrv/config.w32', '"no" == "no"', 'PHP_PDO_SQLSRV == "no"'); - return true; - } - return false; - } - - public function patchBeforeMake(): bool - { - $makefile = SOURCE_PATH . '/php-src/Makefile'; - $makeContent = file_get_contents($makefile); - $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/W4\b/m', '$1', $makeContent); - $makeContent = preg_replace('/^(CFLAGS_(?:PDO_)?SQLSRV=.*?)\s+\/WX\b/m', '$1', $makeContent); - file_put_contents($makefile, $makeContent); - return true; - } -} diff --git a/src/SPC/builder/extension/ssh2.php b/src/SPC/builder/extension/ssh2.php deleted file mode 100644 index aa6a439d0..000000000 --- a/src/SPC/builder/extension/ssh2.php +++ /dev/null @@ -1,23 +0,0 @@ -getLibFilesString() - ); - return true; - } -} diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php deleted file mode 100644 index 7da30ba69..000000000 --- a/src/SPC/builder/extension/swoole.php +++ /dev/null @@ -1,86 +0,0 @@ -source_dir . '/ext-src/php_swoole_private.h', 'PHP_VERSION_ID > 80500', 'PHP_VERSION_ID >= 80600'); - if ($this->builder instanceof MacOSBuilder) { - // Fix swoole with event extension conflict bug - $util_path = shell()->execWithResult('xcrun --show-sdk-path', false)[1][0] . '/usr/include/util.h'; - FileSystem::replaceFileStr( - "{$this->source_dir}/thirdparty/php/standard/proc_open.cc", - 'include ', - 'include "' . $util_path . '"', - ); - return true; - } - return $patched; - } - - public function getExtVersion(): ?string - { - // Get version from source directory - $file = SOURCE_PATH . '/php-src/ext/swoole/include/swoole_version.h'; - // Match #define SWOOLE_VERSION "5.1.3" - $pattern = '/#define SWOOLE_VERSION "(.+)"/'; - if (preg_match($pattern, file_get_contents($file), $matches)) { - return $matches[1]; - } - return null; - } - - public function getUnixConfigureArg(bool $shared = false): string - { - // enable swoole - $arg = '--enable-swoole' . ($shared ? '=shared' : ''); - - // commonly used feature: coroutine-time - $arg .= ' --enable-swoole-coro-time --with-pic'; - $arg .= ' --enable-swoole-ssh --enable-swoole-curl'; - - $arg .= $this->builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; - - // additional features that only require libraries - $arg .= $this->builder->getLib('libcares') ? ' --enable-cares' : ''; - $arg .= $this->builder->getLib('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; - $arg .= $this->builder->getLib('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : ''; - $arg .= $this->builder->getLib('zstd') ? ' --enable-zstd' : ''; - $arg .= $this->builder->getLib('liburing') && getenv('SPC_LIBC') !== 'glibc' ? ' --enable-iouring --enable-uring-socket' : '--disable-iouring'; - $arg .= $this->builder->getExt('sockets') ? ' --enable-sockets' : ''; - - // enable additional features that require the pdo extension, but conflict with pdo_* extensions - // to make sure everything works as it should, this is done in fake addon extensions - $arg .= $this->builder->getExt('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql'; - $arg .= $this->builder->getExt('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; - $arg .= $this->builder->getExt('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; - if ($this->builder->getExt('swoole-hook-odbc')) { - $config = (new SPCConfigUtil($this->builder))->getLibraryConfig($this->builder->getLib('unixodbc')); - $arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"'; - } - $arg .= $this->builder->getExt('ftp')?->isBuildStatic() ? ' --disable-swoole-ftp' : ' --enable-swoole-ftp'; - - if ($this->getExtVersion() >= '6.1.0') { - $arg .= ' --enable-swoole-stdext'; - } - - if (SPCTarget::getTargetOS() === 'Darwin') { - $arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no'; - } - - return $arg; - } -} diff --git a/src/SPC/builder/extension/swoole_hook_mysql.php b/src/SPC/builder/extension/swoole_hook_mysql.php deleted file mode 100644 index f60f4f8e0..000000000 --- a/src/SPC/builder/extension/swoole_hook_mysql.php +++ /dev/null @@ -1,30 +0,0 @@ -execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $this->getSharedExtensionLoadString() . ' --ri "swoole"', false); - $out = implode('', $out); - if ($ret !== 0) { - throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: 'extension swoole_hook_mysql sanity check'); - } - if (!str_contains($out, 'mysqlnd')) { - throw new ValidationException('swoole mysql hook is not enabled correctly.', validation_module: 'Extension swoole mysql hook availability check'); - } - } -} diff --git a/src/SPC/builder/extension/swoole_hook_odbc.php b/src/SPC/builder/extension/swoole_hook_odbc.php deleted file mode 100644 index 8586dc51b..000000000 --- a/src/SPC/builder/extension/swoole_hook_odbc.php +++ /dev/null @@ -1,40 +0,0 @@ -builder->getExt('pdo_odbc')?->isBuildStatic()) { - throw new WrongUsageException('swoole-hook-odbc provides pdo_odbc, if you enable odbc hook for swoole, you must remove pdo_odbc extension.'); - } - } - - public function runCliCheckUnix(): void - { - $sharedExtensions = $this->getSharedExtensionLoadString(); - [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"', false); - $out = implode('', $out); - if ($ret !== 0) { - throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check"); - } - if (!str_contains($out, 'coroutine_odbc')) { - throw new ValidationException('swoole odbc hook is not enabled correctly.', validation_module: 'Extension swoole odbc hook availability check'); - } - } -} diff --git a/src/SPC/builder/extension/swoole_hook_pgsql.php b/src/SPC/builder/extension/swoole_hook_pgsql.php deleted file mode 100644 index bb2b4cfdb..000000000 --- a/src/SPC/builder/extension/swoole_hook_pgsql.php +++ /dev/null @@ -1,46 +0,0 @@ -builder->getExt('pdo_pgsql')?->isBuildStatic()) { - throw new WrongUsageException('swoole-hook-pgsql provides pdo_pgsql, if you enable pgsql hook for swoole, you must remove pdo_pgsql extension.'); - } - } - - public function runCliCheckUnix(): void - { - $sharedExtensions = $this->getSharedExtensionLoadString(); - [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"', false); - $out = implode('', $out); - if ($ret !== 0) { - throw new ValidationException( - "extension {$this->getName()} failed sanity check: php-cli returned {$ret}", - validation_module: 'Extension swoole-hook-pgsql sanity check' - ); - } - if (!str_contains($out, 'coroutine_pgsql')) { - throw new ValidationException( - 'swoole pgsql hook is not enabled correctly.', - validation_module: 'Extension swoole pgsql hook availability check' - ); - } - } -} diff --git a/src/SPC/builder/extension/swoole_hook_sqlite.php b/src/SPC/builder/extension/swoole_hook_sqlite.php deleted file mode 100644 index d87342ba1..000000000 --- a/src/SPC/builder/extension/swoole_hook_sqlite.php +++ /dev/null @@ -1,40 +0,0 @@ -builder->getExt('pdo_sqlite')?->isBuildStatic()) { - throw new WrongUsageException('swoole-hook-sqlite provides pdo_sqlite, if you enable sqlite hook for swoole, you must remove pdo_sqlite extension.'); - } - } - - public function runCliCheckUnix(): void - { - $sharedExtensions = $this->getSharedExtensionLoadString(); - [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"', false); - $out = implode('', $out); - if ($ret !== 0) { - throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check"); - } - if (!str_contains($out, 'coroutine_sqlite')) { - throw new ValidationException('swoole sqlite hook is not enabled correctly.', validation_module: 'Extension swoole sqlite hook availability check'); - } - } -} diff --git a/src/SPC/builder/extension/swow.php b/src/SPC/builder/extension/swow.php deleted file mode 100644 index e2a5cbad0..000000000 --- a/src/SPC/builder/extension/swow.php +++ /dev/null @@ -1,41 +0,0 @@ -builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') { - throw new ValidationException('The latest swow extension requires PHP 8.0 or later'); - } - } - - public function getConfigureArg(bool $shared = false): string - { - $arg = '--enable-swow'; - $arg .= $this->builder->getLib('openssl') ? ' --enable-swow-ssl' : ' --disable-swow-ssl'; - $arg .= $this->builder->getLib('curl') ? ' --enable-swow-curl' : ' --disable-swow-curl'; - return $arg; - } - - public function patchBeforeBuildconf(): bool - { - if ($this->builder->getPHPVersionID() >= 80000 && !is_link(SOURCE_PATH . '/php-src/ext/swow')) { - if (PHP_OS_FAMILY === 'Windows') { - f_passthru('cd ' . SOURCE_PATH . '/php-src/ext && mklink /D swow swow-src\ext'); - } else { - f_passthru('cd ' . SOURCE_PATH . '/php-src/ext && ln -s swow-src/ext swow'); - } - return true; - } - return false; - } -} diff --git a/src/SPC/builder/extension/trader.php b/src/SPC/builder/extension/trader.php deleted file mode 100644 index 030215297..000000000 --- a/src/SPC/builder/extension/trader.php +++ /dev/null @@ -1,19 +0,0 @@ -builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') { - throw new ValidationException('The latest uv extension requires PHP 8.0 or later'); - } - } - - public function patchBeforeSharedMake(): bool - { - parent::patchBeforeSharedMake(); - if (PHP_OS_FAMILY !== 'Linux' || arch2gnu(php_uname('m')) !== 'aarch64') { - return false; - } - FileSystem::replaceFileRegex($this->source_dir . '/Makefile', '/^(LDFLAGS =.*)$/m', '$1 -luv -ldl -lrt -pthread'); - return true; - } -} diff --git a/src/SPC/builder/extension/xhprof.php b/src/SPC/builder/extension/xhprof.php deleted file mode 100644 index c3d98aac3..000000000 --- a/src/SPC/builder/extension/xhprof.php +++ /dev/null @@ -1,33 +0,0 @@ -builder->getLib('openssl')) { - $arg .= ' --with-openssl=' . BUILD_ROOT_PATH; - } - return $arg; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - return '--with-xlswriter'; - } - - public function patchBeforeMake(): bool - { - $patched = parent::patchBeforeMake(); - - // Remove when https://github.com/viest/php-ext-xlswriter/pull/560 is merged - if (PHP_OS_FAMILY !== 'Windows') { - GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -std=gnu17'); - $patched = true; - } - - if (PHP_OS_FAMILY === 'Windows') { - // fix windows build with openssl extension duplicate symbol bug - SourcePatcher::patchFile('spc_fix_xlswriter_win32.patch', $this->source_dir); - $content = file_get_contents($this->source_dir . '/library/libxlsxwriter/src/theme.c'); - $bom = pack('CCC', 0xEF, 0xBB, 0xBF); - if (!str_starts_with($content, $bom)) { - file_put_contents($this->source_dir . '/library/libxlsxwriter/src/theme.c', $bom . $content); - } - return true; - } - return $patched; - } - - // Remove when https://github.com/viest/php-ext-xlswriter/pull/560 is merged - protected function getExtraEnv(): array - { - return ['CFLAGS' => '-std=gnu17']; - } -} diff --git a/src/SPC/builder/extension/xml.php b/src/SPC/builder/extension/xml.php deleted file mode 100644 index a111ed39c..000000000 --- a/src/SPC/builder/extension/xml.php +++ /dev/null @@ -1,52 +0,0 @@ -name) { - 'xml' => '--enable-xml', - 'soap' => '--enable-soap', - 'xmlreader' => '--enable-xmlreader', - 'xmlwriter' => '--enable-xmlwriter', - 'simplexml' => '--enable-simplexml', - default => throw new SPCInternalException('Not accept non-xml extension'), - }; - $arg .= ($shared ? '=shared' : '') . ' --with-libxml="' . BUILD_ROOT_PATH . '"'; - return $arg; - } - - public function patchBeforeBuildconf(): bool - { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/win32/build/config.w32', 'dllmain.c ', ''); - return true; - } - - public function getWindowsConfigureArg(bool $shared = false): string - { - $arg = match ($this->name) { - 'xml' => '--with-xml', - 'soap' => '--enable-soap', - 'xmlreader' => '--enable-xmlreader', - 'xmlwriter' => '--enable-xmlwriter', - 'simplexml' => '--with-simplexml', - default => throw new SPCInternalException('Not accept non-xml extension'), - }; - $arg .= ' --with-libxml'; - return $arg; - } -} diff --git a/src/SPC/builder/extension/yac.php b/src/SPC/builder/extension/yac.php deleted file mode 100644 index 335aafa1c..000000000 --- a/src/SPC/builder/extension/yac.php +++ /dev/null @@ -1,26 +0,0 @@ -builder->getPHPVersionID() >= 80400 ? '' : ' --with-zlib-dir=' . BUILD_ROOT_PATH; - return '--with-zlib' . $zlib_dir; - } -} diff --git a/src/SPC/builder/extension/zstd.php b/src/SPC/builder/extension/zstd.php deleted file mode 100644 index 3a6df602c..000000000 --- a/src/SPC/builder/extension/zstd.php +++ /dev/null @@ -1,17 +0,0 @@ -options = $options; - - // ---------- set necessary options ---------- - // set C Compiler (default: clang) - f_putenv('CC=' . $this->getOption('cc', 'clang')); - // set C++ Compiler (default: clang++) - f_putenv('CXX=' . $this->getOption('cxx', 'clang++')); - // set PATH - f_putenv('PATH=' . BUILD_ROOT_PATH . '/bin:' . getenv('PATH')); - - // set arch (default: current) - $this->setOptionIfNotExist('arch', php_uname('m')); - $this->setOptionIfNotExist('gnu-arch', arch2gnu($this->getOption('arch'))); - - // ---------- set necessary compile environments ---------- - // concurrency - $this->concurrency = SystemUtil::getCpuCount(); - // cflags - $this->arch_c_flags = SystemUtil::getArchCFlags($this->getOption('arch')); - $this->arch_cxx_flags = SystemUtil::getArchCFlags($this->getOption('arch')); - - // create pkgconfig and include dir (some libs cannot create them automatically) - f_mkdir(BUILD_LIB_PATH . '/pkgconfig', recursive: true); - f_mkdir(BUILD_INCLUDE_PATH, recursive: true); - } - - /** - * Just start to build statically linked php binary - * - * @param int $build_target build target - */ - public function buildPHP(int $build_target = BUILD_TARGET_NONE): void - { - $this->emitPatchPoint('before-php-buildconf'); - SourcePatcher::patchBeforeBuildconf($this); - - shell()->cd(SOURCE_PATH . '/php-src')->exec('./buildconf --force'); - - $this->emitPatchPoint('before-php-configure'); - SourcePatcher::patchBeforeConfigure($this); - - $json_74 = $this->getPHPVersionID() < 80000 ? '--enable-json ' : ''; - $zts_enable = $this->getPHPVersionID() < 80000 ? '--enable-maintainer-zts --disable-zend-signals' : '--enable-zts --disable-zend-signals'; - $zts = $this->getOption('enable-zts', false) ? $zts_enable : ''; - - $config_file_path = $this->getOption('with-config-file-path', false) ? - ('--with-config-file-path=' . $this->getOption('with-config-file-path') . ' ') : ''; - $config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ? - ('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : ''; - - $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; - $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; - $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; - $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; - $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; - - shell()->cd(SOURCE_PATH . '/php-src') - ->exec( - './configure ' . - '--prefix= ' . - '--with-valgrind=no ' . // Not detect memory leak - '--enable-shared=no ' . - '--enable-static=yes ' . - "CFLAGS='{$this->arch_c_flags} -Werror=unknown-warning-option' " . - '--disable-all ' . - '--disable-cgi ' . - '--disable-phpdbg ' . - ($enableCli ? '--enable-cli ' : '--disable-cli ') . - ($enableFpm ? '--enable-fpm ' : '--disable-fpm ') . - ($enableEmbed ? '--enable-embed=static ' : '--disable-embed ') . - ($enableMicro ? '--enable-micro ' : '--disable-micro ') . - $config_file_path . - $config_file_scan_dir . - $json_74 . - $zts . - $this->makeStaticExtensionArgs() - ); - - $this->emitPatchPoint('before-php-make'); - SourcePatcher::patchBeforeMake($this); - - $this->cleanMake(); - - if ($enableCli) { - logger()->info('building cli'); - $this->buildCli(); - } - if ($enableFpm) { - logger()->info('building fpm'); - $this->buildFpm(); - } - if ($enableMicro) { - logger()->info('building micro'); - $this->buildMicro(); - } - if ($enableEmbed) { - logger()->info('building embed'); - if ($enableMicro) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); - } - $this->buildEmbed(); - } - $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); - if (!empty($shared_extensions)) { - logger()->info('Building shared extensions ...'); - $this->buildSharedExts(); - } - if ($enableFrankenphp) { - logger()->info('building frankenphp'); - $this->buildFrankenphp(); - } - } - - public function testPHP(int $build_target = BUILD_TARGET_NONE) - { - if (php_uname('m') === $this->getOption('arch')) { - $this->emitPatchPoint('before-sanity-check'); - $this->sanityCheck($build_target); - } - } - - /** - * Build cli sapi - */ - protected function buildCli(): void - { - $vars = SystemUtil::makeEnvVarString([ - 'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size - 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} /usr/lib/libm.a", - ]); - - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $shell->exec('sed -ie "s|//lib|/lib|g" Makefile'); - $shell->exec("make -j{$this->concurrency} {$vars} cli"); - if (!$this->getOption('no-strip', false)) { - $shell->exec('strip sapi/cli/php'); - } - $this->deploySAPIBinary(BUILD_TARGET_CLI); - } - - /** - * Build phpmicro sapi - */ - protected function buildMicro(): void - { - if ($this->getPHPVersionID() < 80000) { - throw new WrongUsageException('phpmicro only support PHP >= 8.0!'); - } - if ($this->getExt('phar')) { - $this->phar_patched = true; - SourcePatcher::patchMicroPhar($this->getPHPVersionID()); - } - - $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $vars = [ - // with debug information, optimize for size, remove identifiers, patch fake cli for micro - 'EXTRA_CFLAGS' => '-g -Os' . $enable_fake_cli, - // link resolv library (macOS needs it) - 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} /usr/lib/libm.a", - ]; - $vars = SystemUtil::makeEnvVarString($vars); - - shell()->cd(SOURCE_PATH . '/php-src') - ->exec("make -j{$this->concurrency} {$vars} micro"); - - if (!$this->getOption('no-strip', false)) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/micro')->exec('strip --strip-unneeded micro.sfx'); - } - $this->deploySAPIBinary(BUILD_TARGET_MICRO); - - if ($this->phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - - /** - * Build fpm sapi - */ - protected function buildFpm(): void - { - $vars = SystemUtil::makeEnvVarString([ - 'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size - 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} /usr/lib/libm.a", // link resolv library (macOS needs it) - ]); - - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $shell->exec("make -j{$this->concurrency} {$vars} fpm"); - if (!$this->getOption('no-strip', false)) { - $shell->exec('strip sapi/fpm/php-fpm'); - } - $this->deploySAPIBinary(BUILD_TARGET_FPM); - } - - /** - * Build embed sapi - */ - protected function buildEmbed(): void - { - $vars = SystemUtil::makeEnvVarString([ - 'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size - 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} /usr/lib/libm.a", // link resolv library (macOS needs it) - ]); - - shell() - ->cd(SOURCE_PATH . '/php-src') - ->exec('make INSTALL_ROOT=' . BUILD_ROOT_PATH . " -j{$this->concurrency} {$vars} install") - // Workaround for https://github.com/php/php-src/issues/12082 - ->exec('rm -Rf ' . BUILD_ROOT_PATH . '/lib/php-o') - ->exec('mkdir ' . BUILD_ROOT_PATH . '/lib/php-o') - ->cd(BUILD_ROOT_PATH . '/lib/php-o') - ->exec('ar x ' . BUILD_ROOT_PATH . '/lib/libphp.a') - ->exec('rm ' . BUILD_ROOT_PATH . '/lib/libphp.a') - ->exec('ar rcs ' . BUILD_ROOT_PATH . '/lib/libphp.a *.o') - ->exec('rm -Rf ' . BUILD_ROOT_PATH . '/lib/php-o'); - } -} diff --git a/src/SPC/builder/freebsd/SystemUtil.php b/src/SPC/builder/freebsd/SystemUtil.php deleted file mode 100644 index cbc206b98..000000000 --- a/src/SPC/builder/freebsd/SystemUtil.php +++ /dev/null @@ -1,46 +0,0 @@ -execWithResult('sysctl -n hw.ncpu'); - if ($ret !== 0) { - throw new EnvironmentException( - 'Failed to get cpu count from FreeBSD sysctl', - 'Please ensure you are running this command on a FreeBSD system and have the sysctl command available.' - ); - } - - return (int) $output[0]; - } - - /** - * Get Target Arch CFlags - * - * @param string $arch Arch Name - * @return string return Arch CFlags string - */ - public static function getArchCFlags(string $arch): string - { - return match ($arch) { - 'amd64', 'x86_64' => '--target=x86_64-unknown-freebsd', - 'arm64','aarch64' => '--target=aarch-unknown-freebsd', - default => throw new WrongUsageException('unsupported arch: ' . $arch), - }; - } -} diff --git a/src/SPC/builder/freebsd/library/BSDLibraryBase.php b/src/SPC/builder/freebsd/library/BSDLibraryBase.php deleted file mode 100644 index 6267fbbe9..000000000 --- a/src/SPC/builder/freebsd/library/BSDLibraryBase.php +++ /dev/null @@ -1,27 +0,0 @@ -builder; - } -} diff --git a/src/SPC/builder/freebsd/library/bzip2.php b/src/SPC/builder/freebsd/library/bzip2.php deleted file mode 100644 index 8695adac7..000000000 --- a/src/SPC/builder/freebsd/library/bzip2.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\freebsd\library; - -use SPC\builder\macos\library\MacOSLibraryBase; - -class openssl extends BSDLibraryBase -{ - public const NAME = 'openssl'; - - protected function build(): void - { - [$lib,,$destdir] = SEPARATED_PATH; - - // lib:zlib - $extra = ''; - $ex_lib = ''; - $zlib = $this->builder->getLib('zlib'); - if ($zlib instanceof MacOSLibraryBase) { - $extra = 'zlib'; - $ex_lib = trim($zlib->getStaticLibFiles() . ' ' . $ex_lib); - } - - shell()->cd($this->source_dir)->initializeEnv($this) - ->exec( - "./Configure no-shared {$extra} " . - '--prefix=/ ' . // use prefix=/ - "--libdir={$lib} " . - '--openssldir=/etc/ssl ' . - 'BSD-' . arch2gnu($this->builder->getOption('arch')) - ) - ->exec('make clean') - ->exec("make -j{$this->builder->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") - ->exec("make install_sw DESTDIR={$destdir}"); - $this->patchPkgconfPrefix(['libssl.pc', 'openssl.pc', 'libcrypto.pc']); - } -} diff --git a/src/SPC/builder/freebsd/library/pkgconfig.php b/src/SPC/builder/freebsd/library/pkgconfig.php deleted file mode 100644 index 62bd8ace7..000000000 --- a/src/SPC/builder/freebsd/library/pkgconfig.php +++ /dev/null @@ -1,15 +0,0 @@ -options = $options; - - GlobalEnvManager::init(); - GlobalEnvManager::afterInit(); - - // concurrency - $this->concurrency = (int) getenv('SPC_CONCURRENCY'); - // cflags - $this->arch_c_flags = getenv('SPC_DEFAULT_C_FLAGS'); - $this->arch_cxx_flags = getenv('SPC_DEFAULT_CXX_FLAGS'); - $this->arch_ld_flags = getenv('SPC_DEFAULT_LD_FLAGS'); - - // create pkgconfig and include dir (some libs cannot create them automatically) - f_mkdir(BUILD_LIB_PATH . '/pkgconfig', recursive: true); - f_mkdir(BUILD_INCLUDE_PATH, recursive: true); - } - - /** - * Build PHP from source. - * - * @param int $build_target Build target, use `BUILD_TARGET_*` constants - */ - public function buildPHP(int $build_target = BUILD_TARGET_NONE): void - { - $cflags = $this->arch_c_flags; - f_putenv('CFLAGS=' . $cflags); - - $this->emitPatchPoint('before-php-buildconf'); - SourcePatcher::patchBeforeBuildconf($this); - - shell()->cd(SOURCE_PATH . '/php-src')->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); - - $this->emitPatchPoint('before-php-configure'); - SourcePatcher::patchBeforeConfigure($this); - - $phpVersionID = $this->getPHPVersionID(); - $json_74 = $phpVersionID < 80000 ? '--enable-json ' : ''; - - $opcache_jit = !$this->getOption('disable-opcache-jit', false); - if ($opcache_jit && ($phpVersionID >= 80500 || $this->getExt('opcache'))) { - // php 8.5 contains opcache extension by default, - // if opcache_jit is enabled for 8.5 or opcache enabled, - // we need to disable undefined behavior sanitizer. - f_putenv('SPC_COMPILER_EXTRA=-fno-sanitize=undefined'); - } - - if ($this->getOption('enable-zts', false)) { - $maxExecutionTimers = $phpVersionID >= 80100 ? '--enable-zend-max-execution-timers ' : ''; - $zts = '--enable-zts --disable-zend-signals '; - } else { - $maxExecutionTimers = ''; - $zts = ''; - } - - $config_file_path = $this->getOption('with-config-file-path', false) ? - ('--with-config-file-path=' . $this->getOption('with-config-file-path') . ' ') : ''; - $config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ? - ('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : ''; - - $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; - $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; - $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; - $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; - $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; - $enableCgi = ($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI; - - // prepare build php envs - // $musl_flag = SPCTarget::getLibc() === 'musl' ? ' -D__MUSL__' : ' -U__MUSL__'; - $php_configure_env = SystemUtil::makeEnvVarString([ - 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'CPPFLAGS' => '-I' . BUILD_INCLUDE_PATH, // . ' -Dsomethinghere', // . $musl_flag, - 'LDFLAGS' => '-L' . BUILD_LIB_PATH, - // 'LIBS' => SPCTarget::getRuntimeLibs(), // do not pass static libraries here yet, they may contain polyfills for libc functions! - ]); - - $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; - - $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - if ($embed_type !== 'static' && SPCTarget::isStatic()) { - throw new WrongUsageException( - 'Linux does not support loading shared libraries when linking libc statically. ' . - 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' - ); - } - - $this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec( - $php_configure_env . ' ' . - getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . - ($enableCli ? '--enable-cli ' : '--disable-cli ') . - ($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . - ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . - ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') . - ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . - $config_file_path . - $config_file_scan_dir . - $json_74 . - $zts . - $maxExecutionTimers . - $phpvars . ' ' . - $this->makeStaticExtensionArgs() . ' ' - )); - - $this->emitPatchPoint('before-php-make'); - SourcePatcher::patchBeforeMake($this); - - $this->cleanMake(); - - if ($enableCli) { - logger()->info('building cli'); - $this->buildCli(); - } - if ($enableFpm) { - logger()->info('building fpm'); - $this->buildFpm(); - } - if ($enableCgi) { - logger()->info('building cgi'); - $this->buildCgi(); - } - if ($enableMicro) { - logger()->info('building micro'); - $this->buildMicro(); - } - if ($enableEmbed) { - logger()->info('building embed'); - if ($enableMicro) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); - } - $this->buildEmbed(); - } - if ($enableFrankenphp) { - logger()->info('building frankenphp'); - $this->buildFrankenphp(); - } - $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); - if (!empty($shared_extensions)) { - if (SPCTarget::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, use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc` or use SPC_MUSL_DYNAMIC=true on alpine.' - ); - } - logger()->info('Building shared extensions...'); - $this->buildSharedExts(); - } - } - - public function testPHP(int $build_target = BUILD_TARGET_NONE) - { - $this->emitPatchPoint('before-sanity-check'); - $this->sanityCheck($build_target); - } - - /** - * Build cli sapi - */ - protected function buildCli(): void - { - if ($this->getExt('readline') && SPCTarget::isStatic()) { - SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src'); - } - - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("make {$concurrency} {$vars} cli"); - - if ($this->getExt('readline') && SPCTarget::isStatic()) { - SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src', true); - } - - $this->deploySAPIBinary(BUILD_TARGET_CLI); - } - - protected function buildCgi(): void - { - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("make {$concurrency} {$vars} cgi"); - - $this->deploySAPIBinary(BUILD_TARGET_CGI); - } - - /** - * Build phpmicro sapi - */ - protected function buildMicro(): void - { - if ($this->getPHPVersionID() < 80000) { - throw new WrongUsageException('phpmicro only support PHP >= 8.0!'); - } - try { - if ($this->getExt('phar')) { - $this->phar_patched = true; - SourcePatcher::patchMicroPhar($this->getPHPVersionID()); - } - - $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $vars = $this->getMakeExtraVars(); - - // patch fake cli for micro - $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; - $vars = SystemUtil::makeEnvVarString($vars); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("make {$concurrency} {$vars} micro"); - - // deploy micro.sfx - $dst = $this->deploySAPIBinary(BUILD_TARGET_MICRO); - - // patch after UPX-ed micro.sfx - $this->processUpxedMicroSfx($dst); - } finally { - if ($this->phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - } - - /** - * Build fpm sapi - */ - protected function buildFpm(): void - { - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("make {$concurrency} {$vars} fpm"); - - $this->deploySAPIBinary(BUILD_TARGET_FPM); - } - - /** - * Build embed sapi - */ - protected function buildEmbed(): void - { - $sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared()); - $sharedExts = array_filter($sharedExts, static function ($ext) { - return Config::getExt($ext->getName(), 'build-with-php') === true; - }); - $install_modules = $sharedExts ? 'install-modules' : ''; - - // detect changes in module path - $diff = new DirDiff(BUILD_MODULES_PATH, true); - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs"); - - // process libphp.so for shared embed - $libphpSo = BUILD_LIB_PATH . '/libphp.so'; - $libphpSoDest = BUILD_LIB_PATH . '/libphp.so'; - if (file_exists($libphpSo)) { - // deploy libphp.so - preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); - if (!empty($matches[1])) { - $libphpSoDest = str_replace('.so', '-' . $matches[1] . '.so', $libphpSo); - } - $this->deployBinary($libphpSo, $libphpSoDest, false); - } - - // process shared extensions build-with-php - $increment_files = $diff->getChangedFiles(); - foreach ($increment_files as $increment_file) { - $this->deployBinary($increment_file, $increment_file, false); - } - - // process libphp.a for static embed - if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { - $AR = getenv('AR') ?: 'ar'; - f_passthru("{$AR} -t " . BUILD_LIB_PATH . "/libphp.a | grep '\\.a$' | xargs -n1 {$AR} d " . BUILD_LIB_PATH . '/libphp.a'); - // export dynamic symbols - SystemUtil::exportDynamicSymbols(BUILD_LIB_PATH . '/libphp.a'); - } - - // patch embed php scripts - $this->patchPhpScripts(); - } - - /** - * Return extra variables for php make command. - */ - private function getMakeExtraVars(): array - { - $config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); - $static = SPCTarget::isStatic() ? '-all-static' : ''; - $lib = BUILD_LIB_PATH; - return array_filter([ - 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'EXTRA_LIBS' => $config['libs'], - 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), - 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", - ]); - } - - /** - * Patch micro.sfx after UPX compression. - * micro needs special section handling in LinuxBuilder. - * The micro.sfx does not support UPX directly, but we can remove UPX - * info segment to adapt. - * This will also make micro.sfx with upx-packed more like a malware fore antivirus - */ - private function processUpxedMicroSfx(string $dst): void - { - if ($this->getOption('with-upx-pack') && version_compare($this->getMicroVersion(), '0.2.0') >= 0) { - // strip first - // cut binary with readelf - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); - } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); - } - } -} diff --git a/src/SPC/builder/linux/SystemUtil.php b/src/SPC/builder/linux/SystemUtil.php deleted file mode 100644 index 30b36b888..000000000 --- a/src/SPC/builder/linux/SystemUtil.php +++ /dev/null @@ -1,198 +0,0 @@ - 'unknown', - 'ver' => 'unknown', - ]; - switch (true) { - case file_exists('/etc/centos-release'): - $lines = file('/etc/centos-release'); - $centos = true; - goto rh; - case file_exists('/etc/redhat-release'): - $lines = file('/etc/redhat-release'); - $centos = false; - rh: - foreach ($lines as $line) { - if (preg_match('/release\s+(\d*(\.\d+)*)/', $line, $matches)) { - /* @phpstan-ignore-next-line */ - $ret['dist'] = $centos ? 'centos' : 'redhat'; - $ret['ver'] = $matches[1]; - } - } - break; - case file_exists('/etc/os-release'): - $lines = file('/etc/os-release'); - foreach ($lines as $line) { - if (preg_match('/^ID=(.*)$/', $line, $matches)) { - $ret['dist'] = $matches[1]; - } - if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) { - $ret['ver'] = $matches[1]; - } - } - $ret['dist'] = trim($ret['dist'], '"\''); - $ret['ver'] = trim($ret['ver'], '"\''); - if (strcasecmp($ret['dist'], 'centos') === 0) { - $ret['dist'] = 'redhat'; - } - break; - } - return $ret; - } - - public static function isMuslDist(): bool - { - return static::getOSRelease()['dist'] === 'alpine'; - } - - public static function getCpuCount(): int - { - $ncpu = 1; - - if (is_file('/proc/cpuinfo')) { - $cpuinfo = file_get_contents('/proc/cpuinfo'); - preg_match_all('/^processor/m', $cpuinfo, $matches); - $ncpu = count($matches[0]); - } - - return $ncpu; - } - - public static function findStaticLib(string $name): ?array - { - $paths = getenv('LIBPATH'); - if (!$paths) { - $paths = '/lib:/lib64:/usr/lib:/usr/lib64:/usr/local/lib:/usr/local/lib64:'; - } - foreach (explode(':', $paths) as $path) { - if (file_exists("{$path}/{$name}")) { - return ["{$path}", "{$name}"]; - } - } - return null; - } - - /** @noinspection PhpUnused */ - public static function findStaticLibs(array $names): ?array - { - $ret = []; - foreach ($names as $name) { - $path = static::findStaticLib($name); - if (!$path) { - logger()->warning("static library {$name} not found"); - return null; - } - $ret[] = $path; - } - return $ret; - } - - public static function findHeader(string $name): ?array - { - $paths = getenv('INCLUDEPATH'); - if (!$paths) { - $paths = '/include:/usr/include:/usr/local/include'; - } - foreach (explode(':', $paths) as $path) { - if (file_exists("{$path}/{$name}") || is_dir("{$path}/{$name}")) { - return ["{$path}", "{$name}"]; - } - } - return null; - } - - /** @noinspection PhpUnused */ - public static function findHeaders(array $names): ?array - { - $ret = []; - foreach ($names as $name) { - $path = static::findHeader($name); - if (!$path) { - logger()->warning("header {$name} not found"); - return null; - } - $ret[] = $path; - } - return $ret; - } - - /** - * Get fully-supported linux distros. - * - * @return string[] List of supported Linux distro name for doctor - */ - public static function getSupportedDistros(): array - { - return [ - // debian-like - 'debian', 'ubuntu', 'Deepin', 'neon', - // rhel-like - 'redhat', - // centos - 'centos', - // alpine - 'alpine', - // arch - 'arch', 'manjaro', - ]; - } - - /** - * Get libc version string from ldd - */ - public static function getLibcVersionIfExists(?string $libc = null): ?string - { - if (self::$libc_version !== null) { - return self::$libc_version; - } - if ($libc === 'glibc') { - $result = shell()->execWithResult('ldd --version', false); - if ($result[0] !== 0) { - return null; - } - // get first line - $first_line = $result[1][0]; - // match ldd version: "ldd (some useless text) 2.17" match 2.17 - $pattern = '/ldd\s+\(.*?\)\s+(\d+\.\d+)/'; - if (preg_match($pattern, $first_line, $matches)) { - self::$libc_version = $matches[1]; - return self::$libc_version; - } - return null; - } - if ($libc === 'musl') { - if (self::isMuslDist()) { - $result = shell()->execWithResult('ldd 2>&1', false); - } elseif (is_file('/usr/local/musl/lib/libc.so')) { - $result = shell()->execWithResult('/usr/local/musl/lib/libc.so 2>&1', false); - } else { - $arch = php_uname('m'); - $result = shell()->execWithResult("/lib/ld-musl-{$arch}.so.1 2>&1", false); - } - // Match Version * line - // match ldd version: "Version 1.2.3" match 1.2.3 - $pattern = '/Version\s+(\d+\.\d+\.\d+)/'; - if (preg_match($pattern, $result[1][1] ?? '', $matches)) { - self::$libc_version = $matches[1]; - return self::$libc_version; - } - } - return null; - } -} diff --git a/src/SPC/builder/linux/library/LinuxLibraryBase.php b/src/SPC/builder/linux/library/LinuxLibraryBase.php deleted file mode 100644 index 2a496590e..000000000 --- a/src/SPC/builder/linux/library/LinuxLibraryBase.php +++ /dev/null @@ -1,36 +0,0 @@ - true,代表依赖 curl 但可选 - */ - protected array $dep_names; - - public function __construct(protected LinuxBuilder $builder) - { - parent::__construct(); - } - - public function getBuilder(): BuilderBase - { - return $this->builder; - } -} diff --git a/src/SPC/builder/linux/library/attr.php b/src/SPC/builder/linux/library/attr.php deleted file mode 100644 index da29d48f7..000000000 --- a/src/SPC/builder/linux/library/attr.php +++ /dev/null @@ -1,12 +0,0 @@ -cd($this->source_dir . '/source')->initializeEnv($this) - ->exec( - "{$cppflags} {$cxxflags} {$ldflags} " . - './runConfigureICU Linux ' . - '--enable-static ' . - '--disable-shared ' . - '--with-data-packaging=static ' . - '--enable-release=yes ' . - '--enable-extras=no ' . - '--enable-icuio=yes ' . - '--enable-dyload=no ' . - '--enable-tools=yes ' . - '--enable-tests=no ' . - '--enable-samples=no ' . - '--prefix=' . BUILD_ROOT_PATH - ) - ->exec('make clean') - ->exec("make -j{$this->builder->concurrency}") - ->exec('make install'); - - $this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX); - FileSystem::removeDir(BUILD_LIB_PATH . '/icu'); - } -} diff --git a/src/SPC/builder/linux/library/idn2.php b/src/SPC/builder/linux/library/idn2.php deleted file mode 100644 index a271760d2..000000000 --- a/src/SPC/builder/linux/library/idn2.php +++ /dev/null @@ -1,12 +0,0 @@ -source_dir . '/Makefile', '-DMAC_OSX_KLUDGE=1', ''); - FileSystem::replaceFileStr($this->source_dir . '/src/osdep/unix/Makefile', 'CC=cc', "CC={$cc}"); - /* FileSystem::replaceFileStr($this->source_dir . '/src/osdep/unix/Makefile', '-lcrypto -lz', '-lcrypto'); - FileSystem::replaceFileStr($this->source_dir . '/src/osdep/unix/Makefile', '-lcrypto', '-lcrypto -lz'); - FileSystem::replaceFileStr( - $this->source_dir . '/src/osdep/unix/ssl_unix.c', - "#include \n#include ", - "#include \n#include " - ); - // SourcePatcher::patchFile('1006_openssl1.1_autoverify.patch', $this->source_dir); - SourcePatcher::patchFile('2014_openssl1.1.1_sni.patch', $this->source_dir); */ - FileSystem::replaceFileStr($this->source_dir . '/Makefile', 'SSLINCLUDE=/usr/include/openssl', 'SSLINCLUDE=' . BUILD_INCLUDE_PATH); - FileSystem::replaceFileStr($this->source_dir . '/Makefile', 'SSLLIB=/usr/lib', 'SSLLIB=' . BUILD_LIB_PATH); - return true; - } - - public function patchPhpConfig(): bool - { - if (SPCTarget::getLibc() === 'glibc') { - FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"'); - return true; - } - return false; - } - - protected function build(): void - { - if ($this->builder->getLib('openssl')) { - $ssl_options = 'SPECIALAUTHENTICATORS=ssl SSLTYPE=unix.nopwd SSLINCLUDE=' . BUILD_INCLUDE_PATH . ' SSLLIB=' . BUILD_LIB_PATH; - } else { - $ssl_options = 'SSLTYPE=none'; - } - $libcVer = SPCTarget::getLibcVersion(); - $extraLibs = $libcVer && version_compare($libcVer, '2.17', '<=') ? 'EXTRALDFLAGS="-ldl -lrt -lpthread"' : ''; - shell()->cd($this->source_dir) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec("yes | make slx {$ssl_options} EXTRACFLAGS='-fPIC -fpermissive' {$extraLibs}"); - try { - shell() - ->exec("cp -rf {$this->source_dir}/c-client/c-client.a " . BUILD_LIB_PATH . '/libc-client.a') - ->exec("cp -rf {$this->source_dir}/c-client/*.c " . BUILD_LIB_PATH . '/') - ->exec("cp -rf {$this->source_dir}/c-client/*.h " . BUILD_INCLUDE_PATH . '/') - ->exec("cp -rf {$this->source_dir}/src/osdep/unix/*.h " . BUILD_INCLUDE_PATH . '/'); - } catch (\Throwable) { - // last command throws an exception, no idea why since it works - } - } -} diff --git a/src/SPC/builder/linux/library/jbig.php b/src/SPC/builder/linux/library/jbig.php deleted file mode 100644 index e47e21f46..000000000 --- a/src/SPC/builder/linux/library/jbig.php +++ /dev/null @@ -1,12 +0,0 @@ -source_dir . '/Makefile', 'LIBRARY_REL ?= lib/x86_64-linux-gnu', 'LIBRARY_REL ?= lib'); - return true; - } -} diff --git a/src/SPC/builder/linux/library/libavif.php b/src/SPC/builder/linux/library/libavif.php deleted file mode 100644 index 410d549d4..000000000 --- a/src/SPC/builder/linux/library/libavif.php +++ /dev/null @@ -1,12 +0,0 @@ -configure()->make(); - - if (is_file(BUILD_ROOT_PATH . '/lib64/libffi.a')) { - copy(BUILD_ROOT_PATH . '/lib64/libffi.a', BUILD_ROOT_PATH . '/lib/libffi.a'); - unlink(BUILD_ROOT_PATH . '/lib64/libffi.a'); - } - $this->patchPkgconfPrefix(['libffi.pc']); - } -} diff --git a/src/SPC/builder/linux/library/libheif.php b/src/SPC/builder/linux/library/libheif.php deleted file mode 100644 index d2cc51630..000000000 --- a/src/SPC/builder/linux/library/libheif.php +++ /dev/null @@ -1,12 +0,0 @@ -addConfigureArgs('-DCMAKE_INSTALL_RPATH=""') - ->build(); - } -} diff --git a/src/SPC/builder/linux/library/libpng.php b/src/SPC/builder/linux/library/libpng.php deleted file mode 100644 index 30d5a7f31..000000000 --- a/src/SPC/builder/linux/library/libpng.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\linux\library; - -use SPC\util\executor\UnixAutoconfExecutor; - -class libpng extends LinuxLibraryBase -{ - public const NAME = 'libpng'; - - public function build(): void - { - UnixAutoconfExecutor::create($this) - ->exec('chmod +x ./configure') - ->exec('chmod +x ./install-sh') - ->appendEnv(['LDFLAGS' => "-L{$this->getLibDir()}"]) - ->configure( - '--enable-hardware-optimizations', - "--with-zlib-prefix={$this->getBuildRootPath()}", - match (getenv('SPC_ARCH')) { - 'x86_64' => '--enable-intel-sse', - 'aarch64' => '--enable-arm-neon', - default => '', - } - ) - ->make('libpng16.la', 'install-libLTLIBRARIES install-data-am', after_env_vars: ['DEFAULT_INCLUDES' => "-I{$this->source_dir} -I{$this->getIncludeDir()}"]); - - $this->patchPkgconfPrefix(['libpng16.pc'], PKGCONF_PATCH_PREFIX); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/linux/library/librabbitmq.php b/src/SPC/builder/linux/library/librabbitmq.php deleted file mode 100644 index 657aece8c..000000000 --- a/src/SPC/builder/linux/library/librabbitmq.php +++ /dev/null @@ -1,12 +0,0 @@ -source_dir . '/configure', 'realpath -s', 'realpath'); - return true; - } - return false; - } - - protected function build(): void - { - $use_libc = ToolchainManager::getToolchainClass() !== GccNativeToolchain::class || version_compare(SPCTarget::getLibcVersion(), '2.30', '>='); - $make = UnixAutoconfExecutor::create($this); - - if ($use_libc) { - $make->appendEnv([ - 'CFLAGS' => '-D_GNU_SOURCE', - ]); - } - - $make - ->removeConfigureArgs( - '--disable-shared', - '--enable-static', - '--with-pic', - '--enable-pic', - ) - ->addConfigureArgs( - $use_libc ? '--use-libc' : '', - ) - ->configure() - ->make('library ENABLE_SHARED=0', 'install ENABLE_SHARED=0', with_clean: false); - - $this->patchPkgconfPrefix(); - } -} diff --git a/src/SPC/builder/linux/library/libuuid.php b/src/SPC/builder/linux/library/libuuid.php deleted file mode 100644 index 85f0d2152..000000000 --- a/src/SPC/builder/linux/library/libuuid.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\linux\library; - -class libwebp extends LinuxLibraryBase -{ - use \SPC\builder\unix\library\libwebp; - - public const NAME = 'libwebp'; -} diff --git a/src/SPC/builder/linux/library/libxml2.php b/src/SPC/builder/linux/library/libxml2.php deleted file mode 100644 index 102f60b78..000000000 --- a/src/SPC/builder/linux/library/libxml2.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\linux\library; - -use SPC\builder\linux\SystemUtil; -use SPC\store\FileSystem; - -class openssl extends LinuxLibraryBase -{ - use \SPC\builder\traits\openssl; - - public const NAME = 'openssl'; - - public function build(): void - { - $extra = ''; - $ex_lib = '-ldl -pthread'; - $arch = getenv('SPC_ARCH'); - - $env = "CC='" . getenv('CC') . ' -idirafter ' . BUILD_INCLUDE_PATH . - ' -idirafter /usr/include/ ' . - ' -idirafter /usr/include/' . getenv('SPC_ARCH') . '-linux-gnu/ ' . - "' "; - // lib:zlib - $zlib = $this->builder->getLib('zlib'); - if ($zlib instanceof LinuxLibraryBase) { - $extra = 'zlib'; - $ex_lib = trim($zlib->getStaticLibFiles() . ' ' . $ex_lib); - $zlib_extra = - '--with-zlib-include=' . BUILD_INCLUDE_PATH . ' ' . - '--with-zlib-lib=' . BUILD_LIB_PATH . ' '; - } else { - $zlib_extra = ''; - } - - $openssl_dir = getenv('OPENSSLDIR') ?: null; - // TODO: in v3 use the following: $openssl_dir ??= SystemUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; - $openssl_dir ??= '/etc/ssl'; - $ex_lib = trim($ex_lib); - - shell()->cd($this->source_dir)->initializeEnv($this) - ->exec( - "{$env} ./Configure no-shared {$extra} " . - '--prefix=' . BUILD_ROOT_PATH . ' ' . - '--libdir=' . BUILD_LIB_PATH . ' ' . - "--openssldir={$openssl_dir} " . - "{$zlib_extra}" . - 'enable-pie ' . - 'no-legacy ' . - 'no-tests ' . - "linux-{$arch}" - ) - ->exec('make clean') - ->exec("make -j{$this->builder->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") - ->exec('make install_sw'); - $this->patchPkgconfPrefix(['libssl.pc', 'openssl.pc', 'libcrypto.pc']); - // patch for openssl 3.3.0+ - if (!str_contains($file = FileSystem::readFile(BUILD_LIB_PATH . '/pkgconfig/libssl.pc'), 'prefix=')) { - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/libssl.pc', 'prefix=' . BUILD_ROOT_PATH . "\n" . $file); - } - if (!str_contains($file = FileSystem::readFile(BUILD_LIB_PATH . '/pkgconfig/openssl.pc'), 'prefix=')) { - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/openssl.pc', 'prefix=' . BUILD_ROOT_PATH . "\n" . $file); - } - if (!str_contains($file = FileSystem::readFile(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc'), 'prefix=')) { - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc', 'prefix=' . BUILD_ROOT_PATH . "\n" . $file); - } - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc', '/Libs.private:.*/m', 'Requires.private: zlib'); - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/cmake/OpenSSL/OpenSSLConfig.cmake', '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")'); - } -} diff --git a/src/SPC/builder/linux/library/pkgconfig.php b/src/SPC/builder/linux/library/pkgconfig.php deleted file mode 100644 index eacca36de..000000000 --- a/src/SPC/builder/linux/library/pkgconfig.php +++ /dev/null @@ -1,15 +0,0 @@ -options = $options; - - // apply global environment variables - GlobalEnvManager::init(); - GlobalEnvManager::afterInit(); - - // ---------- set necessary compile vars ---------- - // concurrency - $this->concurrency = intval(getenv('SPC_CONCURRENCY')); - // cflags - $this->arch_c_flags = getenv('SPC_DEFAULT_C_FLAGS'); - $this->arch_cxx_flags = getenv('SPC_DEFAULT_CXX_FLAGS'); - $this->arch_ld_flags = getenv('SPC_DEFAULT_LD_FLAGS'); - - // create pkgconfig and include dir (some libs cannot create them automatically) - f_mkdir(BUILD_LIB_PATH . '/pkgconfig', recursive: true); - f_mkdir(BUILD_INCLUDE_PATH, recursive: true); - } - - /** - * Get dynamically linked macOS frameworks - * - * @param bool $asString If true, return as string - */ - public function getFrameworks(bool $asString = false): array|string - { - $libs = []; - - // reorder libs - foreach ($this->libs as $lib) { - foreach ($lib->getDependencies() as $dep) { - $libs[] = $dep; - } - $libs[] = $lib; - } - - $frameworks = []; - /** @var MacOSLibraryBase $lib */ - foreach ($libs as $lib) { - array_push($frameworks, ...$lib->getFrameworks()); - } - - foreach ($this->exts as $ext) { - array_push($frameworks, ...$ext->getFrameworks()); - } - - if ($asString) { - return implode(' ', array_map(fn ($x) => "-framework {$x}", $frameworks)); - } - return $frameworks; - } - - /** - * Just start to build statically linked php binary - * - * @param int $build_target build target - */ - public function buildPHP(int $build_target = BUILD_TARGET_NONE): void - { - $this->emitPatchPoint('before-php-buildconf'); - SourcePatcher::patchBeforeBuildconf($this); - - shell()->cd(SOURCE_PATH . '/php-src')->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); - - $this->emitPatchPoint('before-php-configure'); - SourcePatcher::patchBeforeConfigure($this); - - $phpVersionID = $this->getPHPVersionID(); - $json_74 = $phpVersionID < 80000 ? '--enable-json ' : ''; - $zts = $this->getOption('enable-zts', false) ? '--enable-zts --disable-zend-signals ' : ''; - - $config_file_path = $this->getOption('with-config-file-path', false) ? - ('--with-config-file-path=' . $this->getOption('with-config-file-path') . ' ') : ''; - $config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ? - ('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : ''; - - $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; - $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; - $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; - $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; - $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; - $enableCgi = ($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI; - - // prepare build php envs - $envs_build_php = SystemUtil::makeEnvVarString([ - 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'CPPFLAGS' => '-I' . BUILD_INCLUDE_PATH, - 'LDFLAGS' => '-L' . BUILD_LIB_PATH, - ]); - - if ($this->getLib('postgresql')) { - shell() - ->cd(SOURCE_PATH . '/php-src') - ->exec( - 'sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure' - ); - } - - $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - $this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec( - getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . - ($enableCli ? '--enable-cli ' : '--disable-cli ') . - ($enableFpm ? '--enable-fpm ' : '--disable-fpm ') . - ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . - ($enableMicro ? '--enable-micro ' : '--disable-micro ') . - ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . - $config_file_path . - $config_file_scan_dir . - $json_74 . - $zts . - $this->makeStaticExtensionArgs() . ' ' . - $envs_build_php - )); - - $this->emitPatchPoint('before-php-make'); - SourcePatcher::patchBeforeMake($this); - - $this->cleanMake(); - - if ($enableCli) { - logger()->info('building cli'); - $this->buildCli(); - } - if ($enableFpm) { - logger()->info('building fpm'); - $this->buildFpm(); - } - if ($enableCgi) { - logger()->info('building cgi'); - $this->buildCgi(); - } - if ($enableMicro) { - logger()->info('building micro'); - $this->buildMicro(); - } - if ($enableEmbed) { - logger()->info('building embed'); - if ($enableMicro) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); - } - $this->buildEmbed(); - } - if ($enableFrankenphp) { - logger()->info('building frankenphp'); - $this->buildFrankenphp(); - } - $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); - if (!empty($shared_extensions)) { - logger()->info('Building shared extensions ...'); - $this->buildSharedExts(); - } - } - - public function testPHP(int $build_target = BUILD_TARGET_NONE) - { - $this->emitPatchPoint('before-sanity-check'); - $this->sanityCheck($build_target); - } - - /** - * Build cli sapi - */ - protected function buildCli(): void - { - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - $shell->exec("make {$concurrency} {$vars} cli"); - $this->deploySAPIBinary(BUILD_TARGET_CLI); - } - - protected function buildCgi(): void - { - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - $shell->exec("make {$concurrency} {$vars} cgi"); - $this->deploySAPIBinary(BUILD_TARGET_CGI); - } - - /** - * Build phpmicro sapi - */ - protected function buildMicro(): void - { - if ($this->getPHPVersionID() < 80000) { - throw new WrongUsageException('phpmicro only support PHP >= 8.0!'); - } - - try { - if ($this->getExt('phar')) { - $this->phar_patched = true; - SourcePatcher::patchMicroPhar($this->getPHPVersionID()); - } - - $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $vars = $this->getMakeExtraVars(); - - // patch fake cli for micro - $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; - $vars = SystemUtil::makeEnvVarString($vars); - - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - // build - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - $shell->exec("make {$concurrency} {$vars} micro"); - - $this->deploySAPIBinary(BUILD_TARGET_MICRO); - } finally { - if ($this->phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - } - - /** - * Build fpm sapi - */ - protected function buildFpm(): void - { - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - $shell->exec("make {$concurrency} {$vars} fpm"); - $this->deploySAPIBinary(BUILD_TARGET_FPM); - } - - /** - * Build embed sapi - */ - protected function buildEmbed(): void - { - $sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared()); - $sharedExts = array_filter($sharedExts, static function ($ext) { - return Config::getExt($ext->getName(), 'build-with-php') === true; - }); - $install_modules = $sharedExts ? 'install-modules' : ''; - $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - - $diff = new DirDiff(BUILD_MODULES_PATH, true); - - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "" "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs"); - - $libphp = BUILD_LIB_PATH . '/libphp.dylib'; - if (file_exists($libphp)) { - $this->deployBinary($libphp, $libphp, false); - // macOS currently have no -release option for dylib, so we just rename it here - } - - // process shared extensions build-with-php - $increment_files = $diff->getChangedFiles(); - foreach ($increment_files as $increment_file) { - $this->deployBinary($increment_file, $increment_file, false); - } - - if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { - $AR = getenv('AR') ?: 'ar'; - f_passthru("{$AR} -t " . BUILD_LIB_PATH . "/libphp.a | grep '\\.a$' | xargs -n1 {$AR} d " . BUILD_LIB_PATH . '/libphp.a'); - // export dynamic symbols - SystemUtil::exportDynamicSymbols(BUILD_LIB_PATH . '/libphp.a'); - } - $this->patchPhpScripts(); - } - - private function getMakeExtraVars(): array - { - $config = (new SPCConfigUtil($this, ['libs_only_deps' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); - return array_filter([ - 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'EXTRA_LDFLAGS_PROGRAM' => '-L' . BUILD_LIB_PATH, - 'EXTRA_LIBS' => $config['libs'], - ]); - } -} diff --git a/src/SPC/builder/macos/SystemUtil.php b/src/SPC/builder/macos/SystemUtil.php deleted file mode 100644 index a75a78a17..000000000 --- a/src/SPC/builder/macos/SystemUtil.php +++ /dev/null @@ -1,46 +0,0 @@ - '--target=x86_64-apple-darwin', - 'arm64','aarch64' => '--target=arm64-apple-darwin', - default => throw new WrongUsageException('unsupported arch: ' . $arch), - }; - } -} diff --git a/src/SPC/builder/macos/library/MacOSLibraryBase.php b/src/SPC/builder/macos/library/MacOSLibraryBase.php deleted file mode 100644 index 21bb4a33f..000000000 --- a/src/SPC/builder/macos/library/MacOSLibraryBase.php +++ /dev/null @@ -1,33 +0,0 @@ -builder; - } - - public function getFrameworks(): array - { - return Config::getLib(static::NAME, 'frameworks', []); - } -} diff --git a/src/SPC/builder/macos/library/brotli.php b/src/SPC/builder/macos/library/brotli.php deleted file mode 100644 index 1061d9136..000000000 --- a/src/SPC/builder/macos/library/brotli.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class brotli extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\brotli; - - public const NAME = 'brotli'; -} diff --git a/src/SPC/builder/macos/library/bzip2.php b/src/SPC/builder/macos/library/bzip2.php deleted file mode 100644 index 7719770c2..000000000 --- a/src/SPC/builder/macos/library/bzip2.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class bzip2 extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\bzip2; - - public const NAME = 'bzip2'; -} diff --git a/src/SPC/builder/macos/library/curl.php b/src/SPC/builder/macos/library/curl.php deleted file mode 100644 index 2de97f320..000000000 --- a/src/SPC/builder/macos/library/curl.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -use SPC\store\FileSystem; - -class curl extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\curl; - - public const NAME = 'curl'; - - public function patchBeforeBuild(): bool - { - FileSystem::replaceFileRegex( - SOURCE_PATH . '/curl/CMakeLists.txt', - '/NOT COREFOUNDATION_FRAMEWORK/m', - 'FALSE' - ); - FileSystem::replaceFileRegex( - SOURCE_PATH . '/curl/CMakeLists.txt', - '/NOT SYSTEMCONFIGURATION_FRAMEWORK/m', - 'FALSE' - ); - FileSystem::replaceFileRegex( - SOURCE_PATH . '/curl/CMakeLists.txt', - '/NOT CORESERVICES_FRAMEWORK/m', - 'FALSE' - ); - return true; - } -} diff --git a/src/SPC/builder/macos/library/fastlz.php b/src/SPC/builder/macos/library/fastlz.php deleted file mode 100644 index db0f517a4..000000000 --- a/src/SPC/builder/macos/library/fastlz.php +++ /dev/null @@ -1,12 +0,0 @@ -setBuildDir("{$this->source_dir}/vendor/glfw") - ->setReset(false) - ->addConfigureArgs( - '-DGLFW_BUILD_EXAMPLES=OFF', - '-DGLFW_BUILD_TESTS=OFF', - ) - ->build('.'); - // patch pkgconf - $this->patchPkgconfPrefix(['glfw3.pc']); - } -} diff --git a/src/SPC/builder/macos/library/gmp.php b/src/SPC/builder/macos/library/gmp.php deleted file mode 100644 index f75d0bbd1..000000000 --- a/src/SPC/builder/macos/library/gmp.php +++ /dev/null @@ -1,15 +0,0 @@ -cd($this->source_dir . '/source') - ->exec("./runConfigureICU MacOSX --enable-static --disable-shared --disable-extras --disable-samples --disable-tests --prefix={$root}") - ->exec('make clean') - ->exec("make -j{$this->builder->concurrency}") - ->exec('make install'); - - $this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX); - FileSystem::removeDir(BUILD_LIB_PATH . '/icu'); - } -} diff --git a/src/SPC/builder/macos/library/idn2.php b/src/SPC/builder/macos/library/idn2.php deleted file mode 100644 index a41eeeb15..000000000 --- a/src/SPC/builder/macos/library/idn2.php +++ /dev/null @@ -1,12 +0,0 @@ -source_dir); - // FileSystem::replaceFileStr($this->source_dir . '/Makefile', '-DMAC_OSX_KLUDGE=1', ''); - FileSystem::replaceFileStr($this->source_dir . '/src/osdep/unix/Makefile', 'CC=cc', "CC={$cc}"); - /* FileSystem::replaceFileStr($this->source_dir . '/src/osdep/unix/Makefile', '-lcrypto -lz', '-lcrypto'); - FileSystem::replaceFileStr($this->source_dir . '/src/osdep/unix/Makefile', '-lcrypto', '-lcrypto -lz'); - FileSystem::replaceFileStr( - $this->source_dir . '/src/osdep/unix/ssl_unix.c', - "#include \n#include ", - "#include \n#include " - ); - // SourcePatcher::patchFile('1006_openssl1.1_autoverify.patch', $this->source_dir); - SourcePatcher::patchFile('2014_openssl1.1.1_sni.patch', $this->source_dir); */ - FileSystem::replaceFileStr($this->source_dir . '/Makefile', 'SSLINCLUDE=/usr/include/openssl', 'SSLINCLUDE=' . BUILD_INCLUDE_PATH); - FileSystem::replaceFileStr($this->source_dir . '/Makefile', 'SSLLIB=/usr/lib', 'SSLLIB=' . BUILD_LIB_PATH); - return true; - } - - protected function build(): void - { - if ($this->builder->getLib('openssl')) { - $ssl_options = 'SPECIALAUTHENTICATORS=ssl SSLTYPE=unix.nopwd SSLINCLUDE=' . BUILD_INCLUDE_PATH . ' SSLLIB=' . BUILD_LIB_PATH; - } else { - $ssl_options = 'SSLTYPE=none'; - } - $out = shell()->execWithResult('echo "-include $(xcrun --show-sdk-path)/usr/include/poll.h -include $(xcrun --show-sdk-path)/usr/include/time.h -include $(xcrun --show-sdk-path)/usr/include/utime.h"')[1][0]; - shell()->cd($this->source_dir) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec( - "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" - ); - try { - shell() - ->exec("cp -rf {$this->source_dir}/c-client/c-client.a " . BUILD_LIB_PATH . '/libc-client.a') - ->exec("cp -rf {$this->source_dir}/c-client/*.c " . BUILD_LIB_PATH . '/') - ->exec("cp -rf {$this->source_dir}/c-client/*.h " . BUILD_INCLUDE_PATH . '/') - ->exec("cp -rf {$this->source_dir}/src/osdep/unix/*.h " . BUILD_INCLUDE_PATH . '/'); - } catch (\Throwable) { - // last command throws an exception, no idea why since it works - } - } -} diff --git a/src/SPC/builder/macos/library/jbig.php b/src/SPC/builder/macos/library/jbig.php deleted file mode 100644 index 10b649742..000000000 --- a/src/SPC/builder/macos/library/jbig.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class libavif extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\libavif; - - public const NAME = 'libavif'; -} diff --git a/src/SPC/builder/macos/library/libcares.php b/src/SPC/builder/macos/library/libcares.php deleted file mode 100644 index 6eb92ec00..000000000 --- a/src/SPC/builder/macos/library/libcares.php +++ /dev/null @@ -1,12 +0,0 @@ -configure( - "--host={$arch}-apple-darwin", - "--target={$arch}-apple-darwin", - ) - ->make(); - $this->patchPkgconfPrefix(['libffi.pc']); - } -} diff --git a/src/SPC/builder/macos/library/libheif.php b/src/SPC/builder/macos/library/libheif.php deleted file mode 100644 index af99740b7..000000000 --- a/src/SPC/builder/macos/library/libheif.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class libiconv extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\libiconv; - - public const NAME = 'libiconv'; -} diff --git a/src/SPC/builder/macos/library/libjpeg.php b/src/SPC/builder/macos/library/libjpeg.php deleted file mode 100644 index 6b86baae3..000000000 --- a/src/SPC/builder/macos/library/libjpeg.php +++ /dev/null @@ -1,12 +0,0 @@ -build(); - } -} diff --git a/src/SPC/builder/macos/library/libpng.php b/src/SPC/builder/macos/library/libpng.php deleted file mode 100644 index 91873bd4a..000000000 --- a/src/SPC/builder/macos/library/libpng.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -use SPC\util\executor\UnixAutoconfExecutor; - -class libpng extends MacOSLibraryBase -{ - public const NAME = 'libpng'; - - protected function build(): void - { - $arch = arch2gnu(php_uname('m')); - UnixAutoconfExecutor::create($this) - ->exec('chmod +x ./configure') - ->exec('chmod +x ./install-sh') - ->appendEnv(['LDFLAGS' => "-L{$this->getLibDir()}"]) - ->configure( - "--host={$arch}-apple-darwin", - '--enable-hardware-optimizations', - "--with-zlib-prefix={$this->getBuildRootPath()}", - match (getenv('SPC_ARCH')) { - 'x86_64' => '--enable-intel-sse', - 'aarch64' => '--enable-arm-neon', - default => '', - } - ) - ->make('libpng16.la', 'install-libLTLIBRARIES install-data-am', after_env_vars: ['DEFAULT_INCLUDES' => "-I{$this->source_dir} -I{$this->getIncludeDir()}"]); - - shell()->cd(BUILD_LIB_PATH)->exec('ln -sf libpng16.a libpng.a'); - $this->patchPkgconfPrefix(['libpng16.pc'], PKGCONF_PATCH_PREFIX); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/macos/library/librabbitmq.php b/src/SPC/builder/macos/library/librabbitmq.php deleted file mode 100644 index aba46aa5d..000000000 --- a/src/SPC/builder/macos/library/librabbitmq.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class libwebp extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\libwebp; - - public const NAME = 'libwebp'; -} diff --git a/src/SPC/builder/macos/library/libxml2.php b/src/SPC/builder/macos/library/libxml2.php deleted file mode 100644 index 51fd031eb..000000000 --- a/src/SPC/builder/macos/library/libxml2.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class nghttp2 extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\nghttp2; - - public const NAME = 'nghttp2'; -} diff --git a/src/SPC/builder/macos/library/nghttp3.php b/src/SPC/builder/macos/library/nghttp3.php deleted file mode 100644 index 9feebb01e..000000000 --- a/src/SPC/builder/macos/library/nghttp3.php +++ /dev/null @@ -1,12 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -use SPC\store\FileSystem; - -class openssl extends MacOSLibraryBase -{ - use \SPC\builder\traits\openssl; - - public const NAME = 'openssl'; - - protected function build(): void - { - // lib:zlib - $extra = ''; - $ex_lib = ''; - $zlib = $this->builder->getLib('zlib'); - if ($zlib instanceof MacOSLibraryBase) { - $extra = 'zlib'; - $ex_lib = trim($zlib->getStaticLibFiles() . ' ' . $ex_lib); - } - $arch = getenv('SPC_ARCH'); - - shell()->cd($this->source_dir)->initializeEnv($this) - ->exec( - "./Configure no-shared {$extra} " . - '--prefix=' . BUILD_ROOT_PATH . ' ' . // use prefix=/ - '--libdir=lib ' . - '--openssldir=/etc/ssl ' . - "darwin64-{$arch}-cc" - ) - ->exec('make clean') - ->exec("make -j{$this->builder->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") - ->exec('make install_sw'); - $this->patchPkgconfPrefix(['libssl.pc', 'openssl.pc', 'libcrypto.pc']); - // patch for openssl 3.3.0+ - if (!str_contains($file = FileSystem::readFile(BUILD_LIB_PATH . '/pkgconfig/libssl.pc'), 'prefix=')) { - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/libssl.pc', 'prefix=' . BUILD_ROOT_PATH . "\n" . $file); - } - if (!str_contains($file = FileSystem::readFile(BUILD_LIB_PATH . '/pkgconfig/openssl.pc'), 'prefix=')) { - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/openssl.pc', 'prefix=' . BUILD_ROOT_PATH . "\n" . $file); - } - if (!str_contains($file = FileSystem::readFile(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc'), 'prefix=')) { - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc', 'prefix=' . BUILD_ROOT_PATH . "\n" . $file); - } - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/pkgconfig/libcrypto.pc', '/Libs.private:.*/m', 'Requires.private: zlib'); - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '/cmake/OpenSSL/OpenSSLConfig.cmake', '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")'); - } -} diff --git a/src/SPC/builder/macos/library/pkgconfig.php b/src/SPC/builder/macos/library/pkgconfig.php deleted file mode 100644 index bb74dfaaf..000000000 --- a/src/SPC/builder/macos/library/pkgconfig.php +++ /dev/null @@ -1,15 +0,0 @@ - - * - * lwmbs is licensed under Mulan PSL v2. You can use this - * software according to the terms and conditions of the - * Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: - * - * http://license.coscl.org.cn/MulanPSL2 - * - * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, - * WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, - * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. - * - * See the Mulan PSL v2 for more details. - */ - -declare(strict_types=1); - -namespace SPC\builder\macos\library; - -class zlib extends MacOSLibraryBase -{ - use \SPC\builder\unix\library\zlib; - - public const NAME = 'zlib'; -} diff --git a/src/SPC/builder/macos/library/zstd.php b/src/SPC/builder/macos/library/zstd.php deleted file mode 100644 index 251d6e4f1..000000000 --- a/src/SPC/builder/macos/library/zstd.php +++ /dev/null @@ -1,12 +0,0 @@ -getDependencies(recursive: true))); - $config = new SPCConfigUtil($this->builder, options: ['libs_only_deps' => true, 'absolute_libs' => true]); - $res = $config->config(libraries: array_map(fn ($x) => $x->getName(), $libs)); - return $res['libs']; - } - - /** - * Patch pkgconfig file prefix, exec_prefix, libdir, includedir for correct build. - * - * @param array $files File list to patch, if empty, will use pkg-configs from config (e.g. ['zlib.pc', 'openssl.pc']) - * @param int $patch_option Patch options - * @param null|array $custom_replace Custom replace rules, if provided, will be used to replace in the format [regex, replacement] - */ - public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCONF_PATCH_ALL, ?array $custom_replace = null): void - { - logger()->info('Patching library [' . static::NAME . '] pkgconfig'); - if ($files === [] && ($conf_pc = Config::getLib(static::NAME, 'pkg-configs', [])) !== []) { - $files = array_map(fn ($x) => "{$x}.pc", $conf_pc); - } - foreach ($files as $name) { - $realpath = realpath(BUILD_LIB_PATH . '/pkgconfig/' . $name); - if ($realpath === false) { - throw new PatchException('pkg-config prefix patcher', 'Cannot find library [' . static::NAME . '] pkgconfig file [' . $name . '] in ' . BUILD_LIB_PATH . '/pkgconfig/ !'); - } - logger()->debug('Patching ' . $realpath); - // replace prefix - $file = FileSystem::readFile($realpath); - $file = ($patch_option & PKGCONF_PATCH_PREFIX) === PKGCONF_PATCH_PREFIX ? preg_replace('/^prefix\s*=.*$/m', 'prefix=' . BUILD_ROOT_PATH, $file) : $file; - $file = ($patch_option & PKGCONF_PATCH_EXEC_PREFIX) === PKGCONF_PATCH_EXEC_PREFIX ? preg_replace('/^exec_prefix\s*=.*$/m', 'exec_prefix=${prefix}', $file) : $file; - $file = ($patch_option & PKGCONF_PATCH_LIBDIR) === PKGCONF_PATCH_LIBDIR ? preg_replace('/^libdir\s*=.*$/m', 'libdir=${prefix}/lib', $file) : $file; - $file = ($patch_option & PKGCONF_PATCH_INCLUDEDIR) === PKGCONF_PATCH_INCLUDEDIR ? preg_replace('/^includedir\s*=.*$/m', 'includedir=${prefix}/include', $file) : $file; - $file = ($patch_option & PKGCONF_PATCH_CUSTOM) === PKGCONF_PATCH_CUSTOM && $custom_replace !== null ? preg_replace($custom_replace[0], $custom_replace[1], $file) : $file; - FileSystem::writeFile($realpath, $file); - } - } - - public function patchLaDependencyPrefix(?array $files = null): void - { - logger()->info('Patching library [' . static::NAME . '] la files'); - $throwOnMissing = true; - if ($files === null) { - $files = $this->getStaticLibs(); - $files = array_map(fn ($name) => str_replace('.a', '.la', $name), $files); - $throwOnMissing = false; - } - foreach ($files as $name) { - $realpath = realpath(BUILD_LIB_PATH . '/' . $name); - if ($realpath === false) { - if ($throwOnMissing) { - throw new PatchException('la dependency patcher', 'Cannot find library [' . static::NAME . '] la file [' . $name . '] !'); - } - logger()->warning('Cannot find library [' . static::NAME . '] la file [' . $name . '] !'); - continue; - } - logger()->debug('Patching ' . $realpath); - // replace prefix - $file = FileSystem::readFile($realpath); - $file = str_replace( - ' /lib/', - ' ' . BUILD_LIB_PATH . '/', - $file - ); - $file = preg_replace('/^libdir=.*$/m', "libdir='" . BUILD_LIB_PATH . "'", $file); - FileSystem::writeFile($realpath, $file); - } - } - - public function getLibExtraCFlags(): string - { - $env = getenv($this->getSnakeCaseName() . '_CFLAGS') ?: ''; - if (!str_contains($env, $this->builder->arch_c_flags)) { - $env .= ' ' . $this->builder->arch_c_flags; - } - return trim($env); - } - - public function getLibExtraCXXFlags(): string - { - $env = getenv($this->getSnakeCaseName() . '_CXXFLAGS') ?: ''; - if (!str_contains($env, $this->builder->arch_cxx_flags)) { - $env .= ' ' . $this->builder->arch_cxx_flags; - } - return trim($env); - } - - public function getLibExtraLdFlags(): string - { - $env = getenv($this->getSnakeCaseName() . '_LDFLAGS') ?: ''; - if (!str_contains($env, $this->builder->arch_ld_flags)) { - $env .= ' ' . $this->builder->arch_ld_flags; - } - return trim($env); - } - - public function getLibExtraLibs(): string - { - return getenv($this->getSnakeCaseName() . '_LIBS') ?: ''; - } -} diff --git a/src/SPC/builder/traits/UnixSystemUtilTrait.php b/src/SPC/builder/traits/UnixSystemUtilTrait.php deleted file mode 100644 index 33f824f36..000000000 --- a/src/SPC/builder/traits/UnixSystemUtilTrait.php +++ /dev/null @@ -1,127 +0,0 @@ -execWithResult($cmd); - if ($result[0] !== 0) { - throw new ExecutionException($cmd, 'Failed to get defined symbols from ' . $lib_file); - } - // parse shell output and filter - $defined = []; - foreach ($result[1] as $line) { - $line = trim($line); - if ($line === '' || str_ends_with($line, '.o:') || str_ends_with($line, '.o]:')) { - continue; - } - $name = strtok($line, " \t"); - if (!$name) { - continue; - } - $name = preg_replace('/@.*$/', '', $name); - if ($name !== '' && $name !== false) { - $defined[] = $name; - } - } - $defined = array_unique($defined); - sort($defined); - // export - if (SPCTarget::getTargetOS() === 'Linux') { - file_put_contents("{$lib_file}.dynsym", "{\n" . implode("\n", array_map(fn ($x) => " {$x};", $defined)) . "};\n"); - } else { - file_put_contents("{$lib_file}.dynsym", implode("\n", $defined) . "\n"); - } - } - - /** - * Get linker flag to export dynamic symbols from a static library. - * - * @param string $lib_file Static library file path (e.g. /path/to/libxxx.a) - * @return null|string Linker flag to export dynamic symbols, null if no .dynsym file found - */ - public static function getDynamicExportedSymbols(string $lib_file): ?string - { - $symbol_file = "{$lib_file}.dynsym"; - if (!is_file($symbol_file)) { - self::exportDynamicSymbols($lib_file); - } - if (!is_file($symbol_file)) { - throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); - } - // https://github.com/ziglang/zig/issues/24662 - if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { - return '-Wl,--export-dynamic'; // needs release 0.16, can be removed then - } - // macOS/zig - if (SPCTarget::getTargetOS() !== 'Linux' || ToolchainManager::getToolchainClass() === ZigToolchain::class) { - return "-Wl,-exported_symbols_list,{$symbol_file}"; - } - return "-Wl,--dynamic-list={$symbol_file}"; - } - - /** - * Find a command in given paths or system PATH. - * If $name is an absolute path, check if it exists. - * - * @param string $name Command name or absolute path - * @param array $paths Paths to search, if empty, use system PATH - * @return null|string Absolute path of the command if found, null otherwise - */ - public static function findCommand(string $name, array $paths = []): ?string - { - if (!$paths) { - $paths = explode(PATH_SEPARATOR, getenv('PATH')); - } - if (str_starts_with($name, '/')) { - return file_exists($name) ? $name : null; - } - foreach ($paths as $path) { - if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { - return $path . DIRECTORY_SEPARATOR . $name; - } - } - return null; - } - - /** - * Make environment variable string for shell command. - * - * @param array $vars Variables, like: ["CFLAGS" => "-Ixxx"] - * @return string like: CFLAGS="-Ixxx" - */ - public static function makeEnvVarString(array $vars): string - { - $str = ''; - foreach ($vars as $key => $value) { - if ($str !== '') { - $str .= ' '; - } - $str .= $key . '=' . escapeshellarg($value); - } - return $str; - } -} diff --git a/src/SPC/builder/traits/openssl.php b/src/SPC/builder/traits/openssl.php deleted file mode 100644 index 6ffef33af..000000000 --- a/src/SPC/builder/traits/openssl.php +++ /dev/null @@ -1,38 +0,0 @@ -source_dir}/VERSION.dat")) { - // parse as INI - $version = parse_ini_file("{$this->source_dir}/VERSION.dat"); - if ($version !== false) { - return "{$version['MAJOR']}.{$version['MINOR']}.{$version['PATCH']}"; - } - } - // get openssl version from pkg-config - if (PHP_OS_FAMILY !== 'Windows') { - try { - return PkgConfigUtil::getModuleVersion('openssl'); - } catch (SPCException) { - } - } - // get openssl version from header openssl/opensslv.h - if (file_exists(BUILD_INCLUDE_PATH . '/openssl/opensslv.h')) { - if (preg_match('/OPENSSL_VERSION_STR "(.*)"/', FileSystem::readFile(BUILD_INCLUDE_PATH . '/openssl/opensslv.h'), $match)) { - return $match[1]; - } - } - return null; - } -} diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php deleted file mode 100644 index fd16656ce..000000000 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ /dev/null @@ -1,488 +0,0 @@ -isLibsOnly()) { - $libraries = array_keys($support_lib_list); - $sorted_libraries = DependencyUtil::getLibs($libraries); - } - - // add lib object for builder - foreach ($sorted_libraries as $library) { - if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) { - continue; - } - // if some libs are not supported (but in config "lib.json", throw exception) - if (!isset($support_lib_list[$library])) { - $os = match (PHP_OS_FAMILY) { - 'Linux' => 'Linux', - 'Darwin' => 'macOS', - 'Windows' => 'Windows', - 'BSD' => 'FreeBSD', - default => PHP_OS_FAMILY, - }; - throw new WrongUsageException("library [{$library}] is in the lib.json list but not supported to build on {$os}."); - } - $lib = new ($support_lib_list[$library])($this); - $this->addLib($lib); - } - - // calculate and check dependencies - foreach ($this->libs as $lib) { - $lib->calcDependency(); - } - $this->lib_list = $sorted_libraries; - } - - /** - * Strip unneeded symbols from binary file. - */ - public function stripBinary(string $binary_path): void - { - shell()->exec(match (PHP_OS_FAMILY) { - 'Darwin' => "strip -S {$binary_path}", - 'Linux' => "strip --strip-unneeded {$binary_path}", - default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'), - }); - } - - /** - * Extract debug information from binary file. - * - * @param string $binary_path the path to the binary file, including executables, shared libraries, etc - */ - 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}" . (PHP_OS_FAMILY === 'Darwin' ? '.dwarf' : '.debug'); - if (PHP_OS_FAMILY === 'Darwin') { - shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); - } elseif (PHP_OS_FAMILY === 'Linux') { - if ($eu_strip = SystemUtil::findCommand('eu-strip')) { - shell() - ->exec("{$eu_strip} -f {$debug_file} {$binary_path}") - ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); - } else { - shell() - ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}") - ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); - } - } else { - throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS'); - } - return $debug_file; - } - - /** - * Deploy the binary file from src to dst. - */ - public function deployBinary(string $src, string $dst, bool $executable = true): string - { - logger()->debug('Deploying binary from ' . $src . ' to ' . $dst); - - // file must exists - if (!file_exists($src)) { - throw new SPCInternalException("Deploy failed. Cannot find file: {$src}"); - } - // dst dir must exists - FileSystem::createDir(dirname($dst)); - - // ignore copy to self - if (realpath($src) !== realpath($dst)) { - shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); - } - - // file exist - if (!file_exists($dst)) { - throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); - } - - if (!$this->getOption('no-strip')) { - // extract debug info - $this->extractDebugInfo($dst); - // extra strip - $this->stripBinary($dst); - } - - // UPX for linux - $upx_option = $this->getOption('with-upx-pack'); - if ($upx_option && PHP_OS_FAMILY === 'Linux' && $executable) { - if ($this->getOption('no-strip')) { - logger()->warning('UPX compression is not recommended when --no-strip is enabled.'); - } - logger()->info("Compressing {$dst} with UPX"); - shell()->exec(getenv('UPX_EXEC') . " --best {$dst}"); - } - - return $dst; - } - - /** - * Sanity check after build complete. - */ - protected function sanityCheck(int $build_target): void - { - // sanity check for php-cli - if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { - logger()->info('running cli sanity check'); - [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); - $raw_output = implode('', $output); - if ($ret !== 0 || trim($raw_output) !== 'hello') { - throw new ValidationException("cli failed sanity check. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli sanity check'); - } - - foreach ($this->getExts() as $ext) { - logger()->debug('testing ext: ' . $ext->getName()); - $ext->runCliCheckUnix(); - } - } - - // sanity check for phpmicro - if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - $test_task = $this->getMicroTestTasks(); - foreach ($test_task as $task_name => $task) { - $test_file = SOURCE_PATH . '/' . $task_name . '.exe'; - if (file_exists($test_file)) { - @unlink($test_file); - } - file_put_contents($test_file, file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') . $task['content']); - chmod($test_file, 0755); - [$ret, $out] = shell()->execWithResult($test_file); - foreach ($task['conditions'] as $condition => $closure) { - if (!$closure($ret, $out)) { - $raw_out = trim(implode('', $out)); - throw new ValidationException( - "failure info: {$condition}, code: {$ret}, output: {$raw_out}", - validation_module: "phpmicro sanity check item [{$task_name}]" - ); - } - } - } - } - - // sanity check for php-cgi - if (($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI) { - logger()->info('running cgi sanity check'); - [$ret, $output] = shell()->execWithResult("echo 'Hello, World!\";' | " . BUILD_BIN_PATH . '/php-cgi -n'); - $raw_output = implode('', $output); - if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!') || !str_contains($raw_output, 'text/html')) { - throw new ValidationException("cgi failed sanity check. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi sanity check'); - } - } - - // sanity check for embed - if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) { - logger()->info('running embed sanity check'); - $sample_file_path = SOURCE_PATH . '/embed-test'; - if (!is_dir($sample_file_path)) { - @mkdir($sample_file_path); - } - // copy embed test files - copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); - copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); - $util = new SPCConfigUtil($this); - $config = $util->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); - $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; - if (SPCTarget::isStatic()) { - $lens .= ' -static'; - } - $dynamic_exports = ''; - $embedType = 'static'; - // if someone changed to EMBED_TYPE=shared, we need to add LD_LIBRARY_PATH - if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { - $embedType = 'shared'; - if (PHP_OS_FAMILY === 'Darwin') { - $ext_path = 'DYLD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$DYLD_LIBRARY_PATH '; - } else { - $ext_path = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$LD_LIBRARY_PATH '; - } - FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a'); - } else { - $ext_path = ''; - $suffix = PHP_OS_FAMILY === 'Darwin' ? 'dylib' : 'so'; - foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) { - unlink($file); - } - // calling linux system util in other unix OS is okay - if ($dynamic_exports = LinuxSystemUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { - $dynamic_exports = ' ' . $dynamic_exports; - } - } - $cc = getenv('CC'); - - [$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens} {$dynamic_exports}"); - if ($ret !== 0) { - throw new ValidationException( - 'embed failed to build. Error message: ' . implode("\n", $out), - validation_module: $embedType . ' libphp embed build sanity check' - ); - } - [$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed'); - if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { - throw new ValidationException( - 'embed failed to run. Error message: ' . implode("\n", $output), - validation_module: $embedType . ' libphp embed run sanity check' - ); - } - } - - // sanity check for frankenphp - if (($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { - logger()->info('running frankenphp sanity check'); - $frankenphp = BUILD_BIN_PATH . '/frankenphp'; - if (!file_exists($frankenphp)) { - throw new ValidationException( - "FrankenPHP binary not found: {$frankenphp}", - validation_module: 'FrankenPHP sanity check' - ); - } - $prefix = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_' : 'LD_'; - [$ret, $output] = shell() - ->setEnv(["{$prefix}LIBRARY_PATH" => BUILD_LIB_PATH]) - ->execWithResult("{$frankenphp} version"); - if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { - throw new ValidationException( - 'FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']', - validation_module: 'FrankenPHP sanity check' - ); - } - } - } - - /** - * Deploy binaries that produces executable SAPI - */ - protected function deploySAPIBinary(int $type): string - { - $src = match ($type) { - BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php', - BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx', - BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm', - BUILD_TARGET_CGI => SOURCE_PATH . '/php-src/sapi/cgi/php-cgi', - BUILD_TARGET_FRANKENPHP => BUILD_BIN_PATH . '/frankenphp', - default => throw new SPCInternalException("Deployment does not accept type {$type}"), - }; - $dst = BUILD_BIN_PATH . '/' . basename($src); - return $this->deployBinary($src, $dst); - } - - /** - * Run php clean - */ - protected function cleanMake(): void - { - logger()->info('cleaning up php-src build files'); - shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean'); - } - - /** - * Patch phpize and php-config if needed - */ - protected function patchPhpScripts(): 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#'); - } - // 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); - } - foreach ($this->getLibs() as $lib) { - if ($lib->patchPhpConfig()) { - logger()->debug("Library {$lib->getName()} patched php-config"); - } - } - } - - /** - * Process the --with-frankenphp-app option - * Creates app.tar and app.checksum in source/frankenphp directory - */ - protected function processFrankenphpApp(): void - { - $frankenphpSourceDir = getenv('FRANKENPHP_SOURCE_PATH') ?: SOURCE_PATH . '/frankenphp'; - if (!is_dir($frankenphpSourceDir)) { - SourceManager::initSource(['frankenphp'], ['frankenphp']); - } - $frankenphpAppPath = $this->getOption('with-frankenphp-app'); - - if ($frankenphpAppPath) { - $frankenphpAppPath = trim($frankenphpAppPath, "\"'"); - if (!is_dir($frankenphpAppPath)) { - throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); - } - $appTarPath = $frankenphpSourceDir . '/app.tar'; - logger()->info("Creating app.tar from {$frankenphpAppPath}"); - - shell()->exec('tar -cf ' . escapeshellarg($appTarPath) . ' -C ' . escapeshellarg($frankenphpAppPath) . ' .'); - - $checksum = hash_file('md5', $appTarPath); - file_put_contents($frankenphpSourceDir . '/app_checksum.txt', $checksum); - } else { - FileSystem::removeFileIfExists($frankenphpSourceDir . '/app.tar'); - FileSystem::removeFileIfExists($frankenphpSourceDir . '/app_checksum.txt'); - file_put_contents($frankenphpSourceDir . '/app.tar', ''); - file_put_contents($frankenphpSourceDir . '/app_checksum.txt', ''); - } - } - - protected function getFrankenPHPVersion(): string - { - if ($version = getenv('FRANKENPHP_VERSION')) { - return $version; - } - $frankenphpSourceDir = getenv('FRANKENPHP_SOURCE_PATH') ?: SOURCE_PATH . '/frankenphp'; - $goModPath = $frankenphpSourceDir . '/caddy/go.mod'; - - if (!file_exists($goModPath)) { - throw new SPCInternalException("FrankenPHP caddy/go.mod file not found at {$goModPath}, why did we not download FrankenPHP?"); - } - - $content = file_get_contents($goModPath); - if (preg_match('/github\.com\/dunglas\/frankenphp\s+v?(\d+\.\d+\.\d+)/', $content, $matches)) { - return $matches[1]; - } - - throw new SPCInternalException('Could not find FrankenPHP version in caddy/go.mod'); - } - - protected function buildFrankenphp(): void - { - GlobalEnvManager::addPathIfNotExists(GoXcaddy::getPath()); - $this->processFrankenphpApp(); - $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; - $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; - $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); - $frankenphpSourceDir = getenv('FRANKENPHP_SOURCE_PATH') ?: SOURCE_PATH . '/frankenphp'; - - $xcaddyModules = preg_replace('#--with github.com/dunglas/frankenphp\S*#', '', $xcaddyModules); - $xcaddyModules = "--with github.com/dunglas/frankenphp={$frankenphpSourceDir} " . - "--with github.com/dunglas/frankenphp/caddy={$frankenphpSourceDir}/caddy {$xcaddyModules}"; - if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { - logger()->warning('caddy-cbrotli module is enabled, but brotli library is not built. Disabling caddy-cbrotli.'); - $xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules); - } - - $frankenPhpVersion = $this->getFrankenPHPVersion(); - $libphpVersion = $this->getPHPVersion(); - $dynamic_exports = ''; - if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { - $libphpVersion = preg_replace('/\.\d+$/', '', $libphpVersion); - } else { - if ($dynamicSymbolsArgument = LinuxSystemUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { - $dynamic_exports = ' ' . $dynamicSymbolsArgument; - } - } - $extLdFlags = "-extldflags '-pie{$dynamic_exports} {$this->arch_ld_flags}'"; - $muslTags = ''; - $staticFlags = ''; - if (SPCTarget::isStatic()) { - $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$this->arch_ld_flags}'"; - $muslTags = 'static_build,'; - $staticFlags = '-static-pie'; - } - - $config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list); - $cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -DFRANKENPHP_VERSION=' . $frankenPhpVersion; - $libs = $config['libs']; - // Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix. - if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') || - str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) && - ToolchainManager::getToolchainClass() === GccNativeToolchain::class) { - $cflags .= ' -Wno-error=missing-profile'; - $libs .= ' -lgcov'; - } - $env = [...[ - 'CGO_ENABLED' => '1', - 'CGO_CFLAGS' => clean_spaces($cflags), - 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", - 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . - '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . - '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . - '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp\' ' . - '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . - "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . - "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", - 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, - ], ...GoXcaddy::getEnvironment()]; - shell()->cd(BUILD_BIN_PATH) - ->setEnv($env) - ->exec("xcaddy build --output frankenphp {$xcaddyModules}"); - - $this->deploySAPIBinary(BUILD_TARGET_FRANKENPHP); - } - - /** - * Seek php-src/config.log when building PHP, add it to exception. - */ - protected function seekPhpSrcLogFileOnException(callable $callback): void - { - try { - $callback(); - } catch (SPCException $e) { - if (file_exists(SOURCE_PATH . '/php-src/config.log')) { - $e->addExtraLogFile('php-src config.log', 'php-src.config.log'); - copy(SOURCE_PATH . '/php-src/config.log', SPC_LOGS_DIR . '/php-src.config.log'); - } - throw $e; - } - } -} diff --git a/src/SPC/builder/unix/library/attr.php b/src/SPC/builder/unix/library/attr.php deleted file mode 100644 index 67ac5feb6..000000000 --- a/src/SPC/builder/unix/library/attr.php +++ /dev/null @@ -1,23 +0,0 @@ -appendEnv([ - 'CFLAGS' => '-Wno-int-conversion -Wno-implicit-function-declaration', - ]) - ->exec('libtoolize --force --copy') - ->exec('./autogen.sh || autoreconf -if') - ->configure('--disable-nls') - ->make('install-attributes_h install-data install-libattr_h install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfDATA', with_install: false); - $this->patchPkgconfPrefix(['libattr.pc'], PKGCONF_PATCH_PREFIX); - } -} diff --git a/src/SPC/builder/unix/library/brotli.php b/src/SPC/builder/unix/library/brotli.php deleted file mode 100644 index 64331a56f..000000000 --- a/src/SPC/builder/unix/library/brotli.php +++ /dev/null @@ -1,33 +0,0 @@ -setBuildDir("{$this->getSourceDir()}/build-dir") - ->addConfigureArgs("-DSHARE_INSTALL_PREFIX={$this->getBuildRootPath()}") - ->build(); - - $this->patchPkgconfPrefix(['libbrotlicommon.pc', 'libbrotlidec.pc', 'libbrotlienc.pc']); - FileSystem::replaceFileLineContainsString(BUILD_LIB_PATH . '/pkgconfig/libbrotlidec.pc', 'Libs: -L${libdir} -lbrotlidec', 'Libs: -L${libdir} -lbrotlidec -lbrotlicommon'); - FileSystem::replaceFileLineContainsString(BUILD_LIB_PATH . '/pkgconfig/libbrotlienc.pc', 'Libs: -L${libdir} -lbrotlienc', 'Libs: -L${libdir} -lbrotlienc -lbrotlicommon'); - shell()->cd(BUILD_ROOT_PATH . '/lib')->exec('ln -sf libbrotlicommon.a libbrotli.a'); - foreach (FileSystem::scanDirFiles(BUILD_ROOT_PATH . '/lib/', false, true) as $filename) { - if (str_starts_with($filename, 'libbrotli') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { - unlink(BUILD_ROOT_PATH . '/lib/' . $filename); - } - } - - if (file_exists(BUILD_BIN_PATH . '/brotli')) { - unlink(BUILD_BIN_PATH . '/brotli'); - } - } -} diff --git a/src/SPC/builder/unix/library/bzip2.php b/src/SPC/builder/unix/library/bzip2.php deleted file mode 100644 index 32d87270c..000000000 --- a/src/SPC/builder/unix/library/bzip2.php +++ /dev/null @@ -1,25 +0,0 @@ -source_dir . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); - return true; - } - - protected function build(): void - { - shell()->cd($this->source_dir)->initializeEnv($this) - ->exec("make PREFIX='" . BUILD_ROOT_PATH . "' clean") - ->exec("make -j{$this->builder->concurrency} {$this->builder->getEnvString()} PREFIX='" . BUILD_ROOT_PATH . "' libbz2.a") - ->exec('cp libbz2.a ' . BUILD_LIB_PATH) - ->exec('cp bzlib.h ' . BUILD_INCLUDE_PATH); - } -} diff --git a/src/SPC/builder/unix/library/curl.php b/src/SPC/builder/unix/library/curl.php deleted file mode 100644 index caa97b8c6..000000000 --- a/src/SPC/builder/unix/library/curl.php +++ /dev/null @@ -1,40 +0,0 @@ -cd($this->source_dir)->exec('sed -i.save s@\${CMAKE_C_IMPLICIT_LINK_LIBRARIES}@@ ./CMakeLists.txt'); - - UnixCMakeExecutor::create($this) - ->optionalLib('openssl', '-DCURL_USE_OPENSSL=ON -DCURL_CA_BUNDLE=OFF -DCURL_CA_PATH=OFF -DCURL_CA_FALLBACK=ON', '-DCURL_USE_OPENSSL=OFF -DCURL_ENABLE_SSL=OFF') - ->optionalLib('brotli', ...cmake_boolean_args('CURL_BROTLI')) - ->optionalLib('libssh2', ...cmake_boolean_args('CURL_USE_LIBSSH2')) - ->optionalLib('nghttp2', ...cmake_boolean_args('USE_NGHTTP2')) - ->optionalLib('nghttp3', ...cmake_boolean_args('USE_NGHTTP3')) - ->optionalLib('ngtcp2', ...cmake_boolean_args('USE_NGTCP2')) - ->optionalLib('ldap', ...cmake_boolean_args('CURL_DISABLE_LDAP', true)) - ->optionalLib('zstd', ...cmake_boolean_args('CURL_ZSTD')) - ->optionalLib('idn2', ...cmake_boolean_args('USE_LIBIDN2')) - ->optionalLib('psl', ...cmake_boolean_args('CURL_USE_LIBPSL')) - ->optionalLib('krb5', ...cmake_boolean_args('CURL_USE_GSSAPI')) - ->optionalLib('idn2', ...cmake_boolean_args('CURL_USE_IDN2')) - ->optionalLib('libcares', '-DENABLE_ARES=ON') - ->addConfigureArgs( - '-DBUILD_CURL_EXE=OFF', - '-DBUILD_LIBCURL_DOCS=OFF', - ) - ->build(); - - // patch pkgconf - $this->patchPkgconfPrefix(['libcurl.pc']); - shell()->cd(BUILD_LIB_PATH . '/cmake/CURL/') - ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"" . BUILD_LIB_PATH . "/libcurl.a\"|g' CURLTargets-release.cmake"); - } -} diff --git a/src/SPC/builder/unix/library/fastlz.php b/src/SPC/builder/unix/library/fastlz.php deleted file mode 100644 index 494664754..000000000 --- a/src/SPC/builder/unix/library/fastlz.php +++ /dev/null @@ -1,24 +0,0 @@ -cd($this->source_dir)->initializeEnv($this) - ->exec((getenv('CC') ?: 'cc') . ' -c -O3 -fPIC fastlz.c -o fastlz.o') - ->exec((getenv('AR') ?: 'ar') . ' rcs libfastlz.a fastlz.o'); - - if (!copy($this->source_dir . '/fastlz.h', BUILD_INCLUDE_PATH . '/fastlz.h')) { - throw new BuildFailureException('Failed to copy fastlz.h, file does not exist'); - } - if (!copy($this->source_dir . '/libfastlz.a', BUILD_LIB_PATH . '/libfastlz.a')) { - throw new BuildFailureException('Failed to copy libfastlz.a, file does not exist'); - } - } -} diff --git a/src/SPC/builder/unix/library/freetype.php b/src/SPC/builder/unix/library/freetype.php deleted file mode 100644 index 57c1ac05c..000000000 --- a/src/SPC/builder/unix/library/freetype.php +++ /dev/null @@ -1,34 +0,0 @@ -optionalLib('libpng', ...cmake_boolean_args('FT_DISABLE_PNG', true)) - ->addConfigureArgs('-DFT_DISABLE_BZIP2=ON') - ->addConfigureArgs('-DFT_DISABLE_BROTLI=ON') - ->addConfigureArgs('-DFT_DISABLE_HARFBUZZ=ON'); - - // fix cmake 4.0 compatibility - if (version_compare(get_cmake_version(), '4.0.0', '>=')) { - $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.12'); - } - - $cmake->build(); - - $this->patchPkgconfPrefix(['freetype2.pc']); - FileSystem::replaceFileStr( - BUILD_ROOT_PATH . '/lib/pkgconfig/freetype2.pc', - ' -L/lib ', - ' -L' . BUILD_ROOT_PATH . '/lib ' - ); - } -} diff --git a/src/SPC/builder/unix/library/gettext.php b/src/SPC/builder/unix/library/gettext.php deleted file mode 100644 index d383faf77..000000000 --- a/src/SPC/builder/unix/library/gettext.php +++ /dev/null @@ -1,41 +0,0 @@ -optionalLib('ncurses', "--with-libncurses-prefix={$this->getBuildRootPath()}") - ->optionalLib('libxml2', "--with-libxml2-prefix={$this->getBuildRootPath()}") - ->addConfigureArgs( - '--disable-java', - '--disable-c++', - '--disable-d', - '--disable-rpath', - '--disable-modula2', - '--disable-libasprintf', - '--with-included-libintl', - "--with-iconv-prefix={$this->getBuildRootPath()}", - ); - - // zts - if ($this->builder->getOption('enable-zts')) { - $autoconf->addConfigureArgs('--enable-threads=isoc+posix') - ->appendEnv([ - 'CFLAGS' => '-lpthread -D_REENTRANT', - 'LDFLGAS' => '-lpthread', - ]); - } else { - $autoconf->addConfigureArgs('--disable-threads'); - } - - $autoconf->configure()->make(dir: $this->getSourceDir() . '/gettext-runtime/intl'); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/gmp.php b/src/SPC/builder/unix/library/gmp.php deleted file mode 100644 index 97a88ba1c..000000000 --- a/src/SPC/builder/unix/library/gmp.php +++ /dev/null @@ -1,23 +0,0 @@ -appendEnv([ - 'CFLAGS' => '-std=c17', - ]) - ->configure( - '--enable-fat' - ) - ->make(); - $this->patchPkgconfPrefix(['gmp.pc']); - } -} diff --git a/src/SPC/builder/unix/library/gmssl.php b/src/SPC/builder/unix/library/gmssl.php deleted file mode 100644 index 89fc5207e..000000000 --- a/src/SPC/builder/unix/library/gmssl.php +++ /dev/null @@ -1,15 +0,0 @@ -build(); - } -} diff --git a/src/SPC/builder/unix/library/grpc.php b/src/SPC/builder/unix/library/grpc.php deleted file mode 100644 index f8fed3d95..000000000 --- a/src/SPC/builder/unix/library/grpc.php +++ /dev/null @@ -1,58 +0,0 @@ -source_dir . '/third_party/re2/util/pcre.h', - ["#define UTIL_PCRE_H_\n#include ", '#define UTIL_PCRE_H_'], - ['#define UTIL_PCRE_H_', "#define UTIL_PCRE_H_\n#include "], - ); - return true; - } - - protected function build(): void - { - $cmake = UnixCMakeExecutor::create($this) - ->setBuildDir("{$this->source_dir}/avoid_BUILD_file_conflict") - ->addConfigureArgs( - '-DgRPC_INSTALL_BINDIR=' . BUILD_BIN_PATH, - '-DgRPC_INSTALL_LIBDIR=' . BUILD_LIB_PATH, - '-DgRPC_INSTALL_SHAREDIR=' . BUILD_ROOT_PATH . '/share/grpc', - '-DCMAKE_C_FLAGS="-DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK -L' . BUILD_LIB_PATH . ' -I' . BUILD_INCLUDE_PATH . '"', - '-DCMAKE_CXX_FLAGS="-DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK -L' . BUILD_LIB_PATH . ' -I' . BUILD_INCLUDE_PATH . '"', - '-DgRPC_BUILD_CODEGEN=OFF', - '-DgRPC_DOWNLOAD_ARCHIVES=OFF', - '-DgRPC_BUILD_TESTS=OFF', - // providers - '-DgRPC_ZLIB_PROVIDER=package', - '-DgRPC_CARES_PROVIDER=package', - '-DgRPC_SSL_PROVIDER=package', - ); - - if (PHP_OS_FAMILY === 'Linux' && SPCTarget::isStatic() && !SystemUtil::isMuslDist()) { - $cmake->addConfigureArgs( - '-DCMAKE_EXE_LINKER_FLAGS="-static-libgcc -static-libstdc++"', - '-DCMAKE_SHARED_LINKER_FLAGS="-static-libgcc -static-libstdc++"', - '-DCMAKE_CXX_STANDARD_LIBRARIES="-static-libgcc -static-libstdc++"', - ); - } - - $cmake->build(); - - $re2Content = file_get_contents($this->source_dir . '/third_party/re2/re2.pc'); - $re2Content = 'prefix=' . BUILD_ROOT_PATH . "\nexec_prefix=\${prefix}\n" . $re2Content; - file_put_contents(BUILD_LIB_PATH . '/pkgconfig/re2.pc', $re2Content); - $this->patchPkgconfPrefix(['grpc++.pc', 'grpc.pc', 'grpc++_unsecure.pc', 'grpc_unsecure.pc', 're2.pc']); - } -} diff --git a/src/SPC/builder/unix/library/icu.php b/src/SPC/builder/unix/library/icu.php deleted file mode 100644 index 6f2e86118..000000000 --- a/src/SPC/builder/unix/library/icu.php +++ /dev/null @@ -1,24 +0,0 @@ -configure( - '--disable-nls', - '--disable-doc', - '--enable-year2038', - '--disable-rpath' - ) - ->optionalLib('libiconv', "--with-libiconv-prefix={$this->getBuildRootPath()}") - ->optionalLib('libunistring', "--with-libunistring-prefix={$this->getBuildRootPath()}") - ->optionalLib('gettext', "--with-libnintl-prefix={$this->getBuildRootPath()}") - ->make(); - $this->patchPkgconfPrefix(['libidn2.pc']); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/imagemagick.php b/src/SPC/builder/unix/library/imagemagick.php deleted file mode 100644 index 42064e2d8..000000000 --- a/src/SPC/builder/unix/library/imagemagick.php +++ /dev/null @@ -1,75 +0,0 @@ -builder->arch_ld_flags; - if (str_contains($this->builder->arch_ld_flags, '-Wl,--as-needed')) { - $this->builder->arch_ld_flags = str_replace('-Wl,--as-needed', '', $original_ldflags); - } - - $ac = UnixAutoconfExecutor::create($this) - ->optionalLib('libzip', ...ac_with_args('zip')) - ->optionalLib('libjpeg', ...ac_with_args('jpeg')) - ->optionalLib('libpng', ...ac_with_args('png')) - ->optionalLib('libwebp', ...ac_with_args('webp')) - ->optionalLib('libxml2', ...ac_with_args('xml')) - ->optionalLib('libheif', ...ac_with_args('heic')) - ->optionalLib('zlib', ...ac_with_args('zlib')) - ->optionalLib('xz', ...ac_with_args('lzma')) - ->optionalLib('zstd', ...ac_with_args('zstd')) - ->optionalLib('freetype', ...ac_with_args('freetype')) - ->optionalLib('bzip2', ...ac_with_args('bzlib')) - ->optionalLib('libjxl', ...ac_with_args('jxl')) - ->optionalLib('jbig', ...ac_with_args('jbig')) - ->addConfigureArgs( - '--disable-openmp', - '--without-x', - ); - - // special: linux-static target needs `-static` - $ldflags = SPCTarget::isStatic() ? '-static -ldl' : '-ldl'; - - // special: macOS needs -iconv - $libs = SPCTarget::getTargetOS() === 'Darwin' ? '-liconv' : ''; - - $ac->appendEnv([ - 'LDFLAGS' => $ldflags, - 'LIBS' => $libs, - 'PKG_CONFIG' => '$PKG_CONFIG --static', - ]); - - $ac->configure()->make(); - - $this->builder->arch_ld_flags = $original_ldflags; - - $filelist = [ - 'ImageMagick.pc', - 'ImageMagick-7.Q16HDRI.pc', - 'Magick++.pc', - 'Magick++-7.Q16HDRI.pc', - 'MagickCore.pc', - 'MagickCore-7.Q16HDRI.pc', - 'MagickWand.pc', - 'MagickWand-7.Q16HDRI.pc', - ]; - $this->patchPkgconfPrefix($filelist); - foreach ($filelist as $file) { - FileSystem::replaceFileRegex( - BUILD_LIB_PATH . '/pkgconfig/' . $file, - '#includearchdir=/include/ImageMagick-7#m', - 'includearchdir=${prefix}/include/ImageMagick-7' - ); - } - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/jbig.php b/src/SPC/builder/unix/library/jbig.php deleted file mode 100644 index 5b00f0d62..000000000 --- a/src/SPC/builder/unix/library/jbig.php +++ /dev/null @@ -1,27 +0,0 @@ -source_dir . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', 'CFLAGS = -O2 -W -Wno-unused-result -fPIC'); - return true; - } - - protected function build(): void - { - shell()->cd($this->source_dir)->initializeEnv($this) - ->exec("make -j{$this->builder->concurrency} {$this->builder->getEnvString()} lib") - ->exec('cp libjbig/libjbig.a ' . BUILD_LIB_PATH) - ->exec('cp libjbig/libjbig85.a ' . BUILD_LIB_PATH) - ->exec('cp libjbig/jbig.h ' . BUILD_INCLUDE_PATH) - ->exec('cp libjbig/jbig85.h ' . BUILD_INCLUDE_PATH) - ->exec('cp libjbig/jbig_ar.h ' . BUILD_INCLUDE_PATH); - } -} diff --git a/src/SPC/builder/unix/library/krb5.php b/src/SPC/builder/unix/library/krb5.php deleted file mode 100644 index 4cf0ad44e..000000000 --- a/src/SPC/builder/unix/library/krb5.php +++ /dev/null @@ -1,60 +0,0 @@ -source_dir; - $this->source_dir .= '/src'; - shell()->cd($this->source_dir)->exec('ls -lah'); - if (!file_exists($this->source_dir . '/configure')) { - shell()->cd($this->source_dir)->exec('autoreconf -if'); - } - $libs = array_map(fn ($x) => $x->getName(), $this->getDependencies(true)); - $spc = new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]); - $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false)); - $extraEnv = [ - 'CFLAGS' => '-fcommon', - 'LIBS' => $config['libs'], - ]; - if (getenv('SPC_LD_LIBRARY_PATH') && getenv('SPC_LIBRARY_PATH')) { - $extraEnv = [...$extraEnv, ...[ - 'LD_LIBRARY_PATH' => getenv('SPC_LD_LIBRARY_PATH'), - 'LIBRARY_PATH' => getenv('SPC_LIBRARY_PATH'), - ]]; - } - $args = [ - '--disable-nls', - '--disable-rpath', - '--without-system-verto', - ]; - if (PHP_OS_FAMILY === 'Darwin') { - $extraEnv['LDFLAGS'] = '-framework Kerberos'; - $args[] = 'ac_cv_func_secure_getenv=no'; - } - UnixAutoconfExecutor::create($this) - ->appendEnv($extraEnv) - ->optionalLib('ldap', '--with-ldap', '--without-ldap') - ->optionalLib('libedit', '--with-libedit', '--without-libedit') - ->configure(...$args) - ->make(); - $this->patchPkgconfPrefix([ - 'krb5-gssapi.pc', - 'krb5.pc', - 'kadm-server.pc', - 'kadm-client.pc', - 'kdb.pc', - 'mit-krb5-gssapi.pc', - 'mit-krb5.pc', - 'gssrpc.pc', - ]); - $this->source_dir = $origin_source_dir; - } -} diff --git a/src/SPC/builder/unix/library/ldap.php b/src/SPC/builder/unix/library/ldap.php deleted file mode 100644 index 3ff72dcba..000000000 --- a/src/SPC/builder/unix/library/ldap.php +++ /dev/null @@ -1,45 +0,0 @@ -source_dir . '/configure', '"-lssl -lcrypto', '"-lssl -lcrypto -lz ' . $extra); - return true; - } - - protected function build(): void - { - UnixAutoconfExecutor::create($this) - ->optionalLib('openssl', '--with-tls=openssl') - ->optionalLib('gmp', '--with-mp=gmp') - ->optionalLib('libsodium', '--with-argon2=libsodium', '--enable-argon2=no') - ->addConfigureArgs( - '--disable-slapd', - '--without-systemd', - '--without-cyrus-sasl', - 'ac_cv_func_pthread_kill_other_threads_np=no' - ) - ->appendEnv([ - 'CFLAGS' => '-Wno-date-time', - 'LDFLAGS' => "-L{$this->getLibDir()}", - 'CPPFLAGS' => "-I{$this->getIncludeDir()}", - ]) - ->configure() - ->exec('sed -i -e "s/SUBDIRS= include libraries clients servers tests doc/SUBDIRS= include libraries clients servers/g" Makefile') - ->make(); - - FileSystem::replaceFileLineContainsString(BUILD_LIB_PATH . '/pkgconfig/ldap.pc', 'Libs: -L${libdir} -lldap', 'Libs: -L${libdir} -lldap -llber'); - $this->patchPkgconfPrefix(['ldap.pc', 'lber.pc']); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/lerc.php b/src/SPC/builder/unix/library/lerc.php deleted file mode 100644 index 5ad0e572e..000000000 --- a/src/SPC/builder/unix/library/lerc.php +++ /dev/null @@ -1,16 +0,0 @@ -build(); - } -} diff --git a/src/SPC/builder/unix/library/libacl.php b/src/SPC/builder/unix/library/libacl.php deleted file mode 100644 index 8c5d699d3..000000000 --- a/src/SPC/builder/unix/library/libacl.php +++ /dev/null @@ -1,32 +0,0 @@ -exec('libtoolize --force --copy') - ->exec('./autogen.sh || autoreconf -if') - ->configure('--disable-nls', '--disable-tests') - ->make('install-acl_h install-libacl_h install-data install-libLTLIBRARIES install-pkgincludeHEADERS install-sysincludeHEADERS install-pkgconfDATA', with_install: false); - $this->patchPkgconfPrefix(['libacl.pc'], PKGCONF_PATCH_PREFIX); - } -} diff --git a/src/SPC/builder/unix/library/libaom.php b/src/SPC/builder/unix/library/libaom.php deleted file mode 100644 index f2f50d85b..000000000 --- a/src/SPC/builder/unix/library/libaom.php +++ /dev/null @@ -1,27 +0,0 @@ -setBuildDir("{$this->source_dir}/builddir") - ->addConfigureArgs('-DAOM_TARGET_CPU=generic') - ->build(); - f_putenv("SPC_COMPILER_EXTRA={$extra}"); - $this->patchPkgconfPrefix(['aom.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libargon2.php b/src/SPC/builder/unix/library/libargon2.php deleted file mode 100644 index 4c7600957..000000000 --- a/src/SPC/builder/unix/library/libargon2.php +++ /dev/null @@ -1,30 +0,0 @@ -cd($this->source_dir)->initializeEnv($this) - ->exec("make PREFIX='' clean") - ->exec("make -j{$this->builder->concurrency} PREFIX=''") - ->exec("make install PREFIX='' DESTDIR=" . BUILD_ROOT_PATH); - - $this->patchPkgconfPrefix(['libargon2.pc']); - - foreach (FileSystem::scanDirFiles(BUILD_ROOT_PATH . '/lib/', false, true) as $filename) { - if (str_starts_with($filename, 'libargon2') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { - unlink(BUILD_ROOT_PATH . '/lib/' . $filename); - } - } - - if (file_exists(BUILD_BIN_PATH . '/argon2')) { - unlink(BUILD_BIN_PATH . '/argon2'); - } - } -} diff --git a/src/SPC/builder/unix/library/libavif.php b/src/SPC/builder/unix/library/libavif.php deleted file mode 100644 index a5b57aef2..000000000 --- a/src/SPC/builder/unix/library/libavif.php +++ /dev/null @@ -1,24 +0,0 @@ -optionalLib('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF') - ->optionalLib('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF') - ->optionalLib('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF') - ->optionalLib('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF') - ->optionalLib('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF') - ->addConfigureArgs('-DAVIF_LIBYUV=OFF') - ->build(); - // patch pkgconfig - $this->patchPkgconfPrefix(['libavif.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libcares.php b/src/SPC/builder/unix/library/libcares.php deleted file mode 100644 index 90fd9d76c..000000000 --- a/src/SPC/builder/unix/library/libcares.php +++ /dev/null @@ -1,27 +0,0 @@ -source_dir . '/src/lib/thirdparty/apple/dnsinfo.h')) { - FileSystem::createDir($this->source_dir . '/src/lib/thirdparty/apple'); - copy(ROOT_DIR . '/src/globals/extra/libcares_dnsinfo.h', $this->source_dir . '/src/lib/thirdparty/apple/dnsinfo.h'); - return true; - } - return false; - } - - protected function build(): void - { - UnixAutoconfExecutor::create($this)->configure('--disable-tests')->make(); - $this->patchPkgconfPrefix(['libcares.pc'], PKGCONF_PATCH_PREFIX); - } -} diff --git a/src/SPC/builder/unix/library/libde265.php b/src/SPC/builder/unix/library/libde265.php deleted file mode 100644 index 184a44261..000000000 --- a/src/SPC/builder/unix/library/libde265.php +++ /dev/null @@ -1,21 +0,0 @@ -addConfigureArgs( - '-DENABLE_SDL=OFF', - '-DENABLE_DECODER=OFF' - ) - ->build(); - $this->patchPkgconfPrefix(['libde265.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libedit.php b/src/SPC/builder/unix/library/libedit.php deleted file mode 100644 index 2de9077fb..000000000 --- a/src/SPC/builder/unix/library/libedit.php +++ /dev/null @@ -1,30 +0,0 @@ -source_dir . '/src/sys.h', - '|//#define\s+strl|', - '#define strl' - ); - return true; - } - - protected function build(): void - { - UnixAutoconfExecutor::create($this) - ->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L']) - ->configure() - ->make(); - $this->patchPkgconfPrefix(['libedit.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libevent.php b/src/SPC/builder/unix/library/libevent.php deleted file mode 100644 index c2474d542..000000000 --- a/src/SPC/builder/unix/library/libevent.php +++ /dev/null @@ -1,72 +0,0 @@ -addConfigureArgs( - '-DEVENT__LIBRARY_TYPE=STATIC', - '-DEVENT__DISABLE_BENCHMARK=ON', - '-DEVENT__DISABLE_THREAD_SUPPORT=ON', - '-DEVENT__DISABLE_TESTS=ON', - '-DEVENT__DISABLE_SAMPLES=ON', - '-DEVENT__DISABLE_MBEDTLS=ON ', - ); - if (version_compare(get_cmake_version(), '4.0.0', '>=')) { - $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.10'); - } - $cmake->build(); - - $this->patchPkgconfPrefix(['libevent.pc', 'libevent_core.pc', 'libevent_extra.pc', 'libevent_openssl.pc']); - - $this->patchPkgconfPrefix( - ['libevent_openssl.pc'], - PKGCONF_PATCH_CUSTOM, - [ - '/Libs.private:.*/m', - 'Libs.private: -lssl -lcrypto', - ] - ); - } - - protected function install(): void - { - parent::install(); - FileSystem::replaceFileStr( - BUILD_LIB_PATH . '/cmake/libevent/LibeventTargets-static.cmake', - '{BUILD_ROOT_PATH}', - BUILD_ROOT_PATH - ); - } -} diff --git a/src/SPC/builder/unix/library/libheif.php b/src/SPC/builder/unix/library/libheif.php deleted file mode 100644 index 095a1f821..000000000 --- a/src/SPC/builder/unix/library/libheif.php +++ /dev/null @@ -1,39 +0,0 @@ -source_dir . '/CMakeLists.txt'), 'libbrotlienc')) { - FileSystem::replaceFileStr( - $this->source_dir . '/CMakeLists.txt', - 'list(APPEND REQUIRES_PRIVATE "libbrotlidec")', - 'list(APPEND REQUIRES_PRIVATE "libbrotlidec")' . "\n" . ' list(APPEND REQUIRES_PRIVATE "libbrotlienc")' - ); - return true; - } - return false; - } - - protected function build(): void - { - UnixCMakeExecutor::create($this) - ->addConfigureArgs( - '--preset=release', - '-DWITH_EXAMPLES=OFF', - '-DWITH_GDK_PIXBUF=OFF', - '-DBUILD_TESTING=OFF', - '-DWITH_LIBSHARPYUV=ON', // optional: libwebp - '-DENABLE_PLUGIN_LOADING=OFF', - ) - ->build(); - $this->patchPkgconfPrefix(['libheif.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libiconv.php b/src/SPC/builder/unix/library/libiconv.php deleted file mode 100644 index 5ccc94843..000000000 --- a/src/SPC/builder/unix/library/libiconv.php +++ /dev/null @@ -1,22 +0,0 @@ -configure( - '--enable-extra-encodings', - '--enable-year2038', - ) - ->make('install-lib', with_install: false) - ->make('install-lib', with_install: false, dir: $this->getSourceDir() . '/libcharset'); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/libjpeg.php b/src/SPC/builder/unix/library/libjpeg.php deleted file mode 100644 index 969fa0608..000000000 --- a/src/SPC/builder/unix/library/libjpeg.php +++ /dev/null @@ -1,26 +0,0 @@ -addConfigureArgs( - '-DENABLE_STATIC=ON', - '-DENABLE_SHARED=OFF', - '-DWITH_SYSTEM_ZLIB=ON', - '-DWITH_TOOLS=OFF', - '-DWITH_TESTS=OFF', - '-DWITH_SIMD=OFF', - ) - ->build(); - // patch pkgconfig - $this->patchPkgconfPrefix(['libjpeg.pc', 'libturbojpeg.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libjxl.php b/src/SPC/builder/unix/library/libjxl.php deleted file mode 100644 index 4c922d9df..000000000 --- a/src/SPC/builder/unix/library/libjxl.php +++ /dev/null @@ -1,47 +0,0 @@ -addConfigureArgs( - '-DJPEGXL_ENABLE_TOOLS=OFF', - '-DJPEGXL_ENABLE_EXAMPLES=OFF', - '-DJPEGXL_ENABLE_MANPAGES=OFF', - '-DJPEGXL_ENABLE_BENCHMARK=OFF', - '-DJPEGXL_ENABLE_PLUGINS=OFF', - '-DJPEGXL_ENABLE_SJPEG=ON', - '-DJPEGXL_ENABLE_JNI=OFF', - '-DJPEGXL_ENABLE_TRANSCODE_JPEG=ON', - '-DJPEGXL_STATIC=' . (SPCTarget::isStatic() ? 'ON' : 'OFF'), - '-DJPEGXL_FORCE_SYSTEM_BROTLI=ON', - '-DBUILD_TESTING=OFF' - ); - - if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { - $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; - $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); - if (!$has_avx512) { - $cmake->addConfigureArgs( - '-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE', - '-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE' - ); - } - } - - $cmake->build(); - } -} diff --git a/src/SPC/builder/unix/library/liblz4.php b/src/SPC/builder/unix/library/liblz4.php deleted file mode 100644 index 2dc2b46fc..000000000 --- a/src/SPC/builder/unix/library/liblz4.php +++ /dev/null @@ -1,37 +0,0 @@ -source_dir . '/programs/Makefile', 'install: lz4', "install: lz4\n\ninstallewfwef: lz4"); - return true; - } - - protected function build(): void - { - shell()->cd($this->source_dir)->initializeEnv($this) - ->exec("make PREFIX='' clean") - ->exec("make lib -j{$this->builder->concurrency} PREFIX=''"); - - FileSystem::replaceFileStr($this->source_dir . '/Makefile', '$(MAKE) -C $(PRGDIR) $@', ''); - - shell()->cd($this->source_dir) - ->exec("make install PREFIX='' DESTDIR=" . BUILD_ROOT_PATH); - - $this->patchPkgconfPrefix(['liblz4.pc']); - - foreach (FileSystem::scanDirFiles(BUILD_ROOT_PATH . '/lib/', false, true) as $filename) { - if (str_starts_with($filename, 'liblz4') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { - unlink(BUILD_ROOT_PATH . '/lib/' . $filename); - } - } - } -} diff --git a/src/SPC/builder/unix/library/libmaxminddb.php b/src/SPC/builder/unix/library/libmaxminddb.php deleted file mode 100644 index 44915f274..000000000 --- a/src/SPC/builder/unix/library/libmaxminddb.php +++ /dev/null @@ -1,20 +0,0 @@ -addConfigureArgs( - '-DBUILD_TESTING=OFF', - '-DMAXMINDDB_BUILD_BINARIES=OFF', - ) - ->build(); - } -} diff --git a/src/SPC/builder/unix/library/librabbitmq.php b/src/SPC/builder/unix/library/librabbitmq.php deleted file mode 100644 index ca67b6895..000000000 --- a/src/SPC/builder/unix/library/librabbitmq.php +++ /dev/null @@ -1,15 +0,0 @@ -addConfigureArgs('-DBUILD_STATIC_LIBS=ON')->build(); - } -} diff --git a/src/SPC/builder/unix/library/librdkafka.php b/src/SPC/builder/unix/library/librdkafka.php deleted file mode 100644 index 222760d0b..000000000 --- a/src/SPC/builder/unix/library/librdkafka.php +++ /dev/null @@ -1,43 +0,0 @@ -source_dir . '/lds-gen.py', - "funcs.append('rd_ut_coverage_check')", - '' - ); - FileSystem::replaceFileStr( - $this->source_dir . '/src/rd.h', - '#error "IOV_MAX not defined"', - "#define IOV_MAX 1024\n#define __GNU__" - ); - return true; - } - - protected function build(): void - { - UnixCMakeExecutor::create($this) - ->optionalLib('zstd', ...cmake_boolean_args('WITH_ZSTD')) - ->optionalLib('curl', ...cmake_boolean_args('WITH_CURL')) - ->optionalLib('openssl', ...cmake_boolean_args('WITH_SSL')) - ->optionalLib('zlib', ...cmake_boolean_args('WITH_ZLIB')) - ->optionalLib('liblz4', ...cmake_boolean_args('ENABLE_LZ4_EXT')) - ->addConfigureArgs( - '-DWITH_SASL=OFF', - '-DRDKAFKA_BUILD_STATIC=ON', - '-DRDKAFKA_BUILD_EXAMPLES=OFF', - '-DRDKAFKA_BUILD_TESTS=OFF', - ) - ->build(); - } -} diff --git a/src/SPC/builder/unix/library/libsodium.php b/src/SPC/builder/unix/library/libsodium.php deleted file mode 100644 index 441166c9f..000000000 --- a/src/SPC/builder/unix/library/libsodium.php +++ /dev/null @@ -1,16 +0,0 @@ -configure()->make(); - $this->patchPkgconfPrefix(['libsodium.pc'], PKGCONF_PATCH_PREFIX); - } -} diff --git a/src/SPC/builder/unix/library/libssh2.php b/src/SPC/builder/unix/library/libssh2.php deleted file mode 100644 index 91fed1a7b..000000000 --- a/src/SPC/builder/unix/library/libssh2.php +++ /dev/null @@ -1,23 +0,0 @@ -optionalLib('zlib', ...cmake_boolean_args('ENABLE_ZLIB_COMPRESSION')) - ->addConfigureArgs( - '-DBUILD_EXAMPLES=OFF', - '-DBUILD_TESTING=OFF' - ) - ->build(); - - $this->patchPkgconfPrefix(['libssh2.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libtiff.php b/src/SPC/builder/unix/library/libtiff.php deleted file mode 100644 index ed14f4268..000000000 --- a/src/SPC/builder/unix/library/libtiff.php +++ /dev/null @@ -1,44 +0,0 @@ -source_dir . '/configure', '-lwebp', '-lwebp -lsharpyuv'); - FileSystem::replaceFileStr($this->source_dir . '/configure', '-l"$lerc_lib_name"', '-l"$lerc_lib_name" ' . $libcpp); - UnixAutoconfExecutor::create($this) - ->optionalLib('lerc', '--enable-lerc', '--disable-lerc') - ->optionalLib('zstd', '--enable-zstd', '--disable-zstd') - ->optionalLib('libwebp', '--enable-webp', '--disable-webp') - ->optionalLib('xz', '--enable-lzma', '--disable-lzma') - ->optionalLib('jbig', '--enable-jbig', '--disable-jbig') - ->configure( - // zlib deps - '--enable-zlib', - "--with-zlib-include-dir={$this->getIncludeDir()}", - "--with-zlib-lib-dir={$this->getLibDir()}", - // libjpeg deps - '--enable-jpeg', - "--with-jpeg-include-dir={$this->getIncludeDir()}", - "--with-jpeg-lib-dir={$this->getLibDir()}", - '--disable-old-jpeg', - '--disable-jpeg12', - '--disable-libdeflate', - '--disable-tools', - '--disable-contrib', - '--disable-cxx', - '--without-x', - ) - ->make(); - $this->patchPkgconfPrefix(['libtiff-4.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libunistring.php b/src/SPC/builder/unix/library/libunistring.php deleted file mode 100644 index 1d410a8c5..000000000 --- a/src/SPC/builder/unix/library/libunistring.php +++ /dev/null @@ -1,18 +0,0 @@ -configure('--disable-nls') - ->make(); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/libuuid.php b/src/SPC/builder/unix/library/libuuid.php deleted file mode 100644 index 2463ac787..000000000 --- a/src/SPC/builder/unix/library/libuuid.php +++ /dev/null @@ -1,34 +0,0 @@ -toStep(2)->build(); - copy($this->source_dir . '/build/libuuid.a', BUILD_LIB_PATH . '/libuuid.a'); - FileSystem::createDir(BUILD_INCLUDE_PATH . '/uuid'); - copy($this->source_dir . '/uuid.h', BUILD_INCLUDE_PATH . '/uuid/uuid.h'); - $pc = FileSystem::readFile($this->source_dir . '/uuid.pc.in'); - $pc = str_replace([ - '@prefix@', - '@exec_prefix@', - '@libdir@', - '@includedir@', - '@LIBUUID_VERSION@', - ], [ - BUILD_ROOT_PATH, - '${prefix}', - '${prefix}/lib', - '${prefix}/include', - '1.0.3', - ], $pc); - FileSystem::writeFile(BUILD_LIB_PATH . '/pkgconfig/uuid.pc', $pc); - } -} diff --git a/src/SPC/builder/unix/library/libuv.php b/src/SPC/builder/unix/library/libuv.php deleted file mode 100644 index 52e4c8191..000000000 --- a/src/SPC/builder/unix/library/libuv.php +++ /dev/null @@ -1,19 +0,0 @@ -addConfigureArgs('-DLIBUV_BUILD_SHARED=OFF') - ->build(); - // patch pkgconfig - $this->patchPkgconfPrefix(['libuv-static.pc']); - } -} diff --git a/src/SPC/builder/unix/library/libwebp.php b/src/SPC/builder/unix/library/libwebp.php deleted file mode 100644 index 015fa73ba..000000000 --- a/src/SPC/builder/unix/library/libwebp.php +++ /dev/null @@ -1,38 +0,0 @@ - -int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }'; - $cc = getenv('CC') ?: 'gcc'; - [$ret] = shell()->execWithResult("printf '%s' '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1"); - $disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64' && PHP_OS_FAMILY === 'Linux'; - - UnixCMakeExecutor::create($this) - ->addConfigureArgs( - '-DWEBP_BUILD_EXTRAS=OFF', - '-DWEBP_BUILD_ANIM_UTILS=OFF', - '-DWEBP_BUILD_CWEBP=OFF', - '-DWEBP_BUILD_DWEBP=OFF', - '-DWEBP_BUILD_GIF2WEBP=OFF', - '-DWEBP_BUILD_IMG2WEBP=OFF', - '-DWEBP_BUILD_VWEBP=OFF', - '-DWEBP_BUILD_WEBPINFO=OFF', - '-DWEBP_BUILD_WEBPMUX=OFF', - '-DWEBP_BUILD_FUZZTEST=OFF', - $disableAvx2 ? '-DWEBP_ENABLE_SIMD=OFF' : '' - ) - ->build(); - // patch pkgconfig - $this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR); - $this->patchPkgconfPrefix(['libsharpyuv.pc'], PKGCONF_PATCH_CUSTOM, ['/^includedir=.*$/m', 'includedir=${prefix}/include/webp']); - } -} diff --git a/src/SPC/builder/unix/library/libxml2.php b/src/SPC/builder/unix/library/libxml2.php deleted file mode 100644 index 7152c0c46..000000000 --- a/src/SPC/builder/unix/library/libxml2.php +++ /dev/null @@ -1,49 +0,0 @@ -optionalLib( - 'zlib', - '-DLIBXML2_WITH_ZLIB=ON ' . - "-DZLIB_LIBRARY={$this->getLibDir()}/libz.a " . - "-DZLIB_INCLUDE_DIR={$this->getIncludeDir()}", - '-DLIBXML2_WITH_ZLIB=OFF', - ) - ->optionalLib('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) - ->addConfigureArgs( - '-DLIBXML2_WITH_ICONV=ON', - '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md - '-DLIBXML2_WITH_PYTHON=OFF', - '-DLIBXML2_WITH_PROGRAMS=OFF', - '-DLIBXML2_WITH_TESTS=OFF', - ); - - if ($this instanceof LinuxLibraryBase) { - $cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF'); - } - - $cmake->build(); - - FileSystem::replaceFileStr( - BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', - '-lxml2 -liconv', - '-lxml2' - ); - FileSystem::replaceFileStr( - BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', - '-lxml2', - '-lxml2 -liconv' - ); - } -} diff --git a/src/SPC/builder/unix/library/libxslt.php b/src/SPC/builder/unix/library/libxslt.php deleted file mode 100644 index f0e7d512c..000000000 --- a/src/SPC/builder/unix/library/libxslt.php +++ /dev/null @@ -1,45 +0,0 @@ -getStaticLibFiles(include_self: false) : ''; - $cpp = $this instanceof MacOSLibraryBase ? '-lc++' : '-lstdc++'; - $ac = UnixAutoconfExecutor::create($this) - ->appendEnv([ - 'CFLAGS' => "-I{$this->getIncludeDir()}", - 'LDFLAGS' => "-L{$this->getLibDir()}", - 'LIBS' => "{$static_libs} {$cpp}", - ]) - ->addConfigureArgs( - '--without-python', - '--without-crypto', - '--without-debug', - '--without-debugger', - "--with-libxml-prefix={$this->getBuildRootPath()}", - ); - if (getenv('SPC_LD_LIBRARY_PATH') && getenv('SPC_LIBRARY_PATH')) { - $ac->appendEnv([ - 'LD_LIBRARY_PATH' => getenv('SPC_LD_LIBRARY_PATH'), - 'LIBRARY_PATH' => getenv('SPC_LIBRARY_PATH'), - ]); - } - $ac->configure()->make(); - - $this->patchPkgconfPrefix(['libexslt.pc', 'libxslt.pc']); - $this->patchLaDependencyPrefix(); - $AR = getenv('AR') ?: 'ar'; - shell()->cd(BUILD_LIB_PATH) - ->exec("{$AR} -t libxslt.a | grep '\\.a$' | xargs -n1 {$AR} d libxslt.a") - ->exec("{$AR} -t libexslt.a | grep '\\.a$' | xargs -n1 {$AR} d libexslt.a"); - } -} diff --git a/src/SPC/builder/unix/library/libyaml.php b/src/SPC/builder/unix/library/libyaml.php deleted file mode 100644 index 12ab39154..000000000 --- a/src/SPC/builder/unix/library/libyaml.php +++ /dev/null @@ -1,35 +0,0 @@ -source_dir . '/CMakeLists.txt'); - if (preg_match('/set \(YAML_VERSION_MAJOR (\d+)\)/', $content, $major) - && preg_match('/set \(YAML_VERSION_MINOR (\d+)\)/', $content, $minor) - && preg_match('/set \(YAML_VERSION_PATCH (\d+)\)/', $content, $patch)) { - return "{$major[1]}.{$minor[1]}.{$patch[1]}"; - } - return null; - } - - protected function build(): void - { - $cmake = UnixCMakeExecutor::create($this)->addConfigureArgs('-DBUILD_TESTING=OFF'); - if (version_compare(get_cmake_version(), '4.0.0', '>=')) { - $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.5'); - } - $cmake->build(); - } -} diff --git a/src/SPC/builder/unix/library/libzip.php b/src/SPC/builder/unix/library/libzip.php deleted file mode 100644 index ad0befea2..000000000 --- a/src/SPC/builder/unix/library/libzip.php +++ /dev/null @@ -1,30 +0,0 @@ -optionalLib('bzip2', ...cmake_boolean_args('ENABLE_BZIP2')) - ->optionalLib('xz', ...cmake_boolean_args('ENABLE_LZMA')) - ->optionalLib('openssl', ...cmake_boolean_args('ENABLE_OPENSSL')) - ->optionalLib('zstd', ...cmake_boolean_args('ENABLE_ZSTD')) - ->addConfigureArgs( - '-DENABLE_GNUTLS=OFF', - '-DENABLE_MBEDTLS=OFF', - '-DBUILD_DOC=OFF', - '-DBUILD_EXAMPLES=OFF', - '-DBUILD_REGRESS=OFF', - '-DBUILD_TOOLS=OFF', - '-DBUILD_OSSFUZZ=OFF', - ) - ->build(); - $this->patchPkgconfPrefix(['libzip.pc'], PKGCONF_PATCH_PREFIX); - } -} diff --git a/src/SPC/builder/unix/library/mimalloc.php b/src/SPC/builder/unix/library/mimalloc.php deleted file mode 100644 index d483ed397..000000000 --- a/src/SPC/builder/unix/library/mimalloc.php +++ /dev/null @@ -1,25 +0,0 @@ -addConfigureArgs( - '-DMI_BUILD_SHARED=OFF', - '-DMI_BUILD_OBJECT=OFF', - '-DMI_INSTALL_TOPLEVEL=ON', - ); - if (SPCTarget::getLibc() === 'musl') { - $cmake->addConfigureArgs('-DMI_LIBC_MUSL=ON'); - } - $cmake->build(); - } -} diff --git a/src/SPC/builder/unix/library/ncurses.php b/src/SPC/builder/unix/library/ncurses.php deleted file mode 100644 index cb8c10df0..000000000 --- a/src/SPC/builder/unix/library/ncurses.php +++ /dev/null @@ -1,57 +0,0 @@ -appendEnv([ - 'CFLAGS' => '-std=c17', - 'LDFLAGS' => SPCTarget::isStatic() ? '-static' : '', - ]) - ->configure( - '--enable-overwrite', - '--with-curses-h', - '--enable-pc-files', - '--enable-echo', - '--disable-widec', - '--with-normal', - '--with-ticlib', - '--without-tests', - '--without-dlsym', - '--without-debug', - '--enable-symlinks', - "--bindir={$this->getBinDir()}", - "--includedir={$this->getIncludeDir()}", - "--libdir={$this->getLibDir()}", - "--prefix={$this->getBuildRootPath()}", - ) - ->make(); - $final = FileSystem::scanDirFiles(BUILD_BIN_PATH, relative: true); - // Remove the new files - $new_files = array_diff($final, $filelist ?: []); - foreach ($new_files as $file) { - @unlink(BUILD_BIN_PATH . '/' . $file); - } - - shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); - shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); - - $pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc']; - $this->patchPkgconfPrefix($pkgconf_list); - - foreach ($pkgconf_list as $pkgconf) { - FileSystem::replaceFileStr(BUILD_LIB_PATH . '/pkgconfig/' . $pkgconf, '-L' . BUILD_LIB_PATH, '-L${libdir}'); - } - } -} diff --git a/src/SPC/builder/unix/library/net_snmp.php b/src/SPC/builder/unix/library/net_snmp.php deleted file mode 100644 index df464d523..000000000 --- a/src/SPC/builder/unix/library/net_snmp.php +++ /dev/null @@ -1,49 +0,0 @@ -source_dir}/configure", 'LIBS="-lssl ${OPENSSL_LIBS}"', 'LIBS="-lssl ${OPENSSL_LIBS} -lpthread -ldl"'); - return true; - } - return false; - } - - protected function build(): void - { - // use --static for PKG_CONFIG - UnixAutoconfExecutor::create($this) - ->setEnv(['PKG_CONFIG' => getenv('PKG_CONFIG') . ' --static']) - ->configure( - '--disable-mibs', - '--without-nl', - '--disable-agent', - '--disable-applications', - '--disable-manuals', - '--disable-scripts', - '--disable-embedded-perl', - '--without-perl-modules', - '--with-out-mib-modules="if-mib host disman/event-mib ucd-snmp/diskio mibII"', - '--with-out-transports="Unix"', - '--with-mib-modules=""', - '--enable-mini-agent', - '--with-default-snmp-version="3"', - '--with-sys-contact="@@no.where"', - '--with-sys-location="Unknown"', - '--with-logfile="/var/log/snmpd.log"', - '--with-persistent-directory="/var/lib/net-snmp"', - '--with-openssl=' . BUILD_ROOT_PATH, - '--with-zlib=' . BUILD_ROOT_PATH, - )->make(with_install: 'installheaders installlibs install_pkgconfig'); - $this->patchPkgconfPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/nghttp2.php b/src/SPC/builder/unix/library/nghttp2.php deleted file mode 100644 index 249472f76..000000000 --- a/src/SPC/builder/unix/library/nghttp2.php +++ /dev/null @@ -1,42 +0,0 @@ -optionalLib('zlib', ...ac_with_args('zlib', true)) - ->optionalLib('openssl', ...ac_with_args('openssl', true)) - ->optionalLib('libxml2', ...ac_with_args('libxml2', true)) - ->optionalLib('libev', ...ac_with_args('libev', true)) - ->optionalLib('libcares', ...ac_with_args('libcares', true)) - ->optionalLib('ngtcp2', ...ac_with_args('libngtcp2', true)) - ->optionalLib('nghttp3', ...ac_with_args('libnghttp3', true)) - // ->optionalLib('libbpf', ...ac_with_args('libbpf', true)) - // ->optionalLib('libevent-openssl', ...ac_with_args('libevent-openssl', true)) - // ->optionalLib('jansson', ...ac_with_args('jansson', true)) - // ->optionalLib('jemalloc', ...ac_with_args('jemalloc', true)) - // ->optionalLib('systemd', ...ac_with_args('systemd', true)) - ->optionalLib( - 'brotli', - fn ($lib) => implode(' ', [ - '--with-brotlidec=yes', - "LIBBROTLIDEC_CFLAGS=\"-I{$lib->getIncludeDir()}\"", - "LIBBROTLIDEC_LIBS=\"{$lib->getStaticLibFiles()}\"", - '--with-libbrotlienc=yes', - "LIBBROTLIENC_CFLAGS=\"-I{$lib->getIncludeDir()}\"", - "LIBBROTLIENC_LIBS=\"{$lib->getStaticLibFiles()}\"", - ]) - ) - ->configure('--enable-lib-only') - ->make(); - $this->patchPkgconfPrefix(['libnghttp2.pc']); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/nghttp3.php b/src/SPC/builder/unix/library/nghttp3.php deleted file mode 100644 index 35368f798..000000000 --- a/src/SPC/builder/unix/library/nghttp3.php +++ /dev/null @@ -1,17 +0,0 @@ -configure('--enable-lib-only')->make(); - $this->patchPkgconfPrefix(['libnghttp3.pc']); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/ngtcp2.php b/src/SPC/builder/unix/library/ngtcp2.php deleted file mode 100644 index 8b97e8b82..000000000 --- a/src/SPC/builder/unix/library/ngtcp2.php +++ /dev/null @@ -1,46 +0,0 @@ -optionalLib('openssl', fn (LinuxLibraryBase|MacOSLibraryBase $lib) => implode(' ', [ - '--with-openssl=yes', - "OPENSSL_LIBS=\"{$lib->getStaticLibFiles()}\"", - "OPENSSL_CFLAGS=\"-I{$lib->getIncludeDir()}\"", - ]), '--with-openssl=no') - ->optionalLib('libev', ...ac_with_args('libev', true)) - ->optionalLib('nghttp3', ...ac_with_args('libnghttp3', true)) - ->optionalLib('jemalloc', ...ac_with_args('jemalloc', true)) - ->optionalLib( - 'brotli', - fn (LinuxLibraryBase|MacOSLibraryBase $lib) => implode(' ', [ - '--with-brotlidec=yes', - "LIBBROTLIDEC_CFLAGS=\"-I{$lib->getIncludeDir()}\"", - "LIBBROTLIDEC_LIBS=\"{$lib->getStaticLibFiles()}\"", - '--with-libbrotlienc=yes', - "LIBBROTLIENC_CFLAGS=\"-I{$lib->getIncludeDir()}\"", - "LIBBROTLIENC_LIBS=\"{$lib->getStaticLibFiles()}\"", - ]) - ) - ->appendEnv(['PKG_CONFIG' => '$PKG_CONFIG --static']) - ->configure('--enable-lib-only') - ->make(); - $this->patchPkgconfPrefix(['libngtcp2.pc', 'libngtcp2_crypto_ossl.pc']); - $this->patchLaDependencyPrefix(); - - // on macOS, the static library may contain other static libraries? - // ld: archive member 'libssl.a' not a mach-o file in libngtcp2_crypto_ossl.a - $AR = getenv('AR') ?: 'ar'; - shell()->cd(BUILD_LIB_PATH)->exec("{$AR} -t libngtcp2_crypto_ossl.a | grep '\\.a$' | xargs -n1 {$AR} d libngtcp2_crypto_ossl.a"); - } -} diff --git a/src/SPC/builder/unix/library/onig.php b/src/SPC/builder/unix/library/onig.php deleted file mode 100644 index 0bd043170..000000000 --- a/src/SPC/builder/unix/library/onig.php +++ /dev/null @@ -1,16 +0,0 @@ -configure()->make(); - $this->patchPkgconfPrefix(['oniguruma.pc']); - } -} diff --git a/src/SPC/builder/unix/library/pkgconfig.php b/src/SPC/builder/unix/library/pkgconfig.php deleted file mode 100644 index e6f672e59..000000000 --- a/src/SPC/builder/unix/library/pkgconfig.php +++ /dev/null @@ -1,31 +0,0 @@ -appendEnv([ - 'CFLAGS' => '-Wimplicit-function-declaration -Wno-int-conversion', - 'LDFLAGS' => SPCTarget::isStatic() ? '--static' : '', - ]) - ->configure( - '--with-internal-glib', - '--disable-host-tool', - '--without-sysroot', - '--without-system-include-path', - '--without-system-library-path', - '--without-pc-path', - ) - ->make(with_install: 'install-exec'); - - shell()->exec('strip ' . BUILD_ROOT_PATH . '/bin/pkg-config'); - } -} diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php deleted file mode 100644 index a3aed130b..000000000 --- a/src/SPC/builder/unix/library/postgresql.php +++ /dev/null @@ -1,101 +0,0 @@ -source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); - // disable shared libs build - FileSystem::replaceFileStr( - "{$this->source_dir}/src/Makefile.shlib", - [ - '$(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(LDFLAGS_SL) $(SHLIB_LINK)', - '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(pkglibdir)/$(shlib)\'', - '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(libdir)/$(shlib)\'', - '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(bindir)/$(shlib)\'', - ], - '' - ); - return true; - } - - protected function build(): void - { - $libs = array_map(fn ($x) => $x->getName(), $this->getDependencies(true)); - $spc = new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]); - $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false)); - - $env_vars = [ - 'CFLAGS' => $config['cflags'] . ' -std=c17', - 'CPPFLAGS' => '-DPIC', - 'LDFLAGS' => $config['ldflags'], - 'LIBS' => $config['libs'], - ]; - - if ($ldLibraryPath = getenv('SPC_LD_LIBRARY_PATH')) { - $env_vars['LD_LIBRARY_PATH'] = $ldLibraryPath; - } - - FileSystem::resetDir($this->source_dir . '/build'); - - // php source relies on the non-private encoding functions in libpgcommon.a - FileSystem::replaceFileStr( - "{$this->source_dir}/src/common/Makefile", - '$(OBJS_FRONTEND): CPPFLAGS += -DUSE_PRIVATE_ENCODING_FUNCS', - '$(OBJS_FRONTEND): CPPFLAGS += -UUSE_PRIVATE_ENCODING_FUNCS -DFRONTEND', - ); - - // configure - $shell = shell()->cd("{$this->source_dir}/build")->initializeEnv($this) - ->appendEnv($env_vars) - ->exec( - '../configure ' . - "--prefix={$this->getBuildRootPath()} " . - '--enable-coverage=no ' . - '--with-ssl=openssl ' . - '--with-readline ' . - '--with-libxml ' . - ($this->builder->getLib('icu') ? '--with-icu ' : '--without-icu ') . - ($this->builder->getLib('ldap') ? '--with-ldap ' : '--without-ldap ') . - ($this->builder->getLib('libxslt') ? '--with-libxslt ' : '--without-libxslt ') . - ($this->builder->getLib('zstd') ? '--with-zstd ' : '--without-zstd ') . - '--without-lz4 ' . - '--without-perl ' . - '--without-python ' . - '--without-pam ' . - '--without-bonjour ' . - '--without-tcl ' - ); - - // patch ldap lib - if ($this->builder->getLib('ldap')) { - $libs = PkgConfigUtil::getLibsArray('ldap'); - $libs = clean_spaces(implode(' ', $libs)); - FileSystem::replaceFileStr($this->source_dir . '/build/config.status', '-lldap', $libs); - FileSystem::replaceFileStr($this->source_dir . '/build/src/Makefile.global', '-lldap', $libs); - } - - $shell - ->exec('make -C src/bin/pg_config install') - ->exec('make -C src/include install') - ->exec('make -C src/common install') - ->exec('make -C src/port install') - ->exec('make -C src/interfaces/libpq install'); - - // remove dynamic libs - shell()->cd($this->source_dir . '/build') - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so*") - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); - - FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); - } -} diff --git a/src/SPC/builder/unix/library/qdbm.php b/src/SPC/builder/unix/library/qdbm.php deleted file mode 100644 index 640e6c4d2..000000000 --- a/src/SPC/builder/unix/library/qdbm.php +++ /dev/null @@ -1,20 +0,0 @@ -configure(); - FileSystem::replaceFileRegex($this->source_dir . '/Makefile', '/MYLIBS = libqdbm.a.*/m', 'MYLIBS = libqdbm.a'); - $ac->make($this instanceof MacOSLibraryBase ? 'mac' : ''); - $this->patchPkgconfPrefix(['qdbm.pc']); - } -} diff --git a/src/SPC/builder/unix/library/re2c.php b/src/SPC/builder/unix/library/re2c.php deleted file mode 100644 index cd6a562ad..000000000 --- a/src/SPC/builder/unix/library/re2c.php +++ /dev/null @@ -1,32 +0,0 @@ -addConfigureArgs( - '-DRE2C_BUILD_TESTS=OFF', - '-DRE2C_BUILD_EXAMPLES=OFF', - '-DRE2C_BUILD_DOCS=OFF', - '-DRE2C_BUILD_RE2D=OFF', - '-DRE2C_BUILD_RE2GO=OFF', - '-DRE2C_BUILD_RE2HS=OFF', - '-DRE2C_BUILD_RE2JAVA=OFF', - '-DRE2C_BUILD_RE2JS=OFF', - '-DRE2C_BUILD_RE2OCAML=OFF', - '-DRE2C_BUILD_RE2PY=OFF', - '-DRE2C_BUILD_RE2RUST=OFF', - '-DRE2C_BUILD_RE2SWIFT=OFF', - '-DRE2C_BUILD_RE2V=OFF', - '-DRE2C_BUILD_RE2ZIG=OFF', - ) - ->build(); - } -} diff --git a/src/SPC/builder/unix/library/readline.php b/src/SPC/builder/unix/library/readline.php deleted file mode 100644 index 758cc03f3..000000000 --- a/src/SPC/builder/unix/library/readline.php +++ /dev/null @@ -1,21 +0,0 @@ -configure( - '--with-curses', - '--enable-multibyte=yes', - ) - ->make(); - $this->patchPkgconfPrefix(['readline.pc']); - } -} diff --git a/src/SPC/builder/unix/library/snappy.php b/src/SPC/builder/unix/library/snappy.php deleted file mode 100644 index 060bb0f46..000000000 --- a/src/SPC/builder/unix/library/snappy.php +++ /dev/null @@ -1,21 +0,0 @@ -setBuildDir("{$this->source_dir}/cmake/build") - ->addConfigureArgs( - '-DSNAPPY_BUILD_TESTS=OFF', - '-DSNAPPY_BUILD_BENCHMARKS=OFF', - ) - ->build('../..'); - } -} diff --git a/src/SPC/builder/unix/library/sqlite.php b/src/SPC/builder/unix/library/sqlite.php deleted file mode 100644 index 8f9add24b..000000000 --- a/src/SPC/builder/unix/library/sqlite.php +++ /dev/null @@ -1,16 +0,0 @@ -configure()->make(); - $this->patchPkgconfPrefix(['sqlite3.pc']); - } -} diff --git a/src/SPC/builder/unix/library/tidy.php b/src/SPC/builder/unix/library/tidy.php deleted file mode 100644 index 6afaa94f6..000000000 --- a/src/SPC/builder/unix/library/tidy.php +++ /dev/null @@ -1,25 +0,0 @@ -setBuildDir("{$this->source_dir}/build-dir") - ->addConfigureArgs( - '-DSUPPORT_CONSOLE_APP=OFF', - '-DBUILD_SHARED_LIB=OFF' - ); - if (version_compare(get_cmake_version(), '4.0.0', '>=')) { - $cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.5'); - } - $cmake->build(); - $this->patchPkgconfPrefix(['tidy.pc']); - } -} diff --git a/src/SPC/builder/unix/library/unixodbc.php b/src/SPC/builder/unix/library/unixodbc.php deleted file mode 100644 index cf923f24b..000000000 --- a/src/SPC/builder/unix/library/unixodbc.php +++ /dev/null @@ -1,45 +0,0 @@ - match (GNU_ARCH) { - 'x86_64' => '/usr/local/etc', - 'aarch64' => '/opt/homebrew/etc', - default => throw new WrongUsageException('Unsupported architecture: ' . GNU_ARCH), - }, - 'Linux' => '/etc', - default => throw new WrongUsageException('Unsupported OS: ' . PHP_OS_FAMILY), - }; - UnixAutoconfExecutor::create($this) - ->configure( - '--disable-debug', - '--disable-dependency-tracking', - "--with-libiconv-prefix={$this->getBuildRootPath()}", - '--with-included-ltdl', - "--sysconfdir={$sysconf_selector}", - '--enable-gui=no', - ) - ->make(); - $pkgConfigs = ['odbc.pc', 'odbccr.pc', 'odbcinst.pc']; - $this->patchPkgconfPrefix($pkgConfigs); - foreach ($pkgConfigs as $file) { - FileSystem::replaceFileStr( - BUILD_LIB_PATH . "/pkgconfig/{$file}", - '$(top_build_prefix)libltdl/libltdlc.la', - '' - ); - } - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/watcher.php b/src/SPC/builder/unix/library/watcher.php deleted file mode 100644 index 359fedd2e..000000000 --- a/src/SPC/builder/unix/library/watcher.php +++ /dev/null @@ -1,26 +0,0 @@ -getLibExtraCXXFlags(); - if (stripos($cflags, '-fpic') === false) { - $cflags .= ' -fPIC'; - } - $ldflags = $this->getLibExtraLdFlags() ? ' ' . $this->getLibExtraLdFlags() : ''; - shell()->cd($this->source_dir . '/watcher-c') - ->exec(getenv('CXX') . " -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra {$cflags}{$ldflags}") - ->exec(getenv('AR') . ' rcs libwatcher-c.a libwatcher-c.o'); - - copy($this->source_dir . '/watcher-c/libwatcher-c.a', BUILD_LIB_PATH . '/libwatcher-c.a'); - FileSystem::createDir(BUILD_INCLUDE_PATH . '/wtr'); - copy($this->source_dir . '/watcher-c/include/wtr/watcher-c.h', BUILD_INCLUDE_PATH . '/wtr/watcher-c.h'); - } -} diff --git a/src/SPC/builder/unix/library/xz.php b/src/SPC/builder/unix/library/xz.php deleted file mode 100644 index e17f92b17..000000000 --- a/src/SPC/builder/unix/library/xz.php +++ /dev/null @@ -1,24 +0,0 @@ -configure( - '--disable-scripts', - '--disable-doc', - '--with-libiconv', - '--bindir=/tmp/xz', // xz binary will corrupt `tar` command, that's really strange. - ) - ->make(); - $this->patchPkgconfPrefix(['liblzma.pc']); - $this->patchLaDependencyPrefix(); - } -} diff --git a/src/SPC/builder/unix/library/zlib.php b/src/SPC/builder/unix/library/zlib.php deleted file mode 100644 index 394e4a4f5..000000000 --- a/src/SPC/builder/unix/library/zlib.php +++ /dev/null @@ -1,16 +0,0 @@ -exec("./configure --static --prefix={$this->getBuildRootPath()}")->make(); - $this->patchPkgconfPrefix(['zlib.pc']); - } -} diff --git a/src/SPC/builder/unix/library/zstd.php b/src/SPC/builder/unix/library/zstd.php deleted file mode 100644 index 59b267014..000000000 --- a/src/SPC/builder/unix/library/zstd.php +++ /dev/null @@ -1,22 +0,0 @@ -setBuildDir("{$this->source_dir}/build/cmake/build") - ->addConfigureArgs( - '-DZSTD_BUILD_STATIC=ON', - '-DZSTD_BUILD_SHARED=OFF', - ) - ->build(); - $this->patchPkgconfPrefix(['libzstd.pc']); - } -} diff --git a/src/SPC/builder/windows/SystemUtil.php b/src/SPC/builder/windows/SystemUtil.php deleted file mode 100644 index 3a0c23ae4..000000000 --- a/src/SPC/builder/windows/SystemUtil.php +++ /dev/null @@ -1,106 +0,0 @@ -|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', - ]; - 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, - ]; - } - } - return false; - } - - /** - * Get CPU count for concurrency. - */ - public static function getCpuCount(): int - { - $result = f_exec('echo %NUMBER_OF_PROCESSORS%', $out, $code); - if ($code !== 0 || !$result) { - return 1; - } - return intval($result); - } - - /** - * Create CMake toolchain file. - * - * @param null|string $cflags CFLAGS for cmake, default use '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP=' - * @param null|string $ldflags LDFLAGS for cmake, default use '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt' - */ - public static function makeCmakeToolchainFile(?string $cflags = null, ?string $ldflags = null): string - { - if ($cflags === null) { - $cflags = '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP='; - } - if ($ldflags === null) { - $ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'; - } - $buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH); - $toolchain = <<options = $options; - - GlobalEnvManager::init(); - GlobalEnvManager::afterInit(); - - // ---------- set necessary options ---------- - // set sdk (require visual studio 16 or 17) - $vs = SystemUtil::findVisualStudio()['version']; - $this->sdk_prefix = getenv('PHP_SDK_PATH') . "\\phpsdk-{$vs}-x64.bat -t"; - - // set zts - $this->zts = $this->getOption('enable-zts', false); - - // set concurrency - $this->concurrency = (int) getenv('SPC_CONCURRENCY'); - - // make cmake toolchain - $this->cmake_toolchain_file = SystemUtil::makeCmakeToolchainFile(); - - f_mkdir(BUILD_INCLUDE_PATH, recursive: true); - f_mkdir(BUILD_LIB_PATH, recursive: true); - } - - public function buildPHP(int $build_target = BUILD_TARGET_NONE): void - { - $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; - $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; - $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; - $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; - $enableCgi = ($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI; - - SourcePatcher::patchBeforeBuildconf($this); - - cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} buildconf.bat"); - - SourcePatcher::patchBeforeConfigure($this); - - $zts = $this->zts ? '--enable-zts=yes ' : '--enable-zts=no '; - - $opcache_jit = !$this->getOption('disable-opcache-jit', false); - $opcache_jit_arg = $opcache_jit ? '--enable-opcache-jit=yes ' : '--enable-opcache-jit=no '; - - if (($logo = $this->getOption('with-micro-logo')) !== null) { - // realpath - // $logo = realpath($logo); - $micro_logo = '--enable-micro-logo=' . $logo . ' '; - copy($logo, SOURCE_PATH . '\php-src\\' . $logo); - } else { - $micro_logo = ''; - } - - $micro_w32 = $this->getOption('enable-micro-win32') ? ' --enable-micro-win32=yes' : ''; - - $config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ? - ('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : ''; - - cmd()->cd(SOURCE_PATH . '\php-src') - ->exec( - "{$this->sdk_prefix} configure.bat --task-args \"" . - '--disable-all ' . - '--with-php-build=' . BUILD_ROOT_PATH . ' ' . - '--with-extra-includes=' . BUILD_INCLUDE_PATH . ' ' . - '--with-extra-libs=' . BUILD_LIB_PATH . ' ' . - ($enableCli ? '--enable-cli ' : '--disable-cli ') . - ($enableMicro ? ('--enable-micro ' . $micro_logo . $micro_w32) : '--disable-micro ') . - ($enableEmbed ? '--enable-embed ' : '--disable-embed ') . - ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . - $config_file_scan_dir . - $opcache_jit_arg . - "{$this->makeStaticExtensionArgs()} " . - $zts . - '"' - ); - - SourcePatcher::patchBeforeMake($this); - - $this->cleanMake(); - - if ($enableCli) { - logger()->info('building cli'); - $this->buildCli(); - } - if ($enableFpm) { - logger()->warning('Windows does not support fpm SAPI, I will skip it.'); - } - if ($enableCgi) { - logger()->info('building cgi'); - $this->buildCgi(); - } - if ($enableMicro) { - logger()->info('building micro'); - $this->buildMicro(); - - SourcePatcher::unpatchMicroWin32(); - } - if ($enableEmbed) { - logger()->warning('Windows does not currently support embed SAPI.'); - // logger()->info('building embed'); - $this->buildEmbed(); - } - } - - public function testPHP(int $build_target = BUILD_TARGET_NONE): void - { - $this->sanityCheck($build_target); - } - - public function buildCli(): void - { - SourcePatcher::patchWindowsCLITarget(); - - $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 ($this->getOption('no-strip', false)) { - // Read current CFLAGS from Makefile and replace optimization flags - $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\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" '; - } - } - - // add nmake wrapper - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cli_wrapper.bat', "nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= %*"); - - cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cli_wrapper.bat --task-args php.exe"); - - $this->deploySAPIBinary(BUILD_TARGET_CLI); - } - - public function buildCgi(): void - { - SourcePatcher::patchWindowsCGITarget(); - - $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; - - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($this->getOption('no-strip', false)) { - $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\Makefile'); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; - } - } - - // add nmake wrapper - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cgi_wrapper.bat', "nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= %*"); - - cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cgi_wrapper.bat --task-args php-cgi.exe"); - - $this->deploySAPIBinary(BUILD_TARGET_CGI); - } - - public function buildEmbed(): void - { - // TODO: add embed support for windows - /* - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_embed_wrapper.bat', 'nmake /nologo %*'); - - cmd()->cd(SOURCE_PATH . '\php-src') - ->exec("{$this->sdk_prefix} nmake_embed_wrapper.bat --task-args php8embed.lib"); - */ - } - - public function buildMicro(): void - { - // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) - $makefile = FileSystem::readFile(SOURCE_PATH . '\php-src\Makefile'); - if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { - $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; - } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { - $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ABI).obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ABI).obj' . "\r\n\r\n"; - } - FileSystem::writeFile(SOURCE_PATH . '\php-src\Makefile', $makefile); - - $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; - - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($this->getOption('no-strip', false) && !$this->getOption('debug', false)) { - $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\Makefile'); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; - } - } - - // add nmake wrapper - $fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI" ' : ''; - $wrapper = "nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" %*"; - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_micro_wrapper.bat', $wrapper); - - // phar patch for micro - if ($this->getExt('phar')) { - $this->phar_patched = true; - SourcePatcher::patchMicroPhar($this->getPHPVersionID()); - } - - try { - cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_micro_wrapper.bat --task-args micro"); - } finally { - if ($this->phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } - - $this->deploySAPIBinary(BUILD_TARGET_MICRO); - } - - public function proveLibs(array $sorted_libraries): void - { - // search all supported libs - $support_lib_list = []; - $classes = FileSystem::getClassesPsr4( - ROOT_DIR . '\src\SPC\builder\\' . osfamily2dir() . '\library', - 'SPC\builder\\' . osfamily2dir() . '\library' - ); - foreach ($classes as $class) { - if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) { - $support_lib_list[$class::NAME] = $class; - } - } - - // if no libs specified, compile all supported libs - if ($sorted_libraries === [] && $this->isLibsOnly()) { - $libraries = array_keys($support_lib_list); - $sorted_libraries = DependencyUtil::getLibs($libraries); - } - - // add lib object for builder - foreach ($sorted_libraries as $library) { - if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) { - continue; - } - // if some libs are not supported (but in config "lib.json", throw exception) - if (!isset($support_lib_list[$library])) { - throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!'); - } - $lib = new ($support_lib_list[$library])($this); - $this->addLib($lib); - } - - // calculate and check dependencies - foreach ($this->libs as $lib) { - $lib->calcDependency(); - } - $this->lib_list = $sorted_libraries; - } - - public function cleanMake(): void - { - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_clean_wrapper.bat', 'nmake /nologo %*'); - cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_clean_wrapper.bat --task-args \"clean\""); - } - - /** - * Run extension and PHP cli and micro check - */ - public function sanityCheck(mixed $build_target): void - { - // remove all .dll from `buildroot/bin/` - logger()->debug('Removing all .dll files from buildroot/bin/'); - $dlls = glob(BUILD_BIN_PATH . '\*.dll'); - foreach ($dlls as $dll) { - @unlink($dll); - } - // sanity check for php-cli - if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { - logger()->info('running cli sanity check'); - [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); - if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { - throw new ValidationException('cli failed sanity check', validation_module: 'php-cli function check'); - } - - foreach ($this->getExts(false) as $ext) { - logger()->debug('testing ext: ' . $ext->getName()); - $ext->runCliCheckWindows(); - } - } - - // sanity check for phpmicro - if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - $test_task = $this->getMicroTestTasks(); - foreach ($test_task as $task_name => $task) { - $test_file = SOURCE_PATH . '/' . $task_name . '.exe'; - if (file_exists($test_file)) { - @unlink($test_file); - } - file_put_contents($test_file, file_get_contents(BUILD_BIN_PATH . '\micro.sfx') . $task['content']); - chmod($test_file, 0755); - [$ret, $out] = cmd()->execWithResult($test_file); - foreach ($task['conditions'] as $condition => $closure) { - if (!$closure($ret, $out)) { - $raw_out = trim(implode('', $out)); - throw new ValidationException( - "failure info: {$condition}, code: {$ret}, output: {$raw_out}", - validation_module: "phpmicro sanity check item [{$task_name}]" - ); - } - } - } - } - - // sanity check for php-cgi - if (($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI) { - logger()->info('running cgi sanity check'); - FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); - [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); - $raw_output = implode("\n", $output); - if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { - throw new ValidationException("cgi failed sanity check. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi sanity check'); - } - } - } - - /** - * Deploy the binary file to buildroot/bin/ - * - * @param int $type Deploy type - */ - public function deploySAPIBinary(int $type): void - { - logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file'); - - $debug_dir = BUILD_ROOT_PATH . '\debug'; - $bin_path = BUILD_BIN_PATH; - - // create dirs - FileSystem::createDir($debug_dir); - FileSystem::createDir($bin_path); - - // determine source path for different SAPI - $rel_type = 'Release'; // TODO: Debug build support - $ts = $this->zts ? '_TS' : ''; - $src = match ($type) { - BUILD_TARGET_CLI => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'], - BUILD_TARGET_MICRO => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'], - BUILD_TARGET_CGI => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'], - default => throw new SPCInternalException("Deployment does not accept type {$type}"), - }; - - $src = "{$src[0]}\\{$src[1]}"; - $dst = BUILD_BIN_PATH . '\\' . basename($src); - - // file must exists - if (!file_exists($src)) { - throw new SPCInternalException("Deploy failed. Cannot find file: {$src}"); - } - // dst dir must exists - FileSystem::createDir(dirname($dst)); - - // copy file - if (realpath($src) !== realpath($dst)) { - cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); - } - - // extract debug info in buildroot/debug - if ($this->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { - cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg($debug_dir)); - } - - // with-upx-pack for cli and micro - if ($this->getOption('with-upx-pack', false)) { - if ($type === BUILD_TARGET_CLI || $type === BUILD_TARGET_CGI || ($type === BUILD_TARGET_MICRO && version_compare($this->getMicroVersion(), '0.2.0') >= 0)) { - cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst)); - } - } - } - - /** - * Generate command wrapper prefix for php-sdk internal commands. - * - * @param string $internal_cmd command in php-sdk-tools\bin - * @return string Example: C:\php-sdk-tools\phpsdk-vs17-x64.bat -t source\cmake_wrapper.bat --task-args - */ - public function makeSimpleWrapper(string $internal_cmd): string - { - $wrapper_bat = SOURCE_PATH . '\\' . crc32($internal_cmd) . '_wrapper.bat'; - if (!file_exists($wrapper_bat)) { - file_put_contents($wrapper_bat, $internal_cmd . ' %*'); - } - return "{$this->sdk_prefix} {$wrapper_bat} --task-args"; - } -} diff --git a/src/SPC/builder/windows/library/WindowsLibraryBase.php b/src/SPC/builder/windows/library/WindowsLibraryBase.php deleted file mode 100644 index dfc221c9b..000000000 --- a/src/SPC/builder/windows/library/WindowsLibraryBase.php +++ /dev/null @@ -1,22 +0,0 @@ -builder; - } -} diff --git a/src/SPC/builder/windows/library/brotli.php b/src/SPC/builder/windows/library/brotli.php deleted file mode 100644 index f22f402ce..000000000 --- a/src/SPC/builder/windows/library/brotli.php +++ /dev/null @@ -1,36 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBROTLI_BUILD_TOOLS=OFF ' . - '-DBROTLI_BUNDLED_MODE=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/bzip2.php b/src/SPC/builder/windows/library/bzip2.php deleted file mode 100644 index 81a74b0a9..000000000 --- a/src/SPC/builder/windows/library/bzip2.php +++ /dev/null @@ -1,21 +0,0 @@ -builder->makeSimpleWrapper('nmake /nologo /f Makefile.msc CFLAGS="-DWIN32 -MT -Ox -D_FILE_OFFSET_BITS=64 -nologo"'); - cmd()->cd($this->source_dir) - ->execWithWrapper($nmake, 'clean') - ->execWithWrapper($nmake, 'lib'); - copy($this->source_dir . '\libbz2.lib', BUILD_LIB_PATH . '\libbz2.lib'); - copy($this->source_dir . '\libbz2.lib', BUILD_LIB_PATH . '\libbz2_a.lib'); - copy($this->source_dir . '\bzlib.h', BUILD_INCLUDE_PATH . '\bzlib.h'); - } -} diff --git a/src/SPC/builder/windows/library/curl.php b/src/SPC/builder/windows/library/curl.php deleted file mode 100644 index bba130e1a..000000000 --- a/src/SPC/builder/windows/library/curl.php +++ /dev/null @@ -1,58 +0,0 @@ -source_dir . '\cmakebuild'); - - // lib:zstd - $alt = $this->builder->getLib('zstd') ? '' : '-DCURL_ZSTD=OFF'; - // lib:brotli - $alt .= $this->builder->getLib('brotli') ? '' : ' -DCURL_BROTLI=OFF'; - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B cmakebuild ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' . - '-DBUILD_CURL_EXE=OFF ' . // disable curl.exe - '-DBUILD_TESTING=OFF ' . // disable tests - '-DBUILD_EXAMPLES=OFF ' . // disable examples - '-DUSE_LIBIDN2=OFF ' . // disable libidn2 - '-DCURL_USE_LIBPSL=OFF ' . // disable libpsl - '-DUSE_WINDOWS_SSPI=ON ' . // use Schannel instead of OpenSSL - '-DCURL_USE_SCHANNEL=ON ' . // use Schannel instead of OpenSSL - '-DCURL_USE_OPENSSL=OFF ' . // disable openssl due to certificate issue - '-DCURL_ENABLE_SSL=ON ' . - '-DUSE_NGHTTP2=ON ' . // enable nghttp2 - '-DSHARE_LIB_OBJECT=OFF ' . // disable shared lib object - '-DCURL_USE_LIBSSH2=ON ' . // enable libssh2 - '-DENABLE_IPV6=ON ' . // enable ipv6 - $alt - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build cmakebuild --config Release --target install -j{$this->builder->concurrency}" - ); - // move libcurl.lib to libcurl_a.lib - rename(BUILD_LIB_PATH . '\libcurl.lib', BUILD_LIB_PATH . '\libcurl_a.lib'); - - FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\curl\curl.h', '#ifdef CURL_STATICLIB', '#if 1'); - } -} diff --git a/src/SPC/builder/windows/library/freetype.php b/src/SPC/builder/windows/library/freetype.php deleted file mode 100644 index 8401b4de4..000000000 --- a/src/SPC/builder/windows/library/freetype.php +++ /dev/null @@ -1,38 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DFT_DISABLE_BROTLI=TRUE ' . - '-DFT_DISABLE_BZIP2=TRUE ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - // freetype.lib to libfreetype_a.lib - copy(BUILD_LIB_PATH . '\freetype.lib', BUILD_LIB_PATH . '\libfreetype_a.lib'); - } -} diff --git a/src/SPC/builder/windows/library/gmssl.php b/src/SPC/builder/windows/library/gmssl.php deleted file mode 100644 index ed991635c..000000000 --- a/src/SPC/builder/windows/library/gmssl.php +++ /dev/null @@ -1,33 +0,0 @@ -source_dir . '\builddir'); - - // start build - cmd()->cd($this->source_dir . '\builddir') - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake .. -G "NMake Makefiles" -DWIN32=ON -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" -DCMAKE_CXX_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG" -DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH), - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH - ); - - FileSystem::writeFile($this->source_dir . '\builddir\cmake_install.cmake', 'set(CMAKE_INSTALL_PREFIX "' . str_replace('\\', '/', BUILD_ROOT_PATH) . '")' . PHP_EOL . FileSystem::readFile($this->source_dir . '\builddir\cmake_install.cmake')); - - cmd()->cd($this->source_dir . '\builddir') - ->execWithWrapper( - $this->builder->makeSimpleWrapper('nmake'), - 'install XCFLAGS=/MT' - ); - } -} diff --git a/src/SPC/builder/windows/library/icu_static_win.php b/src/SPC/builder/windows/library/icu_static_win.php deleted file mode 100644 index fdd9e6cd4..000000000 --- a/src/SPC/builder/windows/library/icu_static_win.php +++ /dev/null @@ -1,27 +0,0 @@ -source_dir}\\lib\\icudt.lib", "{$this->getLibDir()}\\icudt.lib"); - copy("{$this->source_dir}\\lib\\icuin.lib", "{$this->getLibDir()}\\icuin.lib"); - copy("{$this->source_dir}\\lib\\icuio.lib", "{$this->getLibDir()}\\icuio.lib"); - copy("{$this->source_dir}\\lib\\icuuc.lib", "{$this->getLibDir()}\\icuuc.lib"); - - // create libpq folder in buildroot/includes/libpq - if (!file_exists("{$this->getIncludeDir()}\\unicode")) { - mkdir("{$this->getIncludeDir()}\\unicode"); - } - - FileSystem::copyDir("{$this->source_dir}\\include\\unicode", "{$this->getIncludeDir()}\\unicode"); - } -} diff --git a/src/SPC/builder/windows/library/libaom.php b/src/SPC/builder/windows/library/libaom.php deleted file mode 100644 index 06d53cbc7..000000000 --- a/src/SPC/builder/windows/library/libaom.php +++ /dev/null @@ -1,41 +0,0 @@ -source_dir . '\builddir'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-S . -B builddir ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DAOM_TARGET_CPU=generic ' . - '-DENABLE_DOCS=OFF ' . - '-DENABLE_EXAMPLES=OFF ' . - '-DENABLE_TESTDATA=OFF ' . - '-DENABLE_TESTS=OFF ' . - '-DENABLE_TOOLS=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build builddir --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/libavif.php b/src/SPC/builder/windows/library/libavif.php deleted file mode 100644 index c9c966a75..000000000 --- a/src/SPC/builder/windows/library/libavif.php +++ /dev/null @@ -1,40 +0,0 @@ -source_dir . '\src\read.c', 'avifFileType ftyp = {};', 'avifFileType ftyp = { 0 };'); - // reset cmake - FileSystem::resetDir($this->source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DAVIF_BUILD_APPS=OFF ' . - '-DAVIF_BUILD_TESTS=OFF ' . - '-DAVIF_LIBYUV=OFF ' . - '-DAVID_ENABLE_GTEST=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/libffi_win.php b/src/SPC/builder/windows/library/libffi_win.php deleted file mode 100644 index 8d92acdf4..000000000 --- a/src/SPC/builder/windows/library/libffi_win.php +++ /dev/null @@ -1,52 +0,0 @@ -vs_ver_dir = match ($ver = SystemUtil::findVisualStudio()['version']) { - 'vs17' => '\win32\vs17_x64', - 'vs16' => '\win32\vs16_x64', - default => throw new EnvironmentException("Current VS version {$ver} is not supported !"), - }; - } - - protected function build(): void - { - // start build - cmd()->cd($this->source_dir . $this->vs_ver_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('msbuild'), - 'libffi-msvc.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64' - ); - FileSystem::createDir(BUILD_LIB_PATH); - FileSystem::createDir(BUILD_INCLUDE_PATH); - - FileSystem::copy("{$this->source_dir}{$this->vs_ver_dir}\\x64\\Release\\libffi.lib", BUILD_LIB_PATH . '\libffi.lib'); - FileSystem::copy("{$this->source_dir}{$this->vs_ver_dir}\\x64\\Release\\libffi.pdb", BUILD_LIB_PATH . '\libffi.pdb'); - FileSystem::copy($this->source_dir . '\include\ffi.h', BUILD_INCLUDE_PATH . '\ffi.h'); - - FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\ffi.h', '#define LIBFFI_H', "#define LIBFFI_H\n#define FFI_BUILDING"); - FileSystem::copy($this->source_dir . '\src\x86\ffitarget.h', BUILD_INCLUDE_PATH . '\ffitarget.h'); - FileSystem::copy($this->source_dir . '\fficonfig.h', BUILD_INCLUDE_PATH . '\fficonfig.h'); - - // copy($this->source_dir . '\msvc_build\out\static-Release\X64\libffi.lib', BUILD_LIB_PATH . '\libffi.lib'); - // copy($this->source_dir . '\msvc_build\include\ffi.h', BUILD_INCLUDE_PATH . '\ffi.h'); - // copy($this->source_dir . '\msvc_build\include\fficonfig.h', BUILD_INCLUDE_PATH . '\fficonfig.h'); - // copy($this->source_dir . '\src\x86\ffitarget.h', BUILD_INCLUDE_PATH . '\ffitarget.h'); - - // FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\ffi.h', '..\..\src\x86\ffitarget.h', 'ffitarget.h'); - } -} diff --git a/src/SPC/builder/windows/library/libiconv_win.php b/src/SPC/builder/windows/library/libiconv_win.php deleted file mode 100644 index 36d7e79e1..000000000 --- a/src/SPC/builder/windows/library/libiconv_win.php +++ /dev/null @@ -1,40 +0,0 @@ -vs_ver_dir = match ($ver = SystemUtil::findVisualStudio()['version']) { - 'vs17' => '\MSVC17', - 'vs16' => '\MSVC16', - default => throw new EnvironmentException("Current VS version {$ver} is not supported yet!"), - }; - } - - protected function build(): void - { - // start build - cmd()->cd($this->source_dir . $this->vs_ver_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('msbuild'), - 'libiconv.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64' - ); - FileSystem::createDir(BUILD_LIB_PATH); - FileSystem::createDir(BUILD_INCLUDE_PATH); - copy("{$this->source_dir}{$this->vs_ver_dir}\\x64\\lib\\libiconv.lib", BUILD_LIB_PATH . '\libiconv.lib'); - copy("{$this->source_dir}{$this->vs_ver_dir}\\x64\\lib\\libiconv_a.lib", BUILD_LIB_PATH . '\libiconv_a.lib'); - copy($this->source_dir . '\source\include\iconv.h', BUILD_INCLUDE_PATH . '\iconv.h'); - } -} diff --git a/src/SPC/builder/windows/library/libjpeg.php b/src/SPC/builder/windows/library/libjpeg.php deleted file mode 100644 index 0e5a69994..000000000 --- a/src/SPC/builder/windows/library/libjpeg.php +++ /dev/null @@ -1,42 +0,0 @@ -builder->getLib('zlib') ? 'ON' : 'OFF'; - // reset cmake - FileSystem::resetDir($this->source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DENABLE_SHARED=OFF ' . - '-DENABLE_STATIC=ON ' . - '-DBUILD_TESTING=OFF ' . - '-DWITH_JAVA=OFF ' . - '-DWITH_SIMD=OFF ' . - '-DWITH_CRT_DLL=OFF ' . - "-DENABLE_ZLIB_COMPRESSION={$zlib} " . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - copy(BUILD_LIB_PATH . '\jpeg-static.lib', BUILD_LIB_PATH . '\libjpeg_a.lib'); - } -} diff --git a/src/SPC/builder/windows/library/libpng.php b/src/SPC/builder/windows/library/libpng.php deleted file mode 100644 index a722fa79e..000000000 --- a/src/SPC/builder/windows/library/libpng.php +++ /dev/null @@ -1,40 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DSKIP_INSTALL_PROGRAM=ON ' . - '-DSKIP_INSTALL_FILES=ON ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DPNG_STATIC=ON ' . - '-DPNG_SHARED=OFF ' . - '-DPNG_TESTS=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - copy(BUILD_LIB_PATH . '\libpng16_static.lib', BUILD_LIB_PATH . '\libpng_a.lib'); - } -} diff --git a/src/SPC/builder/windows/library/librabbitmq.php b/src/SPC/builder/windows/library/librabbitmq.php deleted file mode 100644 index 19c33a31e..000000000 --- a/src/SPC/builder/windows/library/librabbitmq.php +++ /dev/null @@ -1,36 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - rename(BUILD_LIB_PATH . '\librabbitmq.4.lib', BUILD_LIB_PATH . '\rabbitmq.4.lib'); - } -} diff --git a/src/SPC/builder/windows/library/libsodium.php b/src/SPC/builder/windows/library/libsodium.php deleted file mode 100644 index c2ddb9acd..000000000 --- a/src/SPC/builder/windows/library/libsodium.php +++ /dev/null @@ -1,63 +0,0 @@ -vs_ver_dir = match ($ver = SystemUtil::findVisualStudio()['version']) { - 'vs17' => '\vs2022', - 'vs16' => '\vs2019', - default => throw new EnvironmentException("Current VS version {$ver} is not supported yet!"), - }; - } - - public function patchBeforeBuild(): bool - { - FileSystem::replaceFileStr($this->source_dir . '\src\libsodium\include\sodium\export.h', '#ifdef SODIUM_STATIC', '#if 1'); - return true; - } - - protected function build(): void - { - // start build - cmd()->cd($this->source_dir . '\builds\msvc' . $this->vs_ver_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('msbuild'), - 'libsodium.sln /t:Rebuild /p:Configuration=StaticRelease /p:Platform=x64 /p:PreprocessorDefinitions="SODIUM_STATIC=1"' - ); - FileSystem::createDir(BUILD_LIB_PATH); - FileSystem::createDir(BUILD_INCLUDE_PATH); - - // copy include - FileSystem::copyDir($this->source_dir . '\src\libsodium\include\sodium', BUILD_INCLUDE_PATH . '\sodium'); - copy($this->source_dir . '\src\libsodium\include\sodium.h', BUILD_INCLUDE_PATH . '\sodium.h'); - // copy lib - $ls = FileSystem::scanDirFiles($this->source_dir . '\bin'); - $find = false; - foreach ($ls as $file) { - if (str_ends_with($file, 'libsodium.lib')) { - copy($file, BUILD_LIB_PATH . '\libsodium.lib'); - $find = true; - } - if (str_ends_with($file, 'libsodium.pdb')) { - copy($file, BUILD_LIB_PATH . '\libsodium.pdb'); - } - } - if (!$find) { - throw new BuildFailureException("Build libsodium success, but cannot find libsodium.lib in {$this->source_dir}\\bin ."); - } - } -} diff --git a/src/SPC/builder/windows/library/libssh2.php b/src/SPC/builder/windows/library/libssh2.php deleted file mode 100644 index f064e1396..000000000 --- a/src/SPC/builder/windows/library/libssh2.php +++ /dev/null @@ -1,38 +0,0 @@ -builder->getLib('zlib') ? 'ON' : 'OFF'; - // reset cmake - FileSystem::resetDir($this->source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DBUILD_TESTING=OFF ' . - "-DENABLE_ZLIB_COMPRESSION={$zlib} " . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/libwebp.php b/src/SPC/builder/windows/library/libwebp.php deleted file mode 100644 index 96813e68e..000000000 --- a/src/SPC/builder/windows/library/libwebp.php +++ /dev/null @@ -1,52 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DWEBP_LINK_STATIC=ON ' . - '-DWEBP_BUILD_ANIM_UTILS=OFF ' . - '-DWEBP_BUILD_CWEBP=OFF ' . - '-DWEBP_BUILD_DWEBP=OFF ' . - '-DWEBP_BUILD_GIF2WEBP=OFF ' . - '-DWEBP_BUILD_IMG2WEBP=OFF ' . - '-DWEBP_BUILD_VWEBP=OFF ' . - '-DWEBP_BUILD_WEBPINFO=OFF ' . - '-DWEBP_BUILD_LIBWEBPMUX=OFF ' . - '-DWEBP_BUILD_WEBPMUX=OFF ' . - '-DWEBP_BUILD_EXTRAS=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - - // Actually we don't need pkgconf in windows, but for packing, we still need patching prefix. - // for libsharpyuv, libwebp, libwebpdecoder, libwebpdemux - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '\pkgconfig\libsharpyuv.pc', '/^prefix=.*/m', 'prefix=${pcfiledir}/../..'); - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '\pkgconfig\libwebp.pc', '/^prefix=.*/m', 'prefix=${pcfiledir}/../..'); - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '\pkgconfig\libwebpdecoder.pc', '/^prefix=.*/m', 'prefix=${pcfiledir}/../..'); - FileSystem::replaceFileRegex(BUILD_LIB_PATH . '\pkgconfig\libwebpdemux.pc', '/^prefix=.*/m', 'prefix=${pcfiledir}/../..'); - } -} diff --git a/src/SPC/builder/windows/library/libxml2.php b/src/SPC/builder/windows/library/libxml2.php deleted file mode 100644 index d4a4b9b40..000000000 --- a/src/SPC/builder/windows/library/libxml2.php +++ /dev/null @@ -1,44 +0,0 @@ -builder->getLib('zlib') ? 'ON' : 'OFF'; - // reset cmake - FileSystem::resetDir($this->source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBUILD_STATIC_LIBS=ON ' . - "-DLIBXML2_WITH_ZLIB={$zlib} " . - '-DLIBXML2_WITH_PYTHON=OFF ' . - '-DLIBXML2_WITH_ICONV=ON ' . - '-DIconv_LIBRARY=' . BUILD_LIB_PATH . ' ' . - '-DIconv_INCLUDE_DIR=' . BUILD_INCLUDE_PATH . ' ' . - '-DLIBXML2_WITH_LZMA=OFF ' . // xz not supported yet - '-DLIBXML2_WITH_PROGRAMS=OFF ' . - '-DLIBXML2_WITH_TESTS=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - copy(BUILD_LIB_PATH . '\libxml2s.lib', BUILD_LIB_PATH . '\libxml2_a.lib'); - } -} diff --git a/src/SPC/builder/windows/library/libyaml.php b/src/SPC/builder/windows/library/libyaml.php deleted file mode 100644 index 5d4026c58..000000000 --- a/src/SPC/builder/windows/library/libyaml.php +++ /dev/null @@ -1,44 +0,0 @@ -source_dir . '\build'); - - // check missing files: cmake\config.h.in and .\YamlConfig.cmake.in - if (!file_exists($this->source_dir . '\cmake\config.h.in')) { - FileSystem::createDir($this->source_dir . '\cmake'); - copy(ROOT_DIR . '\src\globals\extra\libyaml_config.h.in', $this->source_dir . '\cmake\config.h.in'); - } - if (!file_exists($this->source_dir . '\YamlConfig.cmake.in')) { - copy(ROOT_DIR . '\src\globals\extra\libyaml_YamlConfig.cmake.in', $this->source_dir . '\YamlConfig.cmake.in'); - } - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBUILD_TESTING=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/libzip.php b/src/SPC/builder/windows/library/libzip.php deleted file mode 100644 index 9a8206af2..000000000 --- a/src/SPC/builder/windows/library/libzip.php +++ /dev/null @@ -1,46 +0,0 @@ -source_dir . '\build'); - - $openssl = $this->builder->getLib('openssl') ? 'ON' : 'OFF'; - $zstd = $this->builder->getLib('zstd') ? 'ON' : 'OFF'; - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DENABLE_BZIP2=ON ' . - '-DENABLE_LZMA=ON ' . - "-DENABLE_ZSTD={$zstd} " . - "-DENABLE_OPENSSL={$openssl} " . - '-DBUILD_TOOLS=OFF ' . - '-DBUILD_DOC=OFF ' . - '-DBUILD_EXAMPLES=OFF ' . - '-DBUILD_REGRESS=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - copy(BUILD_LIB_PATH . '\zip.lib', BUILD_LIB_PATH . '\libzip_a.lib'); - } -} diff --git a/src/SPC/builder/windows/library/nghttp2.php b/src/SPC/builder/windows/library/nghttp2.php deleted file mode 100644 index 5a1c6bf15..000000000 --- a/src/SPC/builder/windows/library/nghttp2.php +++ /dev/null @@ -1,44 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DENABLE_SHARED_LIB=OFF ' . - '-DENABLE_STATIC_LIB=ON ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DENABLE_STATIC_CRT=ON ' . - '-DENABLE_LIB_ONLY=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' . - '-DENABLE_STATIC_CRT=ON ' . - '-DENABLE_DOC=OFF ' . - '-DBUILD_TESTING=OFF ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - - FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\nghttp2\nghttp2.h', '#ifdef NGHTTP2_STATICLIB', '#if 1'); - } -} diff --git a/src/SPC/builder/windows/library/nghttp3.php b/src/SPC/builder/windows/library/nghttp3.php deleted file mode 100644 index 1586a4d84..000000000 --- a/src/SPC/builder/windows/library/nghttp3.php +++ /dev/null @@ -1,39 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DENABLE_SHARED_LIB=OFF ' . - '-DENABLE_STATIC_LIB=ON ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DENABLE_STATIC_CRT=ON ' . - '-DENABLE_LIB_ONLY=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/ngtcp2.php b/src/SPC/builder/windows/library/ngtcp2.php deleted file mode 100644 index 79895dd6b..000000000 --- a/src/SPC/builder/windows/library/ngtcp2.php +++ /dev/null @@ -1,40 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DENABLE_SHARED_LIB=OFF ' . - '-DENABLE_STATIC_LIB=ON ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DENABLE_STATIC_CRT=ON ' . - '-DENABLE_LIB_ONLY=ON ' . - '-DENABLE_OPENSSL=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - } -} diff --git a/src/SPC/builder/windows/library/onig.php b/src/SPC/builder/windows/library/onig.php deleted file mode 100644 index 11ce815e4..000000000 --- a/src/SPC/builder/windows/library/onig.php +++ /dev/null @@ -1,37 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DBUILD_STATIC_LIBS=ON ' . - '-DMSVC_STATIC_RUNTIME=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - copy(BUILD_LIB_PATH . '/onig.lib', BUILD_LIB_PATH . '/onig_a.lib'); - } -} diff --git a/src/SPC/builder/windows/library/openssl.php b/src/SPC/builder/windows/library/openssl.php deleted file mode 100644 index 491a33b4d..000000000 --- a/src/SPC/builder/windows/library/openssl.php +++ /dev/null @@ -1,61 +0,0 @@ -perl = file_exists($perl_path_native) ? ($perl_path_native) : SystemUtil::findCommand('perl.exe'); - if ($this->perl === null) { - throw new EnvironmentException( - 'You need to install perl first!', - "Please run \"{$argv[0]} doctor\" to fix the environment.", - ); - } - } - - protected function build(): void - { - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper($this->perl), - 'Configure zlib VC-WIN64A ' . - 'no-shared ' . - '--prefix=' . quote(BUILD_ROOT_PATH) . ' ' . - '--with-zlib-lib=' . quote(BUILD_LIB_PATH) . ' ' . - '--with-zlib-include=' . quote(BUILD_INCLUDE_PATH) . ' ' . - '--release ' . - 'no-legacy ' - ); - - // patch zlib - FileSystem::replaceFileStr($this->source_dir . '\Makefile', 'ZLIB1', 'zlibstatic.lib'); - // patch debug: https://stackoverflow.com/questions/18486243/how-do-i-build-openssl-statically-linked-against-windows-runtime - FileSystem::replaceFileStr($this->source_dir . '\Makefile', '/debug', '/incremental:no /opt:icf /dynamicbase /nxcompat /ltcg /nodefaultlib:msvcrt'); - cmd()->cd($this->source_dir)->execWithWrapper( - $this->builder->makeSimpleWrapper('nmake'), - 'install_dev ' . - 'CNF_LDFLAGS="/NODEFAULTLIB:kernel32.lib /NODEFAULTLIB:msvcrt /NODEFAULTLIB:msvcrtd /DEFAULTLIB:libcmt /LIBPATH:' . BUILD_LIB_PATH . ' zlibstatic.lib"' - ); - copy($this->source_dir . '\ms\applink.c', BUILD_INCLUDE_PATH . '\openssl\applink.c'); - - FileSystem::replaceFileRegex( - BUILD_LIB_PATH . '\cmake\OpenSSL\OpenSSLConfig.cmake', - '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', - 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}" ws2_32.lib gdi32.lib advapi32.lib crypt32.lib user32.lib)' - ); - } -} diff --git a/src/SPC/builder/windows/library/postgresql_win.php b/src/SPC/builder/windows/library/postgresql_win.php deleted file mode 100644 index 7f708be69..000000000 --- a/src/SPC/builder/windows/library/postgresql_win.php +++ /dev/null @@ -1,27 +0,0 @@ -source_dir . '\pgsql\lib\libpq.lib', BUILD_LIB_PATH . '\libpq.lib'); - copy($this->source_dir . '\pgsql\lib\libpgport.lib', BUILD_LIB_PATH . '\libpgport.lib'); - copy($this->source_dir . '\pgsql\lib\libpgcommon.lib', BUILD_LIB_PATH . '\libpgcommon.lib'); - - // create libpq folder in buildroot/includes/libpq - if (!file_exists(BUILD_INCLUDE_PATH . '\libpq')) { - mkdir(BUILD_INCLUDE_PATH . '\libpq'); - } - - $headerFiles = ['libpq-fe.h', 'postgres_ext.h', 'pg_config_ext.h', 'libpq\libpq-fs.h']; - foreach ($headerFiles as $header) { - copy($this->source_dir . '\pgsql\include\\' . $header, BUILD_INCLUDE_PATH . '\\' . $header); - } - } -} diff --git a/src/SPC/builder/windows/library/pthreads4w.php b/src/SPC/builder/windows/library/pthreads4w.php deleted file mode 100644 index 66707cbb8..000000000 --- a/src/SPC/builder/windows/library/pthreads4w.php +++ /dev/null @@ -1,29 +0,0 @@ -cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper( - 'nmake /E /nologo /f Makefile ' . - 'DESTROOT=' . BUILD_ROOT_PATH . ' ' . - 'XCFLAGS="/MT" ' . // no dll - 'EHFLAGS="/I. /DHAVE_CONFIG_H /Os /Ob2 /D__PTW32_STATIC_LIB /D__PTW32_BUILD_INLINED"' - ), - 'pthreadVC3.inlined_static_stamp' - ); - copy($this->source_dir . '\libpthreadVC3.lib', BUILD_LIB_PATH . '\libpthreadVC3.lib'); - copy($this->source_dir . '\_ptw32.h', BUILD_INCLUDE_PATH . '\_ptw32.h'); - copy($this->source_dir . '\pthread.h', BUILD_INCLUDE_PATH . '\pthread.h'); - copy($this->source_dir . '\sched.h', BUILD_INCLUDE_PATH . '\sched.h'); - copy($this->source_dir . '\semaphore.h', BUILD_INCLUDE_PATH . '\semaphore.h'); - } -} diff --git a/src/SPC/builder/windows/library/qdbm.php b/src/SPC/builder/windows/library/qdbm.php deleted file mode 100644 index 67617b346..000000000 --- a/src/SPC/builder/windows/library/qdbm.php +++ /dev/null @@ -1,24 +0,0 @@ -cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('nmake'), - '/f VCMakefile' - ); - copy($this->source_dir . '\qdbm_a.lib', BUILD_LIB_PATH . '\qdbm_a.lib'); - copy($this->source_dir . '\depot.h', BUILD_INCLUDE_PATH . '\depot.h'); - // FileSystem::copyDir($this->source_dir . '\include\curl', BUILD_INCLUDE_PATH . '\curl'); - } -} diff --git a/src/SPC/builder/windows/library/sqlite.php b/src/SPC/builder/windows/library/sqlite.php deleted file mode 100644 index 096c1e97c..000000000 --- a/src/SPC/builder/windows/library/sqlite.php +++ /dev/null @@ -1,21 +0,0 @@ -source_dir . '/Makefile'); - return true; - } - - protected function build(): void - { - cmd()->cd($this->source_dir)->execWithWrapper($this->builder->makeSimpleWrapper('nmake'), 'PREFIX=' . BUILD_ROOT_PATH . ' install-static'); - } -} diff --git a/src/SPC/builder/windows/library/xz.php b/src/SPC/builder/windows/library/xz.php deleted file mode 100644 index a48df52dc..000000000 --- a/src/SPC/builder/windows/library/xz.php +++ /dev/null @@ -1,39 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - - // copy liblzma.lib to liblzma_a.lib - copy(BUILD_LIB_PATH . '/lzma.lib', BUILD_LIB_PATH . '/liblzma_a.lib'); - // patch lzma.h - FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '/lzma.h', 'defined(LZMA_API_STATIC)', 'defined(_WIN32)'); - } -} diff --git a/src/SPC/builder/windows/library/zlib.php b/src/SPC/builder/windows/library/zlib.php deleted file mode 100644 index a5fd346ba..000000000 --- a/src/SPC/builder/windows/library/zlib.php +++ /dev/null @@ -1,54 +0,0 @@ -source_dir . '\build'); - - // start build - cmd()->cd($this->source_dir) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - '-B build ' . - '-A x64 ' . - "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . - '-DCMAKE_BUILD_TYPE=Release ' . - '-DBUILD_SHARED_LIBS=OFF ' . - '-DSKIP_INSTALL_FILES=ON ' . - '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' - ) - ->execWithWrapper( - $this->builder->makeSimpleWrapper('cmake'), - "--build build --config Release --target install -j{$this->builder->concurrency}" - ); - $detect_list = [ - 'zlibstatic.lib', - 'zs.lib', - 'libzs.lib', - 'libz.lib', - ]; - foreach ($detect_list as $item) { - if (file_exists(BUILD_LIB_PATH . '\\' . $item)) { - FileSystem::copy(BUILD_LIB_PATH . '\\' . $item, BUILD_LIB_PATH . '\zlib_a.lib'); - FileSystem::copy(BUILD_LIB_PATH . '\\' . $item, BUILD_LIB_PATH . '\zlibstatic.lib'); - break; - } - } - FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '\bin\zlib.dll'); - FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\zlib.lib'); - FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\libz.dll'); - FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\libz.lib'); - FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\z.lib'); - FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\z.dll'); - } -} diff --git a/src/SPC/command/BaseCommand.php b/src/SPC/command/BaseCommand.php deleted file mode 100644 index 745dbe28a..000000000 --- a/src/SPC/command/BaseCommand.php +++ /dev/null @@ -1,213 +0,0 @@ -addOption('debug', null, null, 'Enable debug mode'); - $this->addOption('no-motd', null, null, 'Disable motd'); - $this->addOption('preserve-log', null, null, 'Preserve log files, do not delete them on initialized'); - } - - public function initialize(InputInterface $input, OutputInterface $output): void - { - if ($input->getOption('no-motd')) { - $this->no_motd = true; - } - - set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) { - $tips = [ - E_WARNING => ['PHP Warning: ', 'warning'], - E_NOTICE => ['PHP Notice: ', 'notice'], - E_USER_ERROR => ['PHP Error: ', 'error'], - E_USER_WARNING => ['PHP Warning: ', 'warning'], - E_USER_NOTICE => ['PHP Notice: ', 'notice'], - E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'], - E_DEPRECATED => ['PHP Deprecated: ', 'notice'], - E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'], - ]; - $level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error']; - $error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line; - logger()->{$level_tip[1]}($error); - // 如果 return false 则错误会继续递交给 PHP 标准错误处理 - return true; - }); - $version = ConsoleApplication::VERSION; - if (!$this->no_motd) { - echo " _ _ _ _ - ___| |_ __ _| |_(_) ___ _ __ | |__ _ __ -/ __| __/ _` | __| |/ __|____| '_ \\| '_ \\| '_ \\ -\\__ \\ || (_| | |_| | (_|_____| |_) | | | | |_) | -|___/\\__\\__,_|\\__|_|\\___| | .__/|_| |_| .__/ v{$version} - |_| |_| -"; - } - } - - abstract public function handle(): int; - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->input = $input; - $this->output = $output; - - // init log - $this->initLogFiles(); - - // init logger - $this->initConsoleLogger(); - - // load attribute maps - AttributeMapper::init(); - - // init windows fallback - $this->initWindowsPromptFallback($input, $output); - - // init GlobalEnv - if (!$this instanceof BuildCommand) { - GlobalEnvManager::init(); - f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes'); - } - - try { - // show raw argv list for logger()->debug - logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); - return $this->handle(); - } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { - // Handle SPCException and log it - ExceptionHandler::handleSPCException($e); - return static::FAILURE; - } catch (\Throwable $e) { - // Handle any other exceptions - ExceptionHandler::handleDefaultException($e); - return static::FAILURE; - } - } - - protected function getOption(string $name): mixed - { - return $this->input->getOption($name); - } - - protected function getArgument(string $name): mixed - { - return $this->input->getArgument($name); - } - - protected function logWithResult(bool $result, string $success_msg, string $fail_msg): int - { - if ($result) { - logger()->info($success_msg); - return static::SUCCESS; - } - logger()->error($fail_msg); - return static::FAILURE; - } - - /** - * Parse extension list from string, replace alias and filter internal extensions. - * - * @param array|string $ext_list Extension string list, e.g. "mbstring,posix,sockets" or array - */ - protected function parseExtensionList(array|string $ext_list): array - { - // replace alias - $ls = array_map(function ($x) { - $lower = strtolower(trim($x)); - if (isset(SPC_EXTENSION_ALIAS[$lower])) { - logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.'); - return SPC_EXTENSION_ALIAS[$lower]; - } - return $lower; - }, is_array($ext_list) ? $ext_list : array_filter(explode(',', $ext_list))); - - // filter internals - return array_values(array_filter($ls, function ($x) { - if (in_array($x, SPC_INTERNAL_EXTENSIONS)) { - logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored."); - return false; - } - return true; - })); - } - - /** - * Initialize spc log files. - */ - private function initLogFiles(): void - { - $log_dir = SPC_LOGS_DIR; - if (!file_exists($log_dir)) { - mkdir($log_dir, 0755, true); - } elseif (!$this->getOption('preserve-log')) { - // Clean up old log files - $files = glob($log_dir . '/*.log'); - foreach ($files as $file) { - if (is_file($file)) { - unlink($file); - } - } - } - } - - /** - * Initialize console logger. - */ - private function initConsoleLogger(): void - { - global $ob_logger; - if ($this->input->getOption('debug') || $this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { - $ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$this->input->getOption('no-ansi')); - define('DEBUG_MODE', true); - } else { - $ob_logger = new ConsoleLogger(decorated: !$this->input->getOption('no-ansi')); - } - $log_file_fd = fopen(SPC_OUTPUT_LOG, 'a'); - $ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) { - if ($log_file_fd) { - fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); - } - return true; - }); - } - - /** - * Initialize Windows prompt fallback for laravel-prompts. - */ - private function initWindowsPromptFallback(InputInterface $input, OutputInterface $output): void - { - Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows'); - ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) { - $helper = new QuestionHelper(); - $case = $prompt->default ? ' [Y/n] ' : ' [y/N] '; - $question = new ConfirmationQuestion($prompt->label . $case, $prompt->default); - return $helper->ask($input, $output, $question); - }); - } -} diff --git a/src/SPC/command/BuildCommand.php b/src/SPC/command/BuildCommand.php deleted file mode 100644 index 0285c3338..000000000 --- a/src/SPC/command/BuildCommand.php +++ /dev/null @@ -1,24 +0,0 @@ -addOption('with-sdk-binary-dir', null, InputOption::VALUE_REQUIRED, 'path to binary sdk'); - $this->addOption('vs-ver', null, InputOption::VALUE_REQUIRED, 'vs version, e.g. "17" for Visual Studio 2022'); - } - - $this->addOption('with-clean', null, null, 'fresh build, remove `source` and `buildroot` dir before build'); - $this->addOption('rebuild', 'r', null, 'Delete old build and rebuild'); - $this->addOption('enable-zts', null, null, 'enable ZTS support'); - } -} diff --git a/src/SPC/command/BuildLibsCommand.php b/src/SPC/command/BuildLibsCommand.php deleted file mode 100644 index de1f1bba0..000000000 --- a/src/SPC/command/BuildLibsCommand.php +++ /dev/null @@ -1,71 +0,0 @@ -addArgument('libraries', InputArgument::REQUIRED, 'The libraries will be compiled, comma separated'); - $this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); - $this->addOption('all', 'A', null, 'Build all libs that static-php-cli needed'); - } - - public function initialize(InputInterface $input, OutputInterface $output): void - { - // --all 等于 "" - if ($input->getOption('all')) { - $input->setArgument('libraries', ''); - } - parent::initialize($input, $output); - } - - public function handle(): int - { - // 从参数中获取要编译的 libraries,并转换为数组 - $libraries = array_map('trim', array_filter(explode(',', $this->getArgument('libraries')))); - - // 删除旧资源 - if ($this->getOption('clean')) { - logger()->warning('You are doing some operations that not recoverable: removing directories below'); - logger()->warning(BUILD_ROOT_PATH); - logger()->warning('I will remove these dir after you press [Enter] !'); - echo 'Confirm operation? [Yes] '; - fgets(STDIN); - if (PHP_OS_FAMILY === 'Windows') { - f_passthru('rmdir /s /q ' . BUILD_ROOT_PATH); - } else { - f_passthru('rm -rf ' . BUILD_ROOT_PATH); - } - } - - // 构建对象 - $builder = BuilderProvider::makeBuilderByInput($this->input); - // 只编译 library 的情况下,标记 - $builder->setLibsOnly(); - // 编译和检查库完整 - $libraries = DependencyUtil::getLibs($libraries); - $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package'])); - - logger()->info('Building libraries: ' . implode(',', $display_libs)); - sleep(2); - $builder->proveLibs($libraries); - $builder->validateLibsAndExts(); - $builder->setupLibs(); - - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Build libs complete, used ' . $time . ' s !'); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php deleted file mode 100644 index ad884b3ba..000000000 --- a/src/SPC/command/BuildPHPCommand.php +++ /dev/null @@ -1,315 +0,0 @@ -addArgument('extensions', InputArgument::REQUIRED, 'The extensions will be compiled, comma separated'); - $this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', ''); - $this->addOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); - $this->addOption('build-micro', null, null, 'Build micro SAPI'); - $this->addOption('build-cli', null, null, 'Build cli SAPI'); - $this->addOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); - $this->addOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); - $this->addOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); - $this->addOption('build-cgi', null, null, 'Build cgi SAPI'); - $this->addOption('build-all', null, null, 'Build all SAPI'); - $this->addOption('no-strip', null, null, 'build without strip, keep symbols to debug'); - $this->addOption('disable-opcache-jit', null, null, 'disable opcache jit'); - $this->addOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', $isWindows ? null : '/usr/local/etc/php'); - $this->addOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', $isWindows ? null : '/usr/local/etc/php/conf.d'); - $this->addOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); - $this->addOption('with-micro-fake-cli', null, null, 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"'); - $this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs'); - $this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts'); - $this->addOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside'); - $this->addOption('without-micro-ext-test', null, null, 'Disable phpmicro with extension test code'); - $this->addOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)'); - $this->addOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)'); - $this->addOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); - $this->addOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); - } - - public function handle(): int - { - // transform string to array - $libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs')))); - // transform string to array - $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); - // transform string to array - $static_extensions = $this->parseExtensionList($this->getArgument('extensions')); - - // parse rule with options - $rule = $this->parseRules($shared_extensions); - - // check dynamic extension build env - // linux must build with glibc - if (!empty($shared_extensions) && SPCTarget::isStatic()) { - $this->output->writeln('Linux does not support dynamic extension loading with fully static builds, please build with a shared C runtime target!'); - return static::FAILURE; - } - $static_and_shared = array_intersect($static_extensions, $shared_extensions); - if (!empty($static_and_shared)) { - $this->output->writeln('Building extensions [' . implode(',', $static_and_shared) . '] as both static and shared, tests may not be accurate or fail.'); - } - - if ($rule === BUILD_TARGET_NONE) { - $this->output->writeln('Please add at least one build SAPI!'); - $this->output->writeln("\t--build-cli\t\tBuild php-cli SAPI"); - $this->output->writeln("\t--build-micro\t\tBuild phpmicro SAPI"); - $this->output->writeln("\t--build-fpm\t\tBuild php-fpm SAPI"); - $this->output->writeln("\t--build-embed\t\tBuild embed SAPI/libphp"); - $this->output->writeln("\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp"); - $this->output->writeln("\t--build-all\t\tBuild all SAPI: cli, micro, fpm, embed, frankenphp"); - return static::FAILURE; - } - if ($rule === BUILD_TARGET_ALL) { - logger()->warning('--build-all option makes `--no-strip` always true, be aware!'); - } - if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO && $this->getOption('with-micro-logo')) { - $logo = $this->getOption('with-micro-logo'); - if (!file_exists($logo)) { - logger()->error('Logo file ' . $logo . ' not exist !'); - return static::FAILURE; - } - } - - // Check upx - $suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : ''; - if ($this->getOption('with-upx-pack')) { - // only available for linux for now - if (!in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) { - logger()->error('UPX is only available on Linux and Windows!'); - return static::FAILURE; - } - // need to install this manually - if (!file_exists(PKG_ROOT_PATH . '/bin/upx' . $suffix)) { - global $argv; - logger()->error('upx does not exist, please install it first:'); - logger()->error(''); - logger()->error("\t" . $argv[0] . ' install-pkg upx'); - logger()->error(''); - return static::FAILURE; - } - // exclusive with no-strip - if ($this->getOption('no-strip')) { - logger()->warning('--with-upx-pack conflicts with --no-strip, --no-strip won\'t work!'); - } - if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - logger()->warning('Some cases micro.sfx cannot be packed via UPX due to dynamic size bug, be aware!'); - } - } - // create builder - $builder = BuilderProvider::makeBuilderByInput($this->input); - $include_suggest_ext = $this->getOption('with-suggested-exts'); - $include_suggest_lib = $this->getOption('with-suggested-libs'); - [$extensions, $libraries, $not_included] = DependencyUtil::getExtsAndLibs(array_merge($static_extensions, $shared_extensions), $libraries, $include_suggest_ext, $include_suggest_lib); - $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package'])); - - // separate static and shared extensions from $extensions - // filter rule: including shared extensions if they are in $static_extensions or $shared_extensions - $static_extensions = array_filter($extensions, fn ($ext) => !in_array($ext, $shared_extensions) || in_array($ext, $static_extensions)); - - // print info - $indent_texts = [ - 'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')', - 'Build Target' => getenv('SPC_TARGET'), - 'Build Toolchain' => getenv('SPC_TOOLCHAIN'), - 'Build SAPI' => $builder->getBuildTypeName($rule), - 'Static Extensions (' . count($static_extensions) . ')' => implode(',', $static_extensions), - 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), - 'Libraries (' . count($libraries) . ')' => implode(',', $display_libs), - 'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes', - 'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no', - ]; - if (!empty($shared_extensions) || ($rule & BUILD_TARGET_EMBED)) { - $indent_texts['Build Dev'] = 'yes'; - } - if (!empty($this->input->getOption('with-config-file-path'))) { - $indent_texts['Config File Path'] = $this->input->getOption('with-config-file-path'); - } - if (!empty($this->input->getOption('with-hardcoded-ini'))) { - $indent_texts['Hardcoded INI'] = $this->input->getOption('with-hardcoded-ini'); - } - if ($this->input->getOption('disable-opcache-jit')) { - $indent_texts['Opcache JIT'] = 'disabled'; - } - if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) { - $indent_texts['UPX Pack'] = 'enabled'; - } - - $ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion(false); - $indent_texts['PHP Version'] = $ver; - - if (!empty($not_included)) { - $indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included); - } - $this->printFormatInfo($this->getDefinedEnvs(), true); - $this->printFormatInfo($indent_texts); - - // bind extra info to exception handler - ExceptionHandler::bindBuildPhpExtraInfo($indent_texts); - - logger()->notice('Build will start after 2s ...'); - sleep(2); - - // compile libraries - $builder->proveLibs($libraries); - // check extensions - $builder->proveExts($static_extensions, $shared_extensions); - // validate libs and extensions - $builder->validateLibsAndExts(); - - // check some things before building all the things - $builder->checkBeforeBuildPHP($rule); - - // clean builds and sources - if ($this->input->getOption('with-clean')) { - logger()->info('Cleaning source and previous build dir...'); - FileSystem::removeDir(SOURCE_PATH); - FileSystem::removeDir(BUILD_ROOT_PATH); - } - - // build or install libraries - $builder->setupLibs(); - - // Process -I option - $custom_ini = []; - foreach ($this->input->getOption('with-hardcoded-ini') as $value) { - [$source_name, $ini_value] = explode('=', $value, 2); - $custom_ini[$source_name] = $ini_value; - logger()->info('Adding hardcoded INI [' . $source_name . ' = ' . $ini_value . ']'); - } - if (!empty($custom_ini)) { - SourcePatcher::patchHardcodedINI($custom_ini); - } - - // add static-php-cli.version to main.c, in order to debug php failure more easily - SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion()); - - // clean old modules that may conflict with the new php build - FileSystem::removeDir(BUILD_MODULES_PATH); - // start to build - $builder->buildPHP($rule); - - $builder->testPHP($rule); - - // compile stopwatch :P - $time = round(microtime(true) - START_TIME, 3); - logger()->info(''); - logger()->info(' Build complete, used ' . $time . ' s !'); - logger()->info(''); - - // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- - $build_root_path = BUILD_ROOT_PATH; - $fixed = ''; - $build_root_path = get_display_path($build_root_path); - if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { - $fixed = ' (host system)'; - } - if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { - $win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : ''; - $path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}"); - logger()->info("Static php binary path{$fixed}: {$path}"); - } - if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - $path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx"); - logger()->info("phpmicro binary path{$fixed}: {$path}"); - } - if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') { - $path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm"); - logger()->info("Static php-fpm binary path{$fixed}: {$path}"); - } - if (!empty($shared_extensions)) { - foreach ($shared_extensions as $ext) { - $path = FileSystem::convertPath("{$build_root_path}/modules/{$ext}.so"); - if (file_exists(BUILD_MODULES_PATH . "/{$ext}.so")) { - logger()->info("Shared extension [{$ext}] path{$fixed}: {$path}"); - } elseif (Config::getExt($ext, 'type') !== 'addon') { - logger()->warning("Shared extension [{$ext}] not found, please check!"); - } - } - } - - // export metadata - file_put_contents(BUILD_ROOT_PATH . '/build-extensions.json', json_encode($extensions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - file_put_contents(BUILD_ROOT_PATH . '/build-libraries.json', json_encode($libraries, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - // export licenses - $dumper = new LicenseDumper(); - $dumper->addExts($extensions)->addLibs($libraries)->addSources(['php-src'])->dump(BUILD_ROOT_PATH . '/license'); - $path = FileSystem::convertPath("{$build_root_path}/license/"); - logger()->info("License path{$fixed}: {$path}"); - return static::SUCCESS; - } - - /** - * Parse build options to rule int. - */ - private function parseRules(array $shared_extensions = []): int - { - $rule = BUILD_TARGET_NONE; - $rule |= ($this->getOption('build-cli') ? BUILD_TARGET_CLI : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE); - $rule |= $this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE; - $rule |= ($this->getOption('build-frankenphp') ? (BUILD_TARGET_FRANKENPHP | BUILD_TARGET_EMBED) : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-cgi') ? BUILD_TARGET_CGI : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); - return $rule; - } - - private function getDefinedEnvs(): array - { - $envs = GlobalEnvManager::getInitializedEnv(); - $final = []; - foreach ($envs as $env) { - $exp = explode('=', $env, 2); - $final['Init var [' . $exp[0] . ']'] = $exp[1]; - } - return $final; - } - - private function printFormatInfo(array $indent_texts, bool $debug = false): void - { - // calculate space count for every line - $maxlen = 0; - foreach ($indent_texts as $k => $v) { - $maxlen = max(strlen($k), $maxlen); - } - foreach ($indent_texts as $k => $v) { - if (is_string($v)) { - /* @phpstan-ignore-next-line */ - logger()->{$debug ? 'debug' : 'info'}($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v)); - } elseif (is_array($v) && !is_assoc_array($v)) { - $first = array_shift($v); - /* @phpstan-ignore-next-line */ - logger()->{$debug ? 'debug' : 'info'}($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first)); - foreach ($v as $vs) { - /* @phpstan-ignore-next-line */ - logger()->{$debug ? 'debug' : 'info'}(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs)); - } - } - } - } -} diff --git a/src/SPC/command/CraftCommand.php b/src/SPC/command/CraftCommand.php deleted file mode 100644 index 6c40133d1..000000000 --- a/src/SPC/command/CraftCommand.php +++ /dev/null @@ -1,180 +0,0 @@ -addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml'); - } - - public function handle(): int - { - $craft_file = $this->getArgument('craft'); - // Check if the craft.yml file exists - if (!file_exists($craft_file)) { - $this->output->writeln('craft.yml not found, please create one!'); - return static::FAILURE; - } - - // Check if the craft.yml file is valid - try { - $craft = ConfigValidator::validateAndParseCraftFile($craft_file, $this); - if ($craft['debug']) { - $this->input->setOption('debug', true); - } - } catch (ValidationException $e) { - $this->output->writeln('craft.yml parse error: ' . $e->getMessage() . ''); - return static::FAILURE; - } - - // Craft!!! - $this->output->writeln('Crafting...'); - - // apply env - if (isset($craft['extra-env'])) { - $env = $craft['extra-env']; - foreach ($env as $key => $val) { - f_putenv("{$key}={$val}"); - } - } - - $static_extensions = implode(',', $craft['extensions']); - $shared_extensions = implode(',', $craft['shared-extensions'] ?? []); - $libs = implode(',', $craft['libs']); - - // craft doctor - if ($craft['craft-options']['doctor']) { - $retcode = $this->runCommand('doctor', '--auto-fix'); - if ($retcode !== 0) { - $this->output->writeln('craft doctor failed'); - return static::FAILURE; - } - } - // install go and xcaddy for frankenphp - if (in_array('frankenphp', $craft['sapi']) && !GoXcaddy::isInstalled()) { - $retcode = $this->runCommand('install-pkg', 'go-xcaddy'); - if ($retcode !== 0) { - $this->output->writeln('craft go-xcaddy failed'); - return static::FAILURE; - } - } - // install zig if requested - if (ToolchainManager::getToolchainClass() === ZigToolchain::class && !Zig::isInstalled()) { - $retcode = $this->runCommand('install-pkg', 'zig'); - if ($retcode !== 0) { - $this->output->writeln('craft zig failed'); - return static::FAILURE; - } - } - // craft download - if ($craft['craft-options']['download']) { - $sharedAppend = $shared_extensions ? ',' . $shared_extensions : ''; - $args = ["--for-extensions={$static_extensions}{$sharedAppend}"]; - if ($craft['libs'] !== []) { - $args[] = "--for-libs={$libs}"; - } - if (isset($craft['php-version'])) { - $args[] = '--with-php=' . $craft['php-version']; - if (!array_key_exists('ignore-cache-sources', $craft['download-options'])) { - $craft['download-options']['ignore-cache-sources'] = 'php-src'; - } - } - $this->optionsToArguments($craft['download-options'], $args); - $retcode = $this->runCommand('download', ...$args); - if ($retcode !== 0) { - $this->output->writeln('craft download failed'); - return static::FAILURE; - } - } - - // craft build - if ($craft['craft-options']['build']) { - $args = [$static_extensions, "--with-libs={$libs}", "--build-shared={$shared_extensions}", ...array_map(fn ($x) => "--build-{$x}", $craft['sapi'])]; - $this->optionsToArguments($craft['build-options'], $args); - $retcode = $this->runCommand('build', ...$args); - if ($retcode !== 0) { - $this->output->writeln('craft build failed'); - return static::FAILURE; - } - } - - return 0; - } - - public function processLogCallback($type, $buffer): void - { - if ($type === Process::ERR) { - fwrite(STDERR, $buffer); - } else { - fwrite(STDOUT, $buffer); - } - } - - private function runCommand(string $cmd, ...$args): int - { - global $argv; - if ($this->getOption('debug')) { - array_unshift($args, '--debug'); - } - array_unshift($args, '--preserve-log'); - $prefix = PHP_SAPI === 'cli' ? [PHP_BINARY, $argv[0]] : [$argv[0]]; - - $env = getenv(); - $process = new Process([...$prefix, $cmd, '--no-motd', ...$args], env: $env, timeout: null); - - if (PHP_OS_FAMILY === 'Windows') { - sapi_windows_set_ctrl_handler(function () use ($process) { - if ($process->isRunning()) { - $process->signal(-1073741510); - } - }); - } elseif (extension_loaded('pcntl')) { - pcntl_signal(SIGINT, function () use ($process) { - /* @noinspection PhpComposerExtensionStubsInspection */ - $process->signal(SIGINT); - }); - } else { - logger()->debug('You have not enabled `pcntl` extension, cannot prevent download file corruption when Ctrl+C'); - } - // $process->setTty(true); - $process->run([$this, 'processLogCallback']); - return $process->getExitCode(); - } - - private function optionsToArguments(array $options, array &$args): void - { - foreach ($options as $option => $val) { - if ((is_bool($val) && $val) || $val === null) { - $args[] = "--{$option}"; - - continue; - } - if (is_string($val)) { - $args[] = "--{$option}={$val}"; - - continue; - } - if (is_array($val)) { - foreach ($val as $v) { - if (is_string($v)) { - $args[] = "--{$option}={$v}"; - } - } - } - } - } -} diff --git a/src/SPC/command/DeleteDownloadCommand.php b/src/SPC/command/DeleteDownloadCommand.php deleted file mode 100644 index 82dd62c4a..000000000 --- a/src/SPC/command/DeleteDownloadCommand.php +++ /dev/null @@ -1,81 +0,0 @@ -addArgument('sources', InputArgument::REQUIRED, 'The sources/packages will be deleted, comma separated'); - $this->addOption('all', 'A', null, 'Delete all downloaded and locked sources/packages'); - $this->addOption('pre-built-only', 'W', null, 'Delete only pre-built sources/packages, not the original ones'); - $this->addOption('source-only', 'S', null, 'Delete only sources, not the pre-built packages'); - } - - public function initialize(InputInterface $input, OutputInterface $output): void - { - if ($input->getOption('all')) { - $input->setArgument('sources', ''); - } - parent::initialize($input, $output); - } - - public function handle(): int - { - // get source list that will be downloaded - $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); - if (empty($sources)) { - logger()->notice('Removing downloads/ directory ...'); - FileSystem::removeDir(DOWNLOAD_PATH); - logger()->info('Removed downloads/ dir!'); - return static::SUCCESS; - } - $chosen_sources = $sources; - - $deleted_sources = []; - foreach ($chosen_sources as $source) { - $source = trim($source); - if (LockFile::get($source) && !$this->getOption('pre-built-only')) { - $deleted_sources[] = $source; - } - if (LockFile::get(Downloader::getPreBuiltLockName($source)) && !$this->getOption('source-only')) { - $deleted_sources[] = Downloader::getPreBuiltLockName($source); - } - } - - foreach ($deleted_sources as $lock_name) { - $lock = LockFile::get($lock_name); - // remove download file/dir if exists - if ($lock['source_type'] === SPC_SOURCE_ARCHIVE) { - if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['filename']))) { - logger()->info('Deleting file ' . $path); - unlink($path); - } else { - logger()->warning("Source/Package [{$lock_name}] file not found, skip deleting file."); - } - } else { - if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['dirname']))) { - logger()->info('Deleting dir ' . $path); - FileSystem::removeDir($path); - } else { - logger()->warning("Source/Package [{$lock_name}] directory not found, skip deleting dir."); - } - } - // remove locked sources - LockFile::put($lock_name, null); - } - logger()->info('Delete success!'); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/DoctorCommand.php b/src/SPC/command/DoctorCommand.php deleted file mode 100644 index 77580b408..000000000 --- a/src/SPC/command/DoctorCommand.php +++ /dev/null @@ -1,95 +0,0 @@ -addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false); - } - - public function handle(): int - { - $fix_policy = match ($this->input->getOption('auto-fix')) { - 'never' => FIX_POLICY_DIE, - true, null => FIX_POLICY_AUTOFIX, - default => FIX_POLICY_PROMPT, - }; - $fix_map = AttributeMapper::getDoctorFixMap(); - - foreach (DoctorHandler::getValidCheckList() as $check) { - // output - $this->output->write("Checking {$check->item_name} ... "); - - // null => skipped - if (($result = call_user_func($check->callback)) === null) { - $this->output->writeln('skipped'); - continue; - } - // invalid return value => skipped - if (!$result instanceof CheckResult) { - $this->output->writeln('Skipped due to invalid return value'); - continue; - } - // true => OK - if ($result->isOK()) { - /* @phpstan-ignore-next-line */ - $this->output->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓')); - continue; - } - - // Failed => output error message - $this->output->writeln('' . $result->getMessage() . ''); - // If the result is not fixable, fail immediately - if ($result->getFixItem() === '') { - $this->output->writeln('This check item can not be fixed !'); - return static::FAILURE; - } - if (!isset($fix_map[$result->getFixItem()])) { - $this->output->writeln("Internal error: Unknown fix item: {$result->getFixItem()}"); - return static::FAILURE; - } - - // prompt for fix - if ($fix_policy === FIX_POLICY_PROMPT) { - if (!confirm('Do you want to fix it?')) { - $this->output->writeln('You canceled fix.'); - return static::FAILURE; - } - if (DoctorHandler::emitFix($this->output, $result)) { - $this->output->writeln('Fix applied successfully!'); - } else { - $this->output->writeln('Failed to apply fix!'); - return static::FAILURE; - } - } - - // auto fix - if ($fix_policy === FIX_POLICY_AUTOFIX) { - $this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...'); - if (DoctorHandler::emitFix($this->output, $result)) { - $this->output->writeln('Fix applied successfully!'); - } else { - $this->output->writeln('Failed to apply fix!'); - return static::FAILURE; - } - } - } - - $this->output->writeln('Doctor check complete !'); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php deleted file mode 100644 index b7f2b078b..000000000 --- a/src/SPC/command/DownloadCommand.php +++ /dev/null @@ -1,365 +0,0 @@ -addArgument('sources', InputArgument::REQUIRED, 'The sources will be compiled, comma separated'); - $this->addOption('shallow-clone', null, null, 'Clone shallow'); - $this->addOption('with-openssl11', null, null, 'Use openssl 1.1'); - $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'version in major.minor format (default 8.5)', '8.5'); - $this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); - $this->addOption('all', 'A', null, 'Fetch all sources that static-php-cli needed'); - $this->addOption('custom-url', 'U', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://downloads.php.net/~eric/php-8.3.0beta1.tar.gz"'); - $this->addOption('custom-git', 'G', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source git url, e.g "php-src:master:https://github.com/php/php-src.git"'); - $this->addOption('from-zip', 'Z', InputOption::VALUE_REQUIRED, 'Fetch from zip archive'); - $this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Fetch by extensions, e.g "openssl,mbstring"'); - $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Fetch by libraries, e.g "libcares,openssl,onig"'); - $this->addOption('without-suggestions', null, null, 'Do not fetch suggested sources when using --for-extensions'); - $this->addOption('ignore-cache-sources', null, InputOption::VALUE_OPTIONAL, 'Ignore some source caches, comma separated, e.g "php-src,curl,openssl"', false); - $this->addOption('retry', 'R', InputOption::VALUE_REQUIRED, 'Set retry time when downloading failed (default: 0)', '0'); - $this->addOption('prefer-pre-built', 'P', null, 'Download pre-built libraries when available'); - $this->addOption('no-alt', null, null, 'Do not download alternative sources'); - } - - public function initialize(InputInterface $input, OutputInterface $output): void - { - // mode: --all - if ($input->getOption('all')) { - $input->setArgument('sources', implode(',', array_keys(Config::getSources()))); - parent::initialize($input, $output); - return; - } - // mode: --clean and --from-zip - if ($input->getOption('clean') || $input->getOption('from-zip')) { - $input->setArgument('sources', ''); - parent::initialize($input, $output); - return; - } - // mode: normal - if (!empty($input->getArgument('sources'))) { - $final_sources = array_map('trim', array_filter(explode(',', $input->getArgument('sources')))); - } else { - $final_sources = []; - } - // mode: --for-extensions - if ($for_ext = $input->getOption('for-extensions')) { - $ext = $this->parseExtensionList($for_ext); - $sources = $this->calculateSourcesByExt($ext, !$input->getOption('without-suggestions')); - $final_sources = array_merge($final_sources, array_diff($sources, $final_sources)); - } - // mode: --for-libs - if ($for_lib = $input->getOption('for-libs')) { - $lib = array_map('trim', array_filter(explode(',', $for_lib))); - $sources = $this->calculateSourcesByLib($lib, !$input->getOption('without-suggestions')); - $final_sources = array_merge($final_sources, array_diff($sources, $final_sources)); - } - if (!empty($final_sources)) { - $input->setArgument('sources', implode(',', $final_sources)); - } - parent::initialize($input, $output); - } - - public function handle(): int - { - if ($this->getOption('clean')) { - return $this->_clean(); - } - - // --from-zip - if ($path = $this->getOption('from-zip')) { - return $this->downloadFromZip($path); - } - - // Define PHP major version - $ver = $this->php_major_ver = $this->getOption('with-php'); - define('SPC_BUILD_PHP_VERSION', $ver); - if ($ver !== 'git' && !preg_match('/^\d+\.\d+$/', $ver)) { - // If not git, we need to check the version format - if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) { - logger()->error("bad version arg: {$ver}, x.y or x.y.z required!"); - return static::FAILURE; - } - } - - // retry - $retry = (int) $this->getOption('retry'); - f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry); - - // Use shallow-clone can reduce git resource download - if ($this->getOption('shallow-clone')) { - define('GIT_SHALLOW_CLONE', true); - } - - // To read config - Config::getSource('openssl'); - - // use openssl 1.1 - if ($this->getOption('with-openssl11')) { - logger()->debug('Using openssl 1.1'); - Config::$source['openssl']['regex'] = '/href="(?openssl-(?1.[^"]+)\.tar\.gz)\"/'; - } - - $chosen_sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); - - $sss = $this->getOption('ignore-cache-sources'); - if ($sss === false) { - // false is no-any-ignores, that is, default. - $force_all = false; - $force_list = []; - } elseif ($sss === null) { - // null means all sources will be ignored, equals to --force-all (but we don't want to add too many options) - $force_all = true; - $force_list = []; - } else { - // ignore some sources - $force_all = false; - $force_list = array_map('trim', array_filter(explode(',', $this->getOption('ignore-cache-sources')))); - } - - if ($this->getOption('all')) { - logger()->notice('Downloading with --all option will take more times to download, we recommend you to download with --for-extensions option !'); - } - - // Process -U options - $custom_urls = []; - foreach ($this->input->getOption('custom-url') as $value) { - [$source_name, $url] = explode(':', $value, 2); - $custom_urls[$source_name] = $url; - } - // Process -G options - $custom_gits = []; - foreach ($this->input->getOption('custom-git') as $value) { - [$source_name, $branch, $url] = explode(':', $value, 3); - $custom_gits[$source_name] = [$branch, $url]; - } - - // If passing --prefer-pre-built option, we need to load pre-built library list from pre-built.json targeted releases - if ($this->getOption('prefer-pre-built')) { - $repo = Config::getPreBuilt('repo'); - $pre_built_libs = Downloader::getLatestGithubRelease($repo, [ - 'repo' => $repo, - 'prefer-stable' => Config::getPreBuilt('prefer-stable'), - ], false); - } else { - $pre_built_libs = []; - } - - // Download them - f_mkdir(DOWNLOAD_PATH); - $cnt = count($chosen_sources); - $ni = 0; - foreach ($chosen_sources as $source) { - ++$ni; - if (isset($custom_urls[$source])) { - $config = Config::getSource($source); - $new_config = [ - 'type' => 'url', - 'url' => $custom_urls[$source], - ]; - if (isset($config['path'])) { - $new_config['path'] = $config['path']; - } - if (isset($config['filename'])) { - $new_config['filename'] = $config['filename']; - } - logger()->info("[{$ni}/{$cnt}] Downloading source {$source} from custom url: {$new_config['url']}"); - Downloader::downloadSource($source, $new_config, true); - } elseif (isset($custom_gits[$source])) { - $config = Config::getSource($source); - $new_config = [ - 'type' => 'git', - 'rev' => $custom_gits[$source][0], - 'url' => $custom_gits[$source][1], - ]; - if (isset($config['path'])) { - $new_config['path'] = $config['path']; - } - logger()->info("[{$ni}/{$cnt}] Downloading source {$source} from custom git: {$new_config['url']}"); - Downloader::downloadSource($source, $new_config, true); - } else { - $config = Config::getSource($source); - // Prefer pre-built, we need to search pre-built library - if ($this->getOption('prefer-pre-built') && ($config['provide-pre-built'] ?? false) === true) { - // We need to replace pattern - $replace = [ - '{name}' => $source, - '{arch}' => arch2gnu(php_uname('m')), - '{os}' => strtolower(PHP_OS_FAMILY), - '{libc}' => SPCTarget::getLibc() ?? 'default', - '{libcver}' => SPCTarget::getLibcVersion() ?? 'default', - ]; - $find = str_replace(array_keys($replace), array_values($replace), Config::getPreBuilt('match-pattern')); - // find filename in asset list - if (($url = $this->findPreBuilt($pre_built_libs, $find)) !== null) { - logger()->info("[{$ni}/{$cnt}] Downloading pre-built content {$source}"); - Downloader::downloadSource($source, ['type' => 'url', 'url' => $url], $force_all || in_array($source, $force_list), SPC_DOWNLOAD_PRE_BUILT); - continue; - } - logger()->warning("Pre-built content not found for {$source}, fallback to source download"); - } - logger()->info("[{$ni}/{$cnt}] Downloading source {$source}"); - try { - Downloader::downloadSource($source, $config, $force_all || in_array($source, $force_list)); - } catch (SPCException $e) { - // if `--no-alt` option is set, we will not download alternative sources - if ($this->getOption('no-alt')) { - throw $e; - } - // if download failed, we will try to download alternative sources - logger()->warning("Download failed: {$e->getMessage()}"); - $alt_sources = Config::getSource($source)['alt'] ?? null; - if ($alt_sources === null) { - logger()->warning("No alternative sources found for {$source}, using default alternative source"); - $alt_config = array_merge($config, Downloader::getDefaultAlternativeSource($source)); - } elseif ($alt_sources === false) { - throw new DownloaderException("No alternative sources found for {$source}, skipping alternative download"); - } else { - logger()->notice("Trying to download alternative sources for {$source}"); - $alt_config = array_merge($config, $alt_sources); - } - Downloader::downloadSource($source, $alt_config, $force_all || in_array($source, $force_list)); - } - } - } - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Download complete, used ' . $time . ' s !'); - return static::SUCCESS; - } - - private function downloadFromZip(string $path): int - { - if (!file_exists($path)) { - logger()->critical('File ' . $path . ' not exist or not a zip archive.'); - return static::FAILURE; - } - // remove old download files first - if (is_dir(DOWNLOAD_PATH)) { - logger()->warning('You are doing some operations that not recoverable: removing directories below'); - logger()->warning(DOWNLOAD_PATH); - logger()->alert('I will remove these dir after 5 seconds !'); - sleep(5); - f_passthru((PHP_OS_FAMILY === 'Windows' ? 'rmdir /s /q ' : 'rm -rf ') . DOWNLOAD_PATH); - } - // unzip command check - if (PHP_OS_FAMILY !== 'Windows' && !self::findCommand('unzip')) { - $this->output->writeln('Missing unzip command, you need to install it first !'); - $this->output->writeln('You can use "bin/spc doctor" command to check and install required tools'); - return static::FAILURE; - } - // create downloads - if (PHP_OS_FAMILY === 'Windows') { - // Windows TODO - $this->output->writeln('Windows currently does not support --from-zip !'); - return static::FAILURE; - } - $abs_path = realpath($path); - f_passthru('mkdir ' . DOWNLOAD_PATH . ' && cd ' . DOWNLOAD_PATH . ' && unzip ' . escapeshellarg($abs_path)); - - if (!file_exists(LockFile::LOCK_FILE)) { - $this->output->writeln('.lock.json not exist in "downloads/", please run "bin/spc download" first !'); - return static::FAILURE; - } - $this->output->writeln('Extract success'); - return static::SUCCESS; - } - - /** - * Calculate the sources by extensions - * - * @param array $extensions extension list - * @param bool $include_suggests include suggested libs and extensions (default: true) - */ - private function calculateSourcesByExt(array $extensions, bool $include_suggests = true): array - { - [$extensions, $libraries] = $include_suggests ? DependencyUtil::getExtsAndLibs($extensions, [], true, true) : DependencyUtil::getExtsAndLibs($extensions); - $sources = []; - foreach ($extensions as $extension) { - if (Config::getExt($extension, 'type') === 'external') { - $sources[] = Config::getExt($extension, 'source'); - } - } - foreach ($libraries as $library) { - $source = Config::getLib($library, 'source'); - if ($source !== null) { - $sources[] = $source; - } - } - return array_values(array_unique($sources)); - } - - /** - * Calculate the sources by libraries - * - * @param array $libs library list - * @param bool $include_suggests include suggested libs (default: true) - */ - private function calculateSourcesByLib(array $libs, bool $include_suggests = true): array - { - $libs = DependencyUtil::getLibs($libs, $include_suggests); - $sources = []; - foreach ($libs as $library) { - $sources[] = Config::getLib($library, 'source'); - } - return array_values(array_unique($sources)); - } - - /** - * @param array $assets Asset list from GitHub API - * @param string $filename Match file name, e.g. pkg-config-aarch64-darwin.txz - * @return null|string Return the download URL if found, otherwise null - */ - private function findPreBuilt(array $assets, string $filename): ?string - { - logger()->debug("Finding pre-built asset {$filename}"); - foreach ($assets as $asset) { - if ($asset['name'] === $filename) { - return $asset['browser_download_url']; - } - } - return null; - } - - private function _clean(): int - { - logger()->warning('You are doing some operations that not recoverable: removing directories below'); - logger()->warning(SOURCE_PATH); - logger()->warning(DOWNLOAD_PATH); - logger()->warning(BUILD_ROOT_PATH); - logger()->alert('I will remove these dir after 5 seconds !'); - sleep(5); - if (PHP_OS_FAMILY === 'Windows') { - f_passthru('rmdir /s /q ' . SOURCE_PATH); - f_passthru('rmdir /s /q ' . DOWNLOAD_PATH); - f_passthru('rmdir /s /q ' . BUILD_ROOT_PATH); - } else { - f_passthru('rm -rf ' . SOURCE_PATH . '/*'); - f_passthru('rm -rf ' . DOWNLOAD_PATH . '/*'); - f_passthru('rm -rf ' . BUILD_ROOT_PATH . '/*'); - } - return static::FAILURE; - } -} diff --git a/src/SPC/command/DumpExtensionsCommand.php b/src/SPC/command/DumpExtensionsCommand.php deleted file mode 100644 index ffb80a46b..000000000 --- a/src/SPC/command/DumpExtensionsCommand.php +++ /dev/null @@ -1,160 +0,0 @@ -addArgument('path', InputArgument::OPTIONAL, 'Path to project root', '.'); - $this->addOption('format', 'F', InputOption::VALUE_REQUIRED, 'Parsed output format', 'default'); - // output zero extension replacement rather than exit as failure - $this->addOption('no-ext-output', 'N', InputOption::VALUE_REQUIRED, 'When no extensions found, output default combination (comma separated)'); - // no dev - $this->addOption('no-dev', null, null, 'Do not include dev dependencies'); - // no spc filter - $this->addOption('no-spc-filter', 'S', null, 'Do not use SPC filter to determine the required extensions'); - } - - public function handle(): int - { - $path = FileSystem::convertPath($this->getArgument('path')); - - $path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json'); - $path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock'); - - $ext_installed = $this->extractFromInstalledJson($path_installed, !$this->getOption('no-dev')); - if ($ext_installed === null) { - if ($this->getOption('format') === 'default') { - $this->output->writeln('vendor/composer/installed.json load failed, skipped'); - } - $ext_installed = []; - } - - $ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev')); - if ($ext_lock === null) { - $this->output->writeln('composer.lock load failed'); - return static::FAILURE; - } - - $extensions = array_unique(array_merge($ext_installed, $ext_lock)); - sort($extensions); - - if (empty($extensions)) { - if ($this->getOption('no-ext-output')) { - $this->outputExtensions(explode(',', $this->getOption('no-ext-output'))); - return static::SUCCESS; - } - $this->output->writeln('No extensions found'); - return static::FAILURE; - } - - $this->outputExtensions($extensions); - return static::SUCCESS; - } - - private function filterExtensions(array $requirements): array - { - return array_map( - fn ($key) => substr($key, 4), - array_keys( - array_filter($requirements, function ($key) { - return str_starts_with($key, 'ext-'); - }, ARRAY_FILTER_USE_KEY) - ) - ); - } - - private function loadJson(string $file): array|bool - { - if (!file_exists($file)) { - return false; - } - - $data = json_decode(file_get_contents($file), true); - if (!$data) { - return false; - } - return $data; - } - - private function extractFromInstalledJson(string $file, bool $include_dev = true): ?array - { - if (!($data = $this->loadJson($file))) { - return null; - } - - $packages = $data['packages'] ?? []; - - if (!$include_dev) { - $packages = array_filter($packages, fn ($package) => !in_array($package['name'], $data['dev-package-names'] ?? [])); - } - - return array_merge( - ...array_map(fn ($x) => isset($x['require']) ? $this->filterExtensions($x['require']) : [], $packages) - ); - } - - private function extractFromComposerLock(string $file, bool $include_dev = true): ?array - { - if (!($data = $this->loadJson($file))) { - return null; - } - - // get packages ext - $packages = $data['packages'] ?? []; - $exts = array_merge( - ...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages) - ); - - // get dev packages ext - if ($include_dev) { - $packages = $data['packages-dev'] ?? []; - $exts = array_merge( - $exts, - ...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages) - ); - } - - // get require ext - $platform = $data['platform'] ?? []; - $exts = array_merge($exts, $this->filterExtensions($platform)); - - // get require-dev ext - if ($include_dev) { - $platform = $data['platform-dev'] ?? []; - $exts = array_merge($exts, $this->filterExtensions($platform)); - } - - return $exts; - } - - private function outputExtensions(array $extensions): void - { - if (!$this->getOption('no-spc-filter')) { - $extensions = $this->parseExtensionList($extensions); - } - switch ($this->getOption('format')) { - case 'json': - $this->output->writeln(json_encode($extensions, JSON_PRETTY_PRINT)); - break; - case 'text': - $this->output->writeln(implode(',', $extensions)); - break; - default: - $this->output->writeln('Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':'); - $this->output->writeln(implode(',', $extensions)); - } - } -} diff --git a/src/SPC/command/DumpLicenseCommand.php b/src/SPC/command/DumpLicenseCommand.php deleted file mode 100644 index e441fd9e1..000000000 --- a/src/SPC/command/DumpLicenseCommand.php +++ /dev/null @@ -1,68 +0,0 @@ -addOption('for-extensions', null, InputOption::VALUE_REQUIRED, 'Dump by extensions and related libraries', null); - $this->addOption('without-php', null, InputOption::VALUE_NONE, 'Dump without php-src'); - $this->addOption('for-libs', null, InputOption::VALUE_REQUIRED, 'Dump by libraries', null); - $this->addOption('for-sources', null, InputOption::VALUE_REQUIRED, 'Dump by original sources (source.json)', null); - $this->addOption('dump-dir', null, InputOption::VALUE_REQUIRED, 'Change dump directory', BUILD_ROOT_PATH . '/license'); - } - - public function handle(): int - { - $dumper = new LicenseDumper(); - if ($this->getOption('for-extensions') !== null) { - // 从参数中获取要编译的 extensions,并转换为数组 - $extensions = $this->parseExtensionList($this->getOption('for-extensions')); - // 根据提供的扩展列表获取依赖库列表并编译 - [$extensions, $libraries] = DependencyUtil::getExtsAndLibs($extensions); - $dumper->addExts($extensions); - $dumper->addLibs($libraries); - if (!$this->getOption('without-php')) { - $dumper->addSources(['php-src']); - } - $dumper->dump($this->getOption('dump-dir')); - $this->output->writeln('Dump license with extensions: ' . implode(', ', $extensions)); - $this->output->writeln('Dump license with libraries: ' . implode(', ', $libraries)); - $this->output->writeln('Dump license with' . ($this->getOption('without-php') ? 'out' : '') . ' php-src'); - $this->output->writeln('Dump target dir: ' . $this->getOption('dump-dir')); - return static::SUCCESS; - } - if ($this->getOption('for-libs') !== null) { - $libraries = array_map('trim', array_filter(explode(',', $this->getOption('for-libs')))); - $libraries = DependencyUtil::getLibs($libraries); - $dumper->addLibs($libraries); - $dumper->dump($this->getOption('dump-dir')); - return $this->logWithResult( - $dumper->dump($this->getOption('dump-dir')), - 'Dump target dir: ' . $this->getOption('dump-dir'), - 'Dump failed!' - ); - } - if ($this->getOption('for-sources') !== null) { - $sources = array_map('trim', array_filter(explode(',', $this->getOption('for-sources')))); - $dumper->addSources($sources); - $dumper->dump($this->getOption('dump-dir')); - $this->output->writeln('Dump target dir: ' . $this->getOption('dump-dir')); - return static::SUCCESS; - } - $this->output->writeln('You must use one of "--for-extensions=", "--for-libs=", "--for-sources=" to dump'); - return static::FAILURE; - } -} diff --git a/src/SPC/command/ExtractCommand.php b/src/SPC/command/ExtractCommand.php deleted file mode 100644 index c189e0cee..000000000 --- a/src/SPC/command/ExtractCommand.php +++ /dev/null @@ -1,34 +0,0 @@ -addArgument('sources', InputArgument::REQUIRED, 'The sources will be compiled, comma separated'); - $this->addOption('source-only', null, null, 'Only check the source exist, do not check the lib and ext'); - } - - public function handle(): int - { - $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); - if (empty($sources)) { - $this->output->writeln('sources cannot be empty, at least contain one !'); - return static::FAILURE; - } - SourceManager::initSource(sources: $sources, source_only: $this->getOption('source-only')); - logger()->info('Extract done !'); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/InstallPkgCommand.php b/src/SPC/command/InstallPkgCommand.php deleted file mode 100644 index 13a078887..000000000 --- a/src/SPC/command/InstallPkgCommand.php +++ /dev/null @@ -1,84 +0,0 @@ -addArgument('packages', InputArgument::REQUIRED, 'The packages will be installed, comma separated'); - $this->addOption('shallow-clone', null, null, 'Clone shallow'); - $this->addOption('custom-url', 'U', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://downloads.php.net/~eric/php-8.3.0beta1.tar.gz"'); - $this->addOption('no-alt', null, null, 'Do not download alternative packages'); - $this->addOption('skip-extract', null, null, 'Skip package extraction, just download the package archive'); - } - - public function handle(): int - { - // Use shallow-clone can reduce git resource download - if ($this->getOption('shallow-clone')) { - define('GIT_SHALLOW_CLONE', true); - } - - // Process -U options - $custom_urls = []; - foreach ($this->input->getOption('custom-url') as $value) { - [$pkg_name, $url] = explode(':', $value, 2); - $custom_urls[$pkg_name] = $url; - } - - $chosen_pkgs = array_map('trim', array_filter(explode(',', $this->getArgument('packages')))); - - // Download them - f_mkdir(DOWNLOAD_PATH); - $ni = 0; - $cnt = count($chosen_pkgs); - - foreach ($chosen_pkgs as $pkg) { - ++$ni; - if (isset($custom_urls[$pkg])) { - $config = Config::getPkg($pkg); - $new_config = [ - 'type' => 'url', - 'url' => $custom_urls[$pkg], - ]; - if (isset($config['extract'])) { - $new_config['extract'] = $config['extract']; - } - if (isset($config['filename'])) { - $new_config['filename'] = $config['filename']; - } - logger()->info("Installing source {$pkg} from custom url [{$ni}/{$cnt}]"); - PackageManager::installPackage( - $pkg, - $new_config, - allow_alt: false, - extract: !$this->getOption('skip-extract') - ); - } else { - logger()->info("Fetching package {$pkg} [{$ni}/{$cnt}]"); - PackageManager::installPackage( - $pkg, - Config::getPkg($pkg), - allow_alt: !$this->getOption('no-alt'), - extract: !$this->getOption('skip-extract') - ); - } - } - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Install packages complete, used ' . $time . ' s !'); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/MicroCombineCommand.php b/src/SPC/command/MicroCombineCommand.php deleted file mode 100644 index 3987ca785..000000000 --- a/src/SPC/command/MicroCombineCommand.php +++ /dev/null @@ -1,120 +0,0 @@ -addArgument('file', InputArgument::REQUIRED, 'The php or phar file to be combined'); - $this->addOption('with-micro', 'M', InputOption::VALUE_REQUIRED, 'Customize your micro.sfx file'); - $this->addOption('with-ini-set', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'ini to inject into micro.sfx when combining'); - $this->addOption('with-ini-file', 'N', InputOption::VALUE_REQUIRED, 'ini file to inject into micro.sfx when combining'); - $this->addOption('output', 'O', InputOption::VALUE_REQUIRED, 'Customize your output binary file name'); - } - - public function handle(): int - { - // 0. Initialize path variables - $internal = FileSystem::convertPath(BUILD_ROOT_PATH . '/bin/micro.sfx'); - $micro_file = $this->input->getOption('with-micro'); - $file = $this->getArgument('file'); - $ini_set = $this->input->getOption('with-ini-set'); - $ini_file = $this->input->getOption('with-ini-file'); - $target_ini = []; - $output = $this->input->getOption('output') ?? 'my-app'; - $ini_part = ''; - // 1. Make sure specified micro.sfx file exists - if ($micro_file !== null && !file_exists($micro_file)) { - $this->output->writeln('The micro.sfx file you specified is incorrect or does not exist!'); - return static::FAILURE; - } - // 2. Make sure buildroot/bin/micro.sfx exists - if ($micro_file === null && !file_exists($internal)) { - $this->output->writeln('You haven\'t compiled micro.sfx yet, please use "build" command and "--build-micro" to compile phpmicro first!'); - return static::FAILURE; - } - // 3. Use buildroot/bin/micro.sfx - if ($micro_file === null) { - $micro_file = $internal; - } - // 4. Make sure php or phar file exists - if (!is_file(FileSystem::convertPath($file))) { - $this->output->writeln('The file to combine does not exist!'); - return static::FAILURE; - } - // 5. Confirm ini files (ini-set has higher priority) - if ($ini_file !== null) { - // Check file exist first - if (!file_exists($ini_file)) { - $this->output->writeln('The ini file to combine does not exist! (' . $ini_file . ')'); - return static::FAILURE; - } - $arr = parse_ini_file($ini_file); - if ($arr === false) { - $this->output->writeln('Cannot parse ini file'); - return static::FAILURE; - } - $target_ini = array_merge($target_ini, $arr); - } - // 6. Confirm ini sets - if ($ini_set !== []) { - foreach ($ini_set as $item) { - $arr = parse_ini_string($item); - if ($arr === false) { - $this->output->writeln('--with-ini-set parse failed'); - return static::FAILURE; - } - $target_ini = array_merge($target_ini, $arr); - } - } - // 7. Generate ini injection parts - if (!empty($target_ini)) { - $ini_str = $this->encodeINI($target_ini); - logger()->debug('Injecting ini parts: ' . PHP_EOL . $ini_str); - $ini_part = "\xfd\xf6\x69\xe6"; - $ini_part .= pack('N', strlen($ini_str)); - $ini_part .= $ini_str; - } - // 8. Combine ! - $output = FileSystem::isRelativePath($output) ? (WORKING_DIR . '/' . $output) : $output; - $file_target = file_get_contents($micro_file) . $ini_part . file_get_contents($file); - if (PHP_OS_FAMILY === 'Windows' && !str_ends_with(strtolower($output), '.exe')) { - $output .= '.exe'; - } - $output = FileSystem::convertPath($output); - $result = file_put_contents($output, $file_target); - if ($result === false) { - $this->output->writeln('Combine failed.'); - return static::FAILURE; - } - // 9. chmod +x - chmod($output, 0755); - $this->output->writeln('Combine success! Binary file: ' . $output . ''); - return static::SUCCESS; - } - - private function encodeINI(array $array): string - { - $res = []; - foreach ($array as $key => $val) { - if (is_array($val)) { - $res[] = "[{$key}]"; - foreach ($val as $skey => $sval) { - $res[] = "{$skey}=" . (is_numeric($sval) ? $sval : '"' . $sval . '"'); - } - } else { - $res[] = "{$key}=" . (is_numeric($val) ? $val : '"' . $val . '"'); - } - } - return implode("\n", $res); - } -} diff --git a/src/SPC/command/SPCConfigCommand.php b/src/SPC/command/SPCConfigCommand.php deleted file mode 100644 index 5e21c9a19..000000000 --- a/src/SPC/command/SPCConfigCommand.php +++ /dev/null @@ -1,55 +0,0 @@ -addArgument('extensions', InputArgument::OPTIONAL, 'The extensions will be compiled, comma separated'); - $this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', ''); - $this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs'); - $this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts'); - $this->addOption('includes', null, null, 'Add additional include path'); - $this->addOption('libs', null, null, 'Add additional libs path'); - $this->addOption('libs-only-deps', null, null, 'Output dependent libraries with -l prefix'); - $this->addOption('absolute-libs', null, null, 'Output absolute paths for libraries'); - $this->addOption('no-php', null, null, 'Do not link to PHP library'); - } - - public function handle(): int - { - // transform string to array - $libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs')))); - // transform string to array - $extensions = $this->getArgument('extensions') ? $this->parseExtensionList($this->getArgument('extensions')) : []; - $include_suggest_ext = $this->getOption('with-suggested-exts'); - $include_suggest_lib = $this->getOption('with-suggested-libs'); - - $util = new SPCConfigUtil(options: [ - 'no_php' => $this->getOption('no-php'), - 'libs_only_deps' => $this->getOption('libs-only-deps'), - 'absolute_libs' => $this->getOption('absolute-libs'), - ]); - $config = $util->config($extensions, $libraries, $include_suggest_ext, $include_suggest_lib); - - $this->output->writeln(match (true) { - $this->getOption('includes') => $config['cflags'], - $this->getOption('libs-only-deps') => $config['libs'], - $this->getOption('libs') => "{$config['ldflags']} {$config['libs']}", - default => "{$config['cflags']} {$config['ldflags']} {$config['libs']}", - }); - - return 0; - } -} diff --git a/src/SPC/command/SwitchPhpVersionCommand.php b/src/SPC/command/SwitchPhpVersionCommand.php deleted file mode 100644 index 04a31a753..000000000 --- a/src/SPC/command/SwitchPhpVersionCommand.php +++ /dev/null @@ -1,65 +0,0 @@ -addArgument( - 'php-major-version', - InputArgument::REQUIRED, - 'PHP major version (supported: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5)', - null, - fn () => ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] - ); - $this->no_motd = true; - - $this->addOption('retry', 'R', InputOption::VALUE_REQUIRED, 'Set retry time when downloading failed (default: 0)', '0'); - } - - public function handle(): int - { - $php_ver = $this->input->getArgument('php-major-version'); - if (!in_array($php_ver, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'])) { - // match x.y.z - preg_match('/^\d+\.\d+\.\d+$/', $php_ver, $matches); - if (!$matches) { - $this->output->writeln('Invalid PHP version ' . $php_ver . ' !'); - return static::FAILURE; - } - } - - if (LockFile::isLockFileExists('php-src')) { - $this->output->writeln('Removing old PHP source...'); - LockFile::put('php-src', null); - } - - // Download new PHP source - $this->output->writeln('Downloading PHP source...'); - define('SPC_BUILD_PHP_VERSION', $php_ver); - - // retry - $retry = intval($this->getOption('retry')); - f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry); - - Downloader::downloadSource('php-src', Config::getSource('php-src')); - - // Remove source/php-src dir - FileSystem::removeDir(SOURCE_PATH . '/php-src'); - - $this->output->writeln('Switched to PHP ' . $php_ver . ' successfully!'); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/dev/AllExtCommand.php b/src/SPC/command/dev/AllExtCommand.php deleted file mode 100644 index 04085778c..000000000 --- a/src/SPC/command/dev/AllExtCommand.php +++ /dev/null @@ -1,88 +0,0 @@ -addArgument('extensions', InputArgument::OPTIONAL, 'List of extensions that will be displayed, comma separated'); - $this->addOption( - 'columns', - null, - InputOption::VALUE_REQUIRED, - 'List of columns that will be displayed, comma separated (lib-depends, lib-suggests, ext-depends, ext-suggests, unix-only)', - 'lib-depends,lib-suggests,ext-depends,ext-suggests,unix-only' - ); - } - - public function handle(): int - { - $extensions = array_map('trim', array_filter(explode(',', $this->getArgument('extensions') ?? ''))); - $columns = array_map('trim', array_filter(explode(',', $this->getOption('columns')))); - - foreach ($columns as $column) { - if (!in_array($column, ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests', 'unix-only', 'type'])) { - $this->output->writeln('Column name [' . $column . '] is not valid.'); - $this->output->writeln('Available column name: lib-depends, lib-suggests, ext-depends, ext-suggests, unix-only, type'); - return static::FAILURE; - } - } - array_unshift($columns, 'name'); - - $style = new SymfonyStyle($this->input, $this->output); - $style->writeln($extensions ? 'Available extensions:' : 'Extensions:'); - - $data = []; - foreach (Config::getExts() as $extension => $details) { - if ($extensions !== [] && !\in_array($extension, $extensions, true)) { - continue; - } - - try { - [, $libraries, $not_included] = DependencyUtil::getExtsAndLibs([$extension]); - } catch (WrongUsageException) { - $libraries = $not_included = []; - } - - $lib_suggests = Config::getExt($extension, 'lib-suggests', []); - $ext_suggests = Config::getExt($extension, 'ext-suggests', []); - - $row = []; - foreach ($columns as $column) { - $row[] = match ($column) { - 'name' => $extension, - 'type' => Config::getExt($extension, 'type'), - 'lib-depends' => implode(', ', $libraries), - 'lib-suggests' => implode(', ', $lib_suggests), - 'ext-depends' => implode(',', $not_included), - 'ext-suggests' => implode(', ', $ext_suggests), - 'unix-only' => Config::getExt($extension, 'unix-only', false) ? 'true' : 'false', - default => '', - }; - } - $data[] = $row; - } - - if ($data === []) { - $style->warning('Unknown extension selected: ' . implode(',', $extensions)); - } else { - $func = PHP_OS_FAMILY === 'Windows' ? [$style, 'table'] : '\Laravel\Prompts\table'; - call_user_func($func, $columns, $data); - } - - return static::SUCCESS; - } -} diff --git a/src/SPC/command/dev/EnvCommand.php b/src/SPC/command/dev/EnvCommand.php deleted file mode 100644 index d5a0cec32..000000000 --- a/src/SPC/command/dev/EnvCommand.php +++ /dev/null @@ -1,37 +0,0 @@ -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/SPC/command/dev/ExtVerCommand.php b/src/SPC/command/dev/ExtVerCommand.php deleted file mode 100644 index 3154a6bb9..000000000 --- a/src/SPC/command/dev/ExtVerCommand.php +++ /dev/null @@ -1,49 +0,0 @@ -addArgument('extension', InputArgument::REQUIRED, 'The library name'); - } - - public function initialize(InputInterface $input, OutputInterface $output): void - { - $this->no_motd = true; - parent::initialize($input, $output); - } - - public function handle(): int - { - // Get lib object - $builder = BuilderProvider::makeBuilderByInput($this->input); - - $builder->proveExts([$this->getArgument('extension')], [], true); - - // Check whether lib is extracted - // if (!is_dir(SOURCE_PATH . '/' . $this->getArgument('library'))) { - // $this->output->writeln("Library {$this->getArgument('library')} is not extracted"); - // return static::FAILURE; - // } - - $version = $builder->getExt($this->getArgument('extension'))->getExtVersion(); - if ($version === null) { - $this->output->writeln("Failed to get version of extension {$this->getArgument('extension')}"); - return static::FAILURE; - } - $this->output->writeln("{$version}"); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/dev/GenerateExtDepDocsCommand.php b/src/SPC/command/dev/GenerateExtDepDocsCommand.php deleted file mode 100644 index afa6c9ffe..000000000 --- a/src/SPC/command/dev/GenerateExtDepDocsCommand.php +++ /dev/null @@ -1,166 +0,0 @@ - $ext) { - $line_linux = [ - "{$ext_name}", - implode('
', $ext['ext-depends-linux'] ?? $ext['ext-depends-unix'] ?? $ext['ext-depends'] ?? []), - implode('
', $ext['ext-suggests-linux'] ?? $ext['ext-suggests-unix'] ?? $ext['ext-suggests'] ?? []), - implode('
', $ext['lib-depends-linux'] ?? $ext['lib-depends-unix'] ?? $ext['lib-depends'] ?? []), - implode('
', $ext['lib-suggests-linux'] ?? $ext['lib-suggests-unix'] ?? $ext['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_linux, $line_linux); - if ($this->isSupported($ext, 'Linux') && !$this->isEmptyLine($line_linux)) { - $md_lines_linux[] = $line_linux; - } - $line_macos = [ - "{$ext_name}", - implode('
', $ext['ext-depends-macos'] ?? $ext['ext-depends-unix'] ?? $ext['ext-depends'] ?? []), - implode('
', $ext['ext-suggests-macos'] ?? $ext['ext-suggests-unix'] ?? $ext['ext-suggests'] ?? []), - implode('
', $ext['lib-depends-macos'] ?? $ext['lib-depends-unix'] ?? $ext['lib-depends'] ?? []), - implode('
', $ext['lib-suggests-macos'] ?? $ext['lib-suggests-unix'] ?? $ext['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_macos, $line_macos); - if ($this->isSupported($ext, 'macOS') && !$this->isEmptyLine($line_macos)) { - $md_lines_macos[] = $line_macos; - } - $line_windows = [ - "{$ext_name}", - implode('
', $ext['ext-depends-windows'] ?? $ext['ext-depends-win'] ?? $ext['ext-depends'] ?? []), - implode('
', $ext['ext-suggests-windows'] ?? $ext['ext-suggests-win'] ?? $ext['ext-suggests'] ?? []), - implode('
', $ext['lib-depends-windows'] ?? $ext['lib-depends-win'] ?? $ext['lib-depends'] ?? []), - implode('
', $ext['lib-suggests-windows'] ?? $ext['lib-suggests-win'] ?? $ext['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_windows, $line_windows); - if ($this->isSupported($ext, 'Windows') && !$this->isEmptyLine($line_windows)) { - $md_lines_windows[] = $line_windows; - } - $line_freebsd = [ - "{$ext_name}", - implode('
', $ext['ext-depends-freebsd'] ?? $ext['ext-depends-bsd'] ?? $ext['ext-depends-unix'] ?? $ext['ext-depends'] ?? []), - implode('
', $ext['ext-suggests-freebsd'] ?? $ext['ext-suggests-bsd'] ?? $ext['ext-suggests-unix'] ?? $ext['ext-suggests'] ?? []), - implode('
', $ext['lib-depends-freebsd'] ?? $ext['lib-depends-bsd'] ?? $ext['lib-depends-unix'] ?? $ext['lib-depends'] ?? []), - implode('
', $ext['lib-suggests-freebsd'] ?? $ext['lib-suggests-bsd'] ?? $ext['lib-suggests-unix'] ?? $ext['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_freebsd, $line_freebsd); - if ($this->isSupported($ext, 'BSD') && !$this->isEmptyLine($line_freebsd)) { - $md_lines_freebsd[] = $line_freebsd; - } - } - - // Generate markdown - if (!empty($md_lines_linux)) { - $content .= "### Linux\n\n"; - $content .= '| '; - $pads = ['Extension Name', 'Required Extensions', 'Suggested Extensions', 'Required Libraries', 'Suggested Libraries']; - // 生成首行 - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_linux[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - // 生成第二行表格分割符 | --- | --- | --- | --- | --- | - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_linux[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_linux as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_linux[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - if (!empty($md_lines_macos)) { - $content .= "\n\n### macOS\n\n"; - $content .= '| '; - $pads = ['Extension Name', 'Required Extensions', 'Suggested Extensions', 'Required Libraries', 'Suggested Libraries']; - // 生成首行 - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_macos[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - // 生成第二行表格分割符 | --- | --- | --- | --- | --- | - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_macos[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_macos as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_macos[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - if (!empty($md_lines_windows)) { - $content .= "\n\n### Windows\n\n"; - $content .= '| '; - $pads = ['Extension Name', 'Required Extensions', 'Suggested Extensions', 'Required Libraries', 'Suggested Libraries']; - // 生成首行 - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_windows[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - // 生成第二行表格分割符 | --- | --- | --- | --- | --- | - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_windows[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_windows as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_windows[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - if (!empty($md_lines_freebsd)) { - $content .= "\n\n### FreeBSD\n\n"; - $content .= '| '; - $pads = ['Extension Name', 'Required Extensions', 'Suggested Extensions', 'Required Libraries', 'Suggested Libraries']; - // 生成首行 - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_freebsd[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - // 生成第二行表格分割符 | --- | --- | --- | --- | --- | - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_freebsd[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_freebsd as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_freebsd[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - - $this->output->writeln($content); - return static::SUCCESS; - } - - private function applyMaxLen(array &$max, array $lines): void - { - foreach ($max as $k => $v) { - $max[$k] = max($v, strlen($lines[$k])); - } - } - - private function isSupported(array $ext, string $os): bool - { - return !in_array($ext['support'][$os] ?? 'yes', ['no', 'wip']); - } - - private function isEmptyLine(array $line): bool - { - return $line[1] === '' && $line[2] === '' && $line[3] === '' && $line[4] === ''; - } -} diff --git a/src/SPC/command/dev/GenerateExtDocCommand.php b/src/SPC/command/dev/GenerateExtDocCommand.php deleted file mode 100644 index c53e529d5..000000000 --- a/src/SPC/command/dev/GenerateExtDocCommand.php +++ /dev/null @@ -1,80 +0,0 @@ - $ext) { - // notes is optional - $name = ($ext['notes'] ?? false) === true ? "[{$ext_name}](./extension-notes#{$ext_name})" : $ext_name; - // calculate max length - $max_name = max($max_name, strlen($name)); - - // linux - $linux = match ($ext['support']['Linux'] ?? 'yes') { - 'wip' => '', - default => $ext['support']['Linux'] ?? 'yes', - }; - $max_linux = max($max_linux, strlen($linux)); - - // macos - $macos = match ($ext['support']['Darwin'] ?? 'yes') { - 'wip' => '', - default => $ext['support']['Darwin'] ?? 'yes', - }; - $max_macos = max($max_macos, strlen($macos)); - - // freebsd - $freebsd = match ($ext['support']['BSD'] ?? 'yes') { - 'wip' => '', - default => $ext['support']['BSD'] ?? 'yes', - }; - $max_freebsd = max($max_freebsd, strlen($freebsd)); - - // windows - $windows = match ($ext['support']['Windows'] ?? 'yes') { - 'wip' => '', - default => $ext['support']['Windows'] ?? 'yes', - }; - $max_windows = max($max_windows, strlen($windows)); - $md_lines[] = [ - $name, - $linux, - $macos, - $freebsd, - $windows, - ]; - } - - // generate markdown - $md = '| ' . str_pad('Extension Name', $max_name) . ' | ' . str_pad('Linux', $max_linux) . ' | ' . str_pad('macOS', $max_macos) . ' | ' . str_pad('FreeBSD', $max_freebsd) . ' | ' . str_pad('Windows', $max_windows) . ' |' . PHP_EOL; - $md .= '| ' . str_repeat('-', $max_name) . ' | ' . str_repeat('-', $max_linux) . ' | ' . str_repeat('-', $max_macos) . ' | ' . str_repeat('-', $max_freebsd) . ' | ' . str_repeat('-', $max_windows) . ' |' . PHP_EOL; - foreach ($md_lines as $line) { - $md .= '| ' . str_pad($line[0], $max_name) . ' | ' . str_pad($line[1], $max_linux) . ' | ' . str_pad($line[2], $max_macos) . ' | ' . str_pad($line[3], $max_freebsd) . ' | ' . str_pad($line[4], $max_windows) . ' |' . PHP_EOL; - } - $this->output->writeln($md); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/dev/GenerateLibDepDocsCommand.php b/src/SPC/command/dev/GenerateLibDepDocsCommand.php deleted file mode 100644 index feee99af1..000000000 --- a/src/SPC/command/dev/GenerateLibDepDocsCommand.php +++ /dev/null @@ -1,172 +0,0 @@ -support_lib_list[$os] = []; - $classes = FileSystem::getClassesPsr4( - FileSystem::convertPath(ROOT_DIR . '/src/SPC/builder/' . $os . '/library'), - 'SPC\builder\\' . $os . '\library' - ); - foreach ($classes as $class) { - if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) { - $this->support_lib_list[$os][$class::NAME] = $class; - } - } - } - - // Get lib.json - $libs = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true); - ConfigValidator::validateLibs($libs); - - // Markdown table needs format, we need to calculate the max length of each column - $content = ''; - - // Calculate table column max length - $max_linux = [0, 20, 19]; - $max_macos = [0, 20, 19]; - $max_windows = [0, 20, 19]; - $max_freebsd = [0, 20, 19]; - - $md_lines_linux = []; - $md_lines_macos = []; - $md_lines_windows = []; - $md_lines_freebsd = []; - - foreach ($libs as $lib_name => $lib) { - $line_linux = [ - "{$lib_name}", - implode('
', $lib['lib-depends-linux'] ?? $lib['lib-depends-unix'] ?? $lib['lib-depends'] ?? []), - implode('
', $lib['lib-suggests-linux'] ?? $lib['lib-suggests-unix'] ?? $lib['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_linux, $line_linux); - if ($this->isSupported($lib_name, 'linux') && !$this->isEmptyLine($line_linux)) { - $md_lines_linux[] = $line_linux; - } - $line_macos = [ - "{$lib_name}", - implode('
', $lib['lib-depends-macos'] ?? $lib['lib-depends-unix'] ?? $lib['lib-depends'] ?? []), - implode('
', $lib['lib-suggests-macos'] ?? $lib['lib-suggests-unix'] ?? $lib['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_macos, $line_macos); - if ($this->isSupported($lib_name, 'macos') && !$this->isEmptyLine($line_macos)) { - $md_lines_macos[] = $line_macos; - } - $line_windows = [ - "{$lib_name}", - implode('
', $lib['lib-depends-windows'] ?? $lib['lib-depends-win'] ?? $lib['lib-depends'] ?? []), - implode('
', $lib['lib-suggests-windows'] ?? $lib['lib-suggests-win'] ?? $lib['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_windows, $line_windows); - if ($this->isSupported($lib_name, 'windows') && !$this->isEmptyLine($line_windows)) { - $md_lines_windows[] = $line_windows; - } - $line_freebsd = [ - "{$lib_name}", - implode('
', $lib['lib-depends-freebsd'] ?? $lib['lib-depends-bsd'] ?? $lib['lib-depends-unix'] ?? $lib['lib-depends'] ?? []), - implode('
', $lib['lib-suggests-freebsd'] ?? $lib['lib-suggests-bsd'] ?? $lib['lib-suggests-unix'] ?? $lib['lib-suggests'] ?? []), - ]; - $this->applyMaxLen($max_freebsd, $line_freebsd); - if ($this->isSupported($lib_name, 'freebsd') && !$this->isEmptyLine($line_freebsd)) { - $md_lines_freebsd[] = $line_freebsd; - } - } - - // Generate markdown - if (!empty($md_lines_linux)) { - $content .= "### Linux\n\n"; - $content .= '| '; - $pads = ['Library Name', 'Required Libraries', 'Suggested Libraries']; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_linux[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_linux[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_linux as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_linux[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - - if (!empty($md_lines_macos)) { - $content .= "### macOS\n\n"; - $content .= '| '; - $pads = ['Library Name', 'Required Libraries', 'Suggested Libraries']; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_macos[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_macos[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_macos as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_macos[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - - if (!empty($md_lines_windows)) { - $content .= "### Windows\n\n"; - $content .= '| '; - $pads = ['Library Name', 'Required Libraries', 'Suggested Libraries']; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_windows[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_windows[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_windows as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_windows[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - - if (!empty($md_lines_freebsd)) { - $content .= "### FreeBSD\n\n"; - $content .= '| '; - $pads = ['Library Name', 'Required Libraries', 'Suggested Libraries']; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad($pad, $max_freebsd[$i]), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - $content .= '| '; - $content .= implode(' | ', array_map(fn ($i, $pad) => str_pad('', $max_freebsd[$i], '-'), array_keys($pads), $pads)); - $content .= ' |' . PHP_EOL; - foreach ($md_lines_freebsd as $line) { - $content .= '| ' . implode(' | ', array_map(fn ($i, $pad) => str_pad($line[$i], $max_freebsd[$i]), array_keys($line), $line)) . ' |' . PHP_EOL; - } - } - - $this->output->writeln($content); - return static::SUCCESS; - } - - private function applyMaxLen(array &$max, array $lines): void - { - foreach ($max as $k => $v) { - $max[$k] = max($v, strlen($lines[$k])); - } - } - - private function isSupported(string $ext_name, string $os): bool - { - if (!in_array($os, ['linux', 'macos', 'freebsd', 'windows'])) { - throw new \InvalidArgumentException('Invalid os: ' . $os); - } - return isset($this->support_lib_list[$os][$ext_name]); - } - - private function isEmptyLine(array $line): bool - { - return $line[1] === '' && $line[2] === ''; - } -} diff --git a/src/SPC/command/dev/LibVerCommand.php b/src/SPC/command/dev/LibVerCommand.php deleted file mode 100644 index 246618e46..000000000 --- a/src/SPC/command/dev/LibVerCommand.php +++ /dev/null @@ -1,64 +0,0 @@ -addArgument('library', InputArgument::REQUIRED, 'The library name'); - } - - public function initialize(InputInterface $input, OutputInterface $output): void - { - $this->no_motd = true; - parent::initialize($input, $output); - } - - public function handle(): int - { - // Get lib object - $builder = BuilderProvider::makeBuilderByInput($this->input); - $builder->setLibsOnly(); - - // check lib name exist in lib.json - try { - Config::getLib($this->getArgument('library')); - } catch (WrongUsageException $e) { - $this->output->writeln("Library {$this->getArgument('library')} is not supported yet"); - return static::FAILURE; - } - - // parse the dependencies - [, $libs] = DependencyUtil::getExtsAndLibs([], [$this->getArgument('library')]); - - $builder->proveLibs($libs); - - // Check whether lib is extracted - if (!is_dir(SOURCE_PATH . '/' . $this->getArgument('library'))) { - $this->output->writeln("Library {$this->getArgument('library')} is not extracted"); - return static::FAILURE; - } - - $version = $builder->getLib($this->getArgument('library'))->getLibVersion(); - if ($version === null) { - $this->output->writeln("Failed to get version of library {$this->getArgument('library')}. The version getter for [{$this->getArgument('library')}] is not implemented."); - return static::FAILURE; - } - $this->output->writeln("{$version}"); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/dev/PackLibCommand.php b/src/SPC/command/dev/PackLibCommand.php deleted file mode 100644 index 9b02fd3f1..000000000 --- a/src/SPC/command/dev/PackLibCommand.php +++ /dev/null @@ -1,175 +0,0 @@ -addArgument('library', InputArgument::REQUIRED, 'The library will be compiled'); - $this->addOption('show-libc-ver', null, null); - } - - public function handle(): int - { - $lib_name = $this->getArgument('library'); - $builder = BuilderProvider::makeBuilderByInput($this->input); - $builder->setLibsOnly(); - $libraries = DependencyUtil::getLibs([$lib_name]); - logger()->info('Building libraries: ' . implode(',', $libraries)); - sleep(2); - - FileSystem::createDir(WORKING_DIR . '/dist'); - - $builder->proveLibs($libraries); - $builder->validateLibsAndExts(); - - // before pack, check if the dependency tree contains lib-suggests - foreach ($libraries as $lib) { - if (Config::getLib($lib, 'lib-suggests', []) !== []) { - logger()->critical("The library {$lib} has lib-suggests, packing [{$lib_name}] is not safe, abort !"); - return static::FAILURE; - } - } - - $origin_files = []; - // get pack placehoder defines - $placehoder = get_pack_replace(); - - foreach ($builder->getLibs() as $lib) { - if ($lib->getName() !== $lib_name) { - // other dependencies: install or build, both ok - $lib->setup(); - } else { - // Get lock info - $source = Config::getLib($lib->getName(), 'source'); - if (($lock = LockFile::get($source)) === null || ($lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT)) { - logger()->critical("The library {$lib->getName()} is downloaded as pre-built, we need to build it instead of installing pre-built."); - return static::FAILURE; - } - // Before build: load buildroot/ directory - $before_buildroot = FileSystem::scanDirFiles(BUILD_ROOT_PATH, relative: true); - // build - $lib->tryBuild(true); - // do something like patching pkg-conf files. - $lib->beforePack(); - // sanity check for libs (check if the libraries are built correctly) - $this->sanityCheckLib($lib); - // After build: load buildroot/ directory, and calculate increase files - $after_buildroot = FileSystem::scanDirFiles(BUILD_ROOT_PATH, relative: true); - $increase_files = array_diff($after_buildroot, $before_buildroot); - - // patch pkg-config and la files with absolute path - foreach ($increase_files as $file) { - if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) { - $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); - $origin_files[$file] = $content; - // replace relative paths with absolute paths - $content = str_replace( - array_keys($placehoder), - array_values($placehoder), - $content - ); - FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); - } - } - - // add .spc-extract-placeholder.json in BUILD_ROOT_PATH - $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; - file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT)); - $increase_files[] = '.spc-extract-placeholder.json'; - - // every file mapped with BUILD_ROOT_PATH - // get BUILD_ROOT_PATH last dir part - $buildroot_part = basename(BUILD_ROOT_PATH); - $increase_files = array_map(fn ($file) => $buildroot_part . '/' . $file, $increase_files); - // write list to packlib_files.txt - FileSystem::writeFile(WORKING_DIR . '/packlib_files.txt', implode("\n", $increase_files)); - // pack - $filename = Config::getPreBuilt('match-pattern'); - $replace = [ - '{name}' => $lib->getName(), - '{arch}' => arch2gnu(php_uname('m')), - '{os}' => strtolower(PHP_OS_FAMILY), - '{libc}' => SPCTarget::getLibc() ?? 'default', - '{libcver}' => SPCTarget::getLibcVersion() ?? 'default', - ]; - // detect suffix, for proper tar option - $tar_option = $this->getTarOptionFromSuffix(Config::getPreBuilt('match-pattern')); - $filename = str_replace(array_keys($replace), array_values($replace), $filename); - $filename = WORKING_DIR . '/dist/' . $filename; - f_passthru("tar {$tar_option} {$filename} -T " . WORKING_DIR . '/packlib_files.txt'); - logger()->info('Pack library ' . $lib->getName() . ' to ' . $filename . ' complete.'); - - // remove temp files - unlink($placeholder_file); - } - } - - foreach ($origin_files as $file => $content) { - // restore original files - if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { - FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); - } - } - - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Build libs complete, used ' . $time . ' s !'); - return static::SUCCESS; - } - - private function sanityCheckLib(LibraryBase $lib): void - { - logger()->info('Sanity check for library ' . $lib->getName()); - // config - foreach ($lib->getStaticLibs() as $static_lib) { - if (!file_exists(FileSystem::convertPath(BUILD_LIB_PATH . '/' . $static_lib))) { - throw new ValidationException( - 'Static library ' . $static_lib . ' not found in ' . BUILD_LIB_PATH, - validation_module: "Static library {$static_lib} existence check" - ); - } - } - } - - /** - * Get tar compress options from suffix - * - * @param string $name Package file name - * @return string Tar options for packaging libs - */ - private function getTarOptionFromSuffix(string $name): string - { - if (str_ends_with($name, '.tar')) { - return '-cf'; - } - if (str_ends_with($name, '.tar.gz') || str_ends_with($name, '.tgz')) { - return '-czf'; - } - if (str_ends_with($name, '.tar.bz2') || str_ends_with($name, '.tbz2')) { - return '-cjf'; - } - if (str_ends_with($name, '.tar.xz') || str_ends_with($name, '.txz')) { - return '-cJf'; - } - if (str_ends_with($name, '.tar.lz') || str_ends_with($name, '.tlz')) { - return '-c --lzma -f'; - } - return '-cf'; - } -} diff --git a/src/SPC/command/dev/PhpVerCommand.php b/src/SPC/command/dev/PhpVerCommand.php deleted file mode 100644 index 05af1267b..000000000 --- a/src/SPC/command/dev/PhpVerCommand.php +++ /dev/null @@ -1,41 +0,0 @@ -no_motd = true; - parent::initialize($input, $output); - } - - public function handle(): int - { - // Find php from source/php-src - $file = SOURCE_PATH . '/php-src/main/php_version.h'; - if (!file_exists($file)) { - $this->output->writeln('PHP source not found, maybe you need to extract first ?'); - - return static::FAILURE; - } - - $result = preg_match('/#define PHP_VERSION "([^"]+)"/', file_get_contents($file), $match); - if ($result === false) { - $this->output->writeln('PHP source not found, maybe you need to extract first ?'); - - return static::FAILURE; - } - - $this->output->writeln('' . $match[1] . ''); - return static::SUCCESS; - } -} diff --git a/src/SPC/command/dev/SortConfigCommand.php b/src/SPC/command/dev/SortConfigCommand.php deleted file mode 100644 index 7c897cd9e..000000000 --- a/src/SPC/command/dev/SortConfigCommand.php +++ /dev/null @@ -1,80 +0,0 @@ -addArgument('config-name', InputArgument::REQUIRED, 'Your config to be sorted, you can sort "lib", "source" and "ext".'); - } - - public function handle(): int - { - switch ($name = $this->getArgument('config-name')) { - case 'lib': - $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true); - ConfigValidator::validateLibs($file); - uksort($file, function ($a, $b) use ($file) { - $type_a = $file[$a]['type'] ?? 'lib'; - $type_b = $file[$b]['type'] ?? 'lib'; - $type_order = ['root', 'target', 'package', 'lib']; - // compare type first - if ($type_a !== $type_b) { - return array_search($type_a, $type_order) <=> array_search($type_b, $type_order); - } - // compare name - return $a <=> $b; - }); - if (!file_put_contents(ROOT_DIR . '/config/lib.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) { - $this->output->writeln('Write file lib.json failed!'); - return static::FAILURE; - } - break; - case 'source': - $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/source.json'), true); - ConfigValidator::validateSource($file); - uksort($file, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : ($a < $b ? -1 : 1))); - if (!file_put_contents(ROOT_DIR . '/config/source.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) { - $this->output->writeln('Write file source.json failed!'); - return static::FAILURE; - } - break; - case 'ext': - $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/ext.json'), true); - ConfigValidator::validateExts($file); - ksort($file); - if (!file_put_contents(ROOT_DIR . '/config/ext.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) { - $this->output->writeln('Write file ext.json failed!'); - return static::FAILURE; - } - break; - case 'pkg': - $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/pkg.json'), true); - ConfigValidator::validatePkgs($file); - ksort($file); - if (!file_put_contents(ROOT_DIR . '/config/pkg.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) { - $this->output->writeln('Write file pkg.json failed!'); - return static::FAILURE; - } - break; - default: - $this->output->writeln("invalid config name: {$name}"); - return 1; - } - $this->output->writeln('sort success'); - return static::SUCCESS; - } -} diff --git a/src/SPC/doctor/AsCheckItem.php b/src/SPC/doctor/AsCheckItem.php deleted file mode 100644 index 0fa7466fd..000000000 --- a/src/SPC/doctor/AsCheckItem.php +++ /dev/null @@ -1,18 +0,0 @@ -message; - } - - public function getFixItem(): string - { - return $this->fix_item; - } - - public function getFixParams(): array - { - return $this->fix_params; - } - - public function isOK(): bool - { - return $this->ok; - } - - public function setFixItem(string $fix_item = '', array $fix_params = []): void - { - $this->fix_item = $fix_item; - $this->fix_params = $fix_params; - } -} diff --git a/src/SPC/doctor/DoctorHandler.php b/src/SPC/doctor/DoctorHandler.php deleted file mode 100644 index 4ae8d2fe9..000000000 --- a/src/SPC/doctor/DoctorHandler.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ - public static function getValidCheckList(): iterable - { - foreach (AttributeMapper::getDoctorCheckMap() as [$item, $optional]) { - /* @var AsCheckItem $item */ - // optional check - if ($optional !== null && !call_user_func($optional)) { - continue; // skip this when the optional check is false - } - // limit_os check - if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) { - continue; - } - // skipped items by env - $skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: '')); - if (in_array($item->item_name, $skip_items)) { - continue; // skip this item - } - yield $item; - } - } - - /** - * Emit the fix for a given CheckResult. - * - * @param OutputInterface $output the output interface to write messages to - * @param CheckResult $result the result of the check that needs fixing - * @return bool returns true if the fix was successful, false otherwise - */ - public static function emitFix(OutputInterface $output, CheckResult $result): bool - { - keyboard_interrupt_register(function () use ($output) { - $output->writeln('You cancelled fix'); - }); - try { - $fix_result = call_user_func(AttributeMapper::getDoctorFixMap()[$result->getFixItem()], ...$result->getFixParams()); - } catch (SPCException $e) { - $output->writeln('Fix failed: ' . $e->getMessage() . ''); - return false; - } catch (\Throwable $e) { - $output->writeln('Fix failed with an unexpected error: ' . $e->getMessage() . ''); - return false; - } - keyboard_interrupt_unregister(); - return $fix_result; - } -} diff --git a/src/SPC/doctor/OptionalCheck.php b/src/SPC/doctor/OptionalCheck.php deleted file mode 100644 index 4dae938b4..000000000 --- a/src/SPC/doctor/OptionalCheck.php +++ /dev/null @@ -1,11 +0,0 @@ -findCommand($cmd) === null) { - $missing[] = $cmd; - } - } - if (!empty($missing)) { - return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools-bsd', [$missing]); - } - return CheckResult::ok(); - } - - #[AsFixItem('build-tools-bsd')] - public function fixBuildTools(array $missing): bool - { - if (get_current_user() !== 'root') { - $prefix = 'sudo '; - logger()->warning('Current user is not root, using sudo for running command'); - } else { - $prefix = ''; - } - shell(true)->exec("ASSUME_ALWAYS_YES=yes {$prefix}pkg install -y " . implode(' ', $missing)); - - return true; - } -} diff --git a/src/SPC/doctor/item/LinuxMuslCheck.php b/src/SPC/doctor/item/LinuxMuslCheck.php deleted file mode 100644 index c11bd4c41..000000000 --- a/src/SPC/doctor/item/LinuxMuslCheck.php +++ /dev/null @@ -1,100 +0,0 @@ -warning('Current user is not root, using sudo for running command'); - } - // The hardcoded version here is to be consistent with the version compiled by `musl-cross-toolchain`. - $musl_version_name = 'musl-1.2.5'; - $musl_source = [ - 'type' => 'url', - 'url' => "https://musl.libc.org/releases/{$musl_version_name}.tar.gz", - ]; - logger()->info('Downloading ' . $musl_source['url']); - Downloader::downloadSource($musl_version_name, $musl_source); - FileSystem::extractSource($musl_version_name, SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . "/{$musl_version_name}.tar.gz"); - - // Apply CVE-2025-26519 patch - SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . "/{$musl_version_name}"); - SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0002.patch', SOURCE_PATH . "/{$musl_version_name}"); - logger()->info('Installing musl wrapper'); - shell()->cd(SOURCE_PATH . "/{$musl_version_name}") - ->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper') - ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j') - ->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); - // TODO: add path using putenv instead of editing /etc/profile - return true; - } - - #[AsFixItem('fix-musl-cross-make')] - public function fixMuslCrossMake(): bool - { - $prefix = ''; - if (get_current_user() !== 'root') { - $prefix = 'sudo '; - logger()->warning('Current user is not root, using sudo for running command'); - } - $arch = arch2gnu(php_uname('m')); - logger()->info("Downloading package musl-toolchain-{$arch}-linux"); - PackageManager::installPackage("musl-toolchain-{$arch}-linux"); - $pkg_root = PKG_ROOT_PATH . "/musl-toolchain-{$arch}-linux"; - shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); - FileSystem::removeDir($pkg_root); - return true; - } -} diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php deleted file mode 100644 index 6e4c5034f..000000000 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ /dev/null @@ -1,136 +0,0 @@ - '/usr/share/perl5/FindBin.pm', - 'binutils-gold' => 'ld.gold', - 'base-devel' => 'automake', - 'gettext-devel' => 'gettextize', - 'gettext-dev' => 'gettextize', - 'perl-IPC-Cmd' => '/usr/share/perl5/vendor_perl/IPC/Cmd.pm', - 'perl-Time-Piece' => '/usr/lib64/perl5/Time/Piece.pm', - ]; - - /** @noinspection PhpUnused */ - #[AsCheckItem('if necessary tools are installed', limit_os: 'Linux', level: 999)] - public function checkCliTools(): ?CheckResult - { - $distro = SystemUtil::getOSRelease(); - - $required = match ($distro['dist']) { - 'alpine' => self::TOOLS_ALPINE, - 'redhat' => self::TOOLS_RHEL, - 'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd', 'perl-Time-Piece']), - 'arch' => self::TOOLS_ARCH, - default => self::TOOLS_DEBIAN, - }; - $missing = []; - foreach ($required as $package) { - if (self::findCommand(self::PROVIDED_COMMAND[$package] ?? $package) === null) { - $missing[] = $package; - } - } - if (!empty($missing)) { - return CheckResult::fail(implode(', ', $missing) . ' not installed on your system', 'install-linux-tools', [$distro, $missing]); - } - return CheckResult::ok(); - } - - #[AsCheckItem('if cmake version >= 3.22', limit_os: 'Linux')] - public function checkCMakeVersion(): ?CheckResult - { - $ver = get_cmake_version(); - if ($ver === null) { - return CheckResult::fail('Failed to get cmake version'); - } - if (version_compare($ver, '3.22.0') < 0) { - return CheckResult::fail('cmake version is too low (' . $ver . '), please update it manually!'); - } - return CheckResult::ok($ver); - } - - /** @noinspection PhpUnused */ - #[AsCheckItem('if necessary linux headers are installed', limit_os: 'Linux')] - public function checkSystemOSPackages(): ?CheckResult - { - if (SystemUtil::isMuslDist()) { - // check linux-headers installation - if (!file_exists('/usr/include/linux/mman.h')) { - return CheckResult::fail('linux-headers not installed on your system', 'install-linux-tools', [SystemUtil::getOSRelease(), ['linux-headers']]); - } - } - return CheckResult::ok(); - } - - #[AsFixItem('install-linux-tools')] - public function fixBuildTools(array $distro, array $missing): bool - { - $install_cmd = match ($distro['dist']) { - 'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y', - 'alpine' => 'apk add', - 'redhat' => 'dnf install -y', - 'centos' => 'yum install -y', - 'arch' => 'pacman -S --noconfirm', - default => throw new EnvironmentException( - "Current linux distro [{$distro['dist']}] does not have an auto-install script for packages yet.", - 'You can submit an issue to request support: https://github.com/crazywhalecc/static-php-cli/issues' - ), - }; - $prefix = ''; - if (($user = exec('whoami')) !== 'root') { - $prefix = 'sudo '; - logger()->warning('Current user (' . $user . ') is not root, using sudo for running command (may require password input)'); - } - - $is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin', 'neon']); - $to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing; - // debian, alpine libtool -> libtoolize - $to_install = str_replace('libtoolize', 'libtool', $to_install); - shell(true)->exec($prefix . $install_cmd . ' ' . implode(' ', $to_install)); - - return true; - } -} diff --git a/src/SPC/doctor/item/MacOSToolCheckList.php b/src/SPC/doctor/item/MacOSToolCheckList.php deleted file mode 100644 index 7cc9ca2a8..000000000 --- a/src/SPC/doctor/item/MacOSToolCheckList.php +++ /dev/null @@ -1,109 +0,0 @@ -findCommand('brew')) === null) { - return CheckResult::fail('Homebrew is not installed', 'brew'); - } - if ($path !== '/opt/homebrew/bin/brew' && getenv('GNU_ARCH') === 'aarch64') { - return CheckResult::fail('Current homebrew (/usr/local/bin/homebrew) is not installed for M1 Mac, please re-install homebrew in /opt/homebrew/ !'); - } - return CheckResult::ok(); - } - - #[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin')] - public function checkCliTools(): ?CheckResult - { - $missing = []; - foreach (self::REQUIRED_COMMANDS as $cmd) { - if ($this->findCommand($cmd) === null) { - $missing[] = $cmd; - } - } - if (!empty($missing)) { - return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]); - } - return CheckResult::ok(); - } - - #[AsCheckItem('if bison version is 3.0 or later', limit_os: 'Darwin')] - public function checkBisonVersion(array $command_path = []): ?CheckResult - { - // if the bison command is /usr/bin/bison, it is the system bison that may be too old - if (($bison = $this->findCommand('bison', $command_path)) === null) { - return CheckResult::fail('bison is not installed or too old', 'build-tools', [['bison']]); - } - // check version: bison (GNU Bison) x.y(.z) - $version = shell()->execWithResult("{$bison} --version", false); - if (preg_match('/bison \(GNU Bison\) (\d+)\.(\d+)(?:\.(\d+))?/', $version[1][0], $matches)) { - $major = (int) $matches[1]; - // major should be 3 or later - if ($major < 3) { - // find homebrew keg-only bison - if ($command_path !== []) { - return CheckResult::fail("Current {$bison} version is too old: " . $matches[0]); - } - return $this->checkBisonVersion(['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin']); - } - return CheckResult::ok($matches[0]); - } - return CheckResult::fail('bison version cannot be determined'); - } - - #[AsFixItem('brew')] - public function fixBrew(): bool - { - shell(true)->exec('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'); - return true; - } - - #[AsFixItem('build-tools')] - public function fixBuildTools(array $missing): bool - { - $replacement = [ - 'glibtoolize' => 'libtool', - ]; - foreach ($missing as $cmd) { - if (isset($replacement[$cmd])) { - $cmd = $replacement[$cmd]; - } - shell(true)->exec('brew install --formula ' . escapeshellarg($cmd)); - } - return true; - } -} diff --git a/src/SPC/doctor/item/OSCheckList.php b/src/SPC/doctor/item/OSCheckList.php deleted file mode 100644 index 7975dde93..000000000 --- a/src/SPC/doctor/item/OSCheckList.php +++ /dev/null @@ -1,26 +0,0 @@ -execWithResult("{$pkgconf} --version", false); - if ($ret === 0) { - return CheckResult::ok(implode(' ', $output)); - } - return CheckResult::fail('pkg-config is not functional', 'install-pkgconfig'); - } - - #[AsFixItem('install-pkgconfig')] - public function installPkgConfig(): bool - { - PackageManager::installPackage('pkg-config'); - return true; - } -} diff --git a/src/SPC/doctor/item/Re2cVersionCheck.php b/src/SPC/doctor/item/Re2cVersionCheck.php deleted file mode 100644 index 1c130379c..000000000 --- a/src/SPC/doctor/item/Re2cVersionCheck.php +++ /dev/null @@ -1,53 +0,0 @@ -= 1.0.3', limit_os: 'Linux', level: 20)] - #[AsCheckItem('if re2c version >= 1.0.3', limit_os: 'Darwin', level: 20)] - public function checkRe2cVersion(): ?CheckResult - { - $ver = shell(false)->execWithResult('re2c --version', false); - // match version: re2c X.X(.X) - if ($ver[0] !== 0 || !preg_match('/re2c\s+(\d+\.\d+(\.\d+)?)/', $ver[1][0], $matches)) { - return CheckResult::fail('Failed to get re2c version', 'build-re2c'); - } - $version_string = $matches[1]; - if (version_compare($version_string, '1.0.3') < 0) { - return CheckResult::fail('re2c version is too low (' . $version_string . ')', 'build-re2c'); - } - return CheckResult::ok($version_string); - } - - #[AsFixItem('build-re2c')] - public function buildRe2c(): bool - { - try { - Downloader::downloadSource('re2c'); - } catch (DownloaderException) { - logger()->warning('Failed to download re2c version, trying alternative'); - $alt = Config::getSource('re2c'); - $alt = [...$alt, ...$alt['alt'] ?? []]; - Downloader::downloadSource('re2c', $alt); - } - $builder = BuilderProvider::makeBuilderByInput(new ArgvInput([])); - $builder->proveLibs(['re2c']); - $builder->setupLibs(); - return true; - } -} diff --git a/src/SPC/doctor/item/WindowsToolCheckList.php b/src/SPC/doctor/item/WindowsToolCheckList.php deleted file mode 100644 index 7f71f10e7..000000000 --- a/src/SPC/doctor/item/WindowsToolCheckList.php +++ /dev/null @@ -1,101 +0,0 @@ -execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) { - return CheckResult::fail($path . ' is not built for msvc.', 'install-perl'); - } - return CheckResult::ok(); - } - - #[AsFixItem('install-php-sdk')] - public function installPhpSdk(): bool - { - FileSystem::removeDir(getenv('PHP_SDK_PATH')); - cmd(true)->exec('git.exe clone --depth 1 https://github.com/php/php-sdk-binary-tools.git ' . getenv('PHP_SDK_PATH')); - return true; - } - - #[AsFixItem('install-nasm')] - public function installNasm(): bool - { - PackageManager::installPackage('nasm-x86_64-win'); - return true; - } - - #[AsFixItem('install-perl')] - public function installPerl(): bool - { - $arch = arch2gnu(php_uname('m')); - PackageManager::installPackage("strawberry-perl-{$arch}-win"); - return true; - } -} diff --git a/src/SPC/doctor/item/ZigCheck.php b/src/SPC/doctor/item/ZigCheck.php deleted file mode 100644 index b201e027e..000000000 --- a/src/SPC/doctor/item/ZigCheck.php +++ /dev/null @@ -1,46 +0,0 @@ - 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; - PackageManager::installPackage("zig-{$arch}-{$os}"); - return Zig::isInstalled(); - } -} diff --git a/src/SPC/exception/BuildFailureException.php b/src/SPC/exception/BuildFailureException.php deleted file mode 100644 index f2509e613..000000000 --- a/src/SPC/exception/BuildFailureException.php +++ /dev/null @@ -1,13 +0,0 @@ -solution; - } -} diff --git a/src/SPC/exception/ExceptionHandler.php b/src/SPC/exception/ExceptionHandler.php deleted file mode 100644 index df5e4bf0c..000000000 --- a/src/SPC/exception/ExceptionHandler.php +++ /dev/null @@ -1,227 +0,0 @@ - Build PHP extra info binding */ - private static array $build_php_extra_info = []; - - public static function handleSPCException(SPCException $e): void - { - // XXX error: yyy - $head_msg = match ($class = get_class($e)) { - BuildFailureException::class => "✗ Build failed: {$e->getMessage()}", - DownloaderException::class => "✗ Download failed: {$e->getMessage()}", - EnvironmentException::class => "⚠ Environment check failed: {$e->getMessage()}", - ExecutionException::class => "✗ Command execution failed: {$e->getMessage()}", - FileSystemException::class => "✗ File system error: {$e->getMessage()}", - InterruptException::class => "⚠ Build interrupted by user: {$e->getMessage()}", - PatchException::class => "✗ Patch apply failed: {$e->getMessage()}", - SPCInternalException::class => "✗ SPC internal error: {$e->getMessage()}", - ValidationException::class => "⚠ Validation failed: {$e->getMessage()}", - WrongUsageException::class => $e->getMessage(), - default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}", - }; - self::logError($head_msg); - - // ---------------------------------------- - $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); - - if ($minor_logs) { - return; - } - - self::logError("----------------------------------------\n"); - - // get the SPCException module - if ($lib_info = $e->getLibraryInfo()) { - self::logError('Failed module: ' . ConsoleColor::yellow("library {$lib_info['library_name']} builder for {$lib_info['os']}")); - } elseif ($ext_info = $e->getExtensionInfo()) { - self::logError('Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder")); - } elseif (self::$builder) { - $os = match (get_class(self::$builder)) { - WindowsBuilder::class => 'Windows', - MacOSBuilder::class => 'macOS', - LinuxBuilder::class => 'Linux', - BSDBuilder::class => 'FreeBSD', - default => 'Unknown OS', - }; - self::logError('Failed module: ' . ConsoleColor::yellow("Builder for {$os}")); - } elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) { - self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); - } - - // get command execution info - if ($e instanceof ExecutionException) { - self::logError(''); - self::logError('Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand())); - if ($cd = $e->getCd()) { - self::logError('Command executed in: ' . ConsoleColor::yellow($cd)); - } - if ($env = $e->getEnv()) { - self::logError('Command inline env variables:'); - foreach ($env as $k => $v) { - self::logError(ConsoleColor::yellow("{$k}={$v}"), 4); - } - } - } - - // validation error - if ($e instanceof ValidationException) { - self::logError('Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString())); - } - - // environment error - if ($e instanceof EnvironmentException) { - self::logError('Failed environment check: ' . ConsoleColor::yellow($e->getMessage())); - if (($solution = $e->getSolution()) !== null) { - self::logError('Solution: ' . ConsoleColor::yellow($solution)); - } - } - - // get patch info - if ($e instanceof PatchException) { - self::logError("Failed patch module: {$e->getPatchModule()}"); - } - - // get internal trace - if ($e instanceof SPCInternalException) { - self::logError('Internal trace:'); - self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4); - } - - // get the full build info if possible - if ($info = ExceptionHandler::$build_php_extra_info) { - self::logError('', output_log: defined('DEBUG_MODE')); - self::logError('Build PHP extra info:', output_log: defined('DEBUG_MODE')); - self::printArrayInfo($info); - } - - // get the full builder options if possible - if ($e->getBuildPHPInfo()) { - $info = $e->getBuildPHPInfo(); - self::logError('', output_log: defined('DEBUG_MODE')); - self::logError('Builder function: ' . ConsoleColor::yellow($info['builder_function']), output_log: defined('DEBUG_MODE')); - } - - self::logError("\n----------------------------------------\n"); - - // convert log file path if in docker - $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); - $shell_log_convert = get_display_path(SPC_SHELL_LOG); - $spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR); - - self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($spc_log_convert)); - if (file_exists(SPC_SHELL_LOG)) { - self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($shell_log_convert)); - } - if ($e->getExtraLogFiles() !== []) { - foreach ($e->getExtraLogFiles() as $key => $file) { - self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none("{$spc_logs_dir_convert}/{$file}")); - } - } - if (!defined('DEBUG_MODE')) { - self::logError('⚠ If you want to see more details in console, use `--debug` option.'); - } - } - - public static function handleDefaultException(\Throwable $e): void - { - $class = get_class($e); - $file = $e->getFile(); - $line = $e->getLine(); - self::logError("✗ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); - self::logError('Stack trace:'); - self::logError(ConsoleColor::gray($e->getTraceAsString()) . PHP_EOL, 4); - self::logError('⚠ Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues'); - } - - public static function bindBuilder(?BuilderBase $bind_builder): void - { - self::$builder = $bind_builder; - } - - public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void - { - self::$build_php_extra_info = $build_php_extra_info; - } - - private static function logError($message, int $indent_space = 0, bool $output_log = true): void - { - $spc_log = fopen(SPC_OUTPUT_LOG, 'a'); - $msg = explode("\n", (string) $message); - foreach ($msg as $v) { - $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); - fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); - if ($output_log) { - echo ConsoleColor::red($line) . PHP_EOL; - } - } - } - - /** - * Print array info to console and log. - */ - private static function printArrayInfo(array $info): void - { - $log_output = defined('DEBUG_MODE'); - $maxlen = 0; - foreach ($info as $k => $v) { - $maxlen = max(strlen($k), $maxlen); - } - foreach ($info as $k => $v) { - if (is_string($v)) { - if ($v === '') { - self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('""'), 4, $log_output); - } else { - self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v), 4, $log_output); - } - } elseif (is_array($v) && !is_assoc_array($v)) { - if ($v === []) { - self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('[]'), 4, $log_output); - continue; - } - $first = array_shift($v); - self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first), 4, $log_output); - foreach ($v as $vs) { - self::logError(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs), 4, $log_output); - } - } elseif (is_bool($v) || is_null($v)) { - self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::cyan($v === true ? 'true' : ($v === false ? 'false' : 'null')), 4, $log_output); - } else { - self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow(json_encode($v, JSON_PRETTY_PRINT)), 4, $log_output); - } - } - } -} diff --git a/src/SPC/exception/ExecutionException.php b/src/SPC/exception/ExecutionException.php deleted file mode 100644 index 6a4d1b9d4..000000000 --- a/src/SPC/exception/ExecutionException.php +++ /dev/null @@ -1,58 +0,0 @@ -cmd instanceof UnixShell || $this->cmd instanceof WindowsCmd) { - return $this->cmd->getLastCommand(); - } - return $this->cmd; - } - - /** - * Returns the directory in which the command was executed. - */ - public function getCd(): ?string - { - return $this->cd; - } - - /** - * Returns the environment variables that were set during the command execution. - */ - public function getEnv(): array - { - return $this->env; - } -} diff --git a/src/SPC/exception/FileSystemException.php b/src/SPC/exception/FileSystemException.php deleted file mode 100644 index 2fa3e9165..000000000 --- a/src/SPC/exception/FileSystemException.php +++ /dev/null @@ -1,7 +0,0 @@ -patch_module; - } -} diff --git a/src/SPC/exception/SPCException.php b/src/SPC/exception/SPCException.php deleted file mode 100644 index e8b233613..000000000 --- a/src/SPC/exception/SPCException.php +++ /dev/null @@ -1,147 +0,0 @@ -loadStackTraceInfo(); - } - - public function bindExtensionInfo(array $extension_info): void - { - $this->extension_info = $extension_info; - } - - public function addExtraLogFile(string $key, string $filename): void - { - $this->extra_log_files[$key] = $filename; - } - - /** - * Returns an array containing information about the SPC module. - * - * This method can be overridden by subclasses to provide specific module information. - * - * @return null|array{ - * library_name: string, - * library_class: string, - * os: string, - * file: null|string, - * line: null|int, - * } an array containing module information - */ - public function getLibraryInfo(): ?array - { - return $this->library_info; - } - - /** - * Returns an array containing information about the PHP build process. - * - * @return null|array{ - * builder_function: string, - * file: null|string, - * line: null|int, - * } an array containing PHP build information - */ - public function getBuildPHPInfo(): ?array - { - return $this->build_php_info; - } - - /** - * Returns an array containing information about the SPC extension. - * - * This method can be overridden by subclasses to provide specific extension information. - * - * @return null|array{ - * extension_name: string, - * extension_class: string, - * file: null|string, - * line: null|int, - * } an array containing extension information - */ - public function getExtensionInfo(): ?array - { - return $this->extension_info; - } - - public function getExtraLogFiles(): array - { - return $this->extra_log_files; - } - - private function loadStackTraceInfo(): void - { - $trace = $this->getTrace(); - foreach ($trace as $frame) { - if (!isset($frame['class'])) { - continue; - } - - // Check if the class is a subclass of LibraryBase - if (!$this->library_info && is_a($frame['class'], LibraryBase::class, true)) { - try { - $reflection = new \ReflectionClass($frame['class']); - if ($reflection->hasConstant('NAME')) { - $name = $reflection->getConstant('NAME'); - if ($name !== 'unknown') { - $this->library_info = [ - 'library_name' => $name, - 'library_class' => $frame['class'], - 'os' => match (true) { - is_a($frame['class'], BSDLibraryBase::class, true) => 'BSD', - is_a($frame['class'], LinuxLibraryBase::class, true) => 'Linux', - is_a($frame['class'], MacOSLibraryBase::class, true) => 'macOS', - is_a($frame['class'], WindowsLibraryBase::class, true) => 'Windows', - default => 'Unknown', - }, - 'file' => $frame['file'] ?? null, - 'line' => $frame['line'] ?? null, - ]; - continue; - } - } - } catch (\ReflectionException) { - continue; - } - } - - // Check if the class is a subclass of BuilderBase and the method is buildPHP - if (!$this->build_php_info && is_a($frame['class'], BuilderBase::class, true)) { - $this->build_php_info = [ - 'builder_function' => $frame['function'], - 'file' => $frame['file'] ?? null, - 'line' => $frame['line'] ?? null, - ]; - } - } - } -} diff --git a/src/SPC/exception/SPCInternalException.php b/src/SPC/exception/SPCInternalException.php deleted file mode 100644 index f0d75d4a6..000000000 --- a/src/SPC/exception/SPCInternalException.php +++ /dev/null @@ -1,12 +0,0 @@ -getTrace() as $trace) { - // Extension validate() => "Extension validator" - if (is_a($trace['class'] ?? null, Extension::class, true) && $trace['function'] === 'validate') { - $this->validation_module = 'Extension validator'; - break; - } - - // Other => "ClassName::functionName" - $this->validation_module = [ - 'class' => $trace['class'] ?? null, - 'function' => $trace['function'], - ]; - break; - } - } else { - $this->validation_module = $validation_module; - } - } - - /** - * Returns the validation module string. - */ - public function getValidationModuleString(): string - { - if ($this->validation_module === null) { - return 'Unknown'; - } - if (is_string($this->validation_module)) { - return $this->validation_module; - } - $str = $this->validation_module['class'] ?? null; - if ($str !== null) { - $str .= '::'; - } - return ($str ?? '') . $this->validation_module['function']; - } -} diff --git a/src/SPC/exception/WrongUsageException.php b/src/SPC/exception/WrongUsageException.php deleted file mode 100644 index 77c93aff6..000000000 --- a/src/SPC/exception/WrongUsageException.php +++ /dev/null @@ -1,13 +0,0 @@ - ['-windows', '-win', ''], - 'Darwin' => ['-macos', '-unix', ''], - 'Linux' => ['-linux', '-unix', ''], - 'BSD' => ['-freebsd', '-bsd', '-unix', ''], - default => throw new WrongUsageException('OS ' . PHP_OS_FAMILY . ' is not supported'), - }; - foreach ($m_key as $v) { - if (isset(self::$pre_built["{$name}{$v}"])) { - return self::$pre_built["{$name}{$v}"]; - } - } - } - return self::$pre_built[$name] ?? null; - } - - /** - * Get source configuration by name - * - * @param string $name The name of the source - * @return null|array The source configuration or null if not found - */ - public static function getSource(string $name): ?array - { - if (self::$source === null) { - self::$source = FileSystem::loadConfigArray('source'); - } - return self::$source[$name] ?? null; - } - - /** - * Get package configuration by name - * - * @param string $name The name of the package - * @return null|array The package configuration or null if not found - */ - public static function getPkg(string $name): ?array - { - if (self::$pkg === null) { - self::$pkg = FileSystem::loadConfigArray('pkg'); - } - return self::$pkg[$name] ?? null; - } - - /** - * Get library configuration by name and optional key - * Supports platform-specific configurations for different operating systems - * - * @param string $name The name of the library - * @param null|string $key The configuration key (static-libs, headers, lib-depends, lib-suggests, frameworks, bin) - * @param mixed $default Default value if key not found - * @return mixed The library configuration or default value - */ - public static function getLib(string $name, ?string $key = null, mixed $default = null) - { - if (self::$lib === null) { - self::$lib = FileSystem::loadConfigArray('lib'); - } - if (!isset(self::$lib[$name])) { - throw new WrongUsageException('lib [' . $name . '] is not supported yet'); - } - $supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks', 'bin']; - if ($key !== null && in_array($key, $supported_sys_based)) { - $m_key = match (PHP_OS_FAMILY) { - 'Windows' => ['-windows', '-win', ''], - 'Darwin' => ['-macos', '-unix', ''], - 'Linux' => ['-linux', '-unix', ''], - 'BSD' => ['-freebsd', '-bsd', '-unix', ''], - default => throw new WrongUsageException('OS ' . PHP_OS_FAMILY . ' is not supported'), - }; - foreach ($m_key as $v) { - if (isset(self::$lib[$name][$key . $v])) { - return self::$lib[$name][$key . $v]; - } - } - return $default; - } - if ($key !== null) { - return self::$lib[$name][$key] ?? $default; - } - return self::$lib[$name]; - } - - /** - * Get all library configurations - * - * @return array All library configurations - */ - public static function getLibs(): array - { - if (self::$lib === null) { - self::$lib = FileSystem::loadConfigArray('lib'); - } - return self::$lib; - } - - /** - * Get extension target configuration by name - * - * @param string $name The name of the extension - * @return null|array The extension target configuration or default ['static', 'shared'] - */ - public static function getExtTarget(string $name): ?array - { - if (self::$ext === null) { - self::$ext = FileSystem::loadConfigArray('ext'); - } - if (!isset(self::$ext[$name])) { - throw new WrongUsageException('ext [' . $name . '] is not supported yet'); - } - return self::$ext[$name]['target'] ?? ['static', 'shared']; - } - - /** - * Get extension configuration by name and optional key - * Supports platform-specific configurations for different operating systems - * - * @param string $name The name of the extension - * @param null|string $key The configuration key (lib-depends, lib-suggests, ext-depends, ext-suggests, arg-type) - * @param mixed $default Default value if key not found - * @return mixed The extension configuration or default value - */ - public static function getExt(string $name, ?string $key = null, mixed $default = null) - { - if (self::$ext === null) { - self::$ext = FileSystem::loadConfigArray('ext'); - } - if (!isset(self::$ext[$name])) { - throw new WrongUsageException('ext [' . $name . '] is not supported yet'); - } - $supported_sys_based = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests', 'arg-type']; - if ($key !== null && in_array($key, $supported_sys_based)) { - $m_key = match (PHP_OS_FAMILY) { - 'Windows' => ['-windows', '-win', ''], - 'Darwin' => ['-macos', '-unix', ''], - 'Linux' => ['-linux', '-unix', ''], - 'BSD' => ['-freebsd', '-bsd', '-unix', ''], - default => throw new WrongUsageException('OS ' . PHP_OS_FAMILY . ' is not supported'), - }; - foreach ($m_key as $v) { - if (isset(self::$ext[$name][$key . $v])) { - return self::$ext[$name][$key . $v]; - } - } - return $default; - } - if ($key !== null) { - return self::$ext[$name][$key] ?? $default; - } - return self::$ext[$name]; - } - - /** - * Get all extension configurations - * - * @return array All extension configurations - */ - public static function getExts(): array - { - if (self::$ext === null) { - self::$ext = FileSystem::loadConfigArray('ext'); - } - return self::$ext; - } - - /** - * Get all source configurations - * - * @return array All source configurations - */ - public static function getSources(): array - { - if (self::$source === null) { - self::$source = FileSystem::loadConfigArray('source'); - } - return self::$source; - } -} diff --git a/src/SPC/store/CurlHook.php b/src/SPC/store/CurlHook.php deleted file mode 100644 index b25ae2bf9..000000000 --- a/src/SPC/store/CurlHook.php +++ /dev/null @@ -1,39 +0,0 @@ -debug('no github token found, skip'); - return; - } - if (getenv('GITHUB_USER')) { - $auth = base64_encode(getenv('GITHUB_USER') . ':' . $token); - $he = "Authorization: Basic {$auth}"; - if (!in_array($he, $headers)) { - $headers[] = $he; - } - logger()->info("using basic github token for {$method} {$url}"); - } else { - $auth = $token; - $he = "Authorization: Bearer {$auth}"; - if (!in_array($he, $headers)) { - $headers[] = $he; - } - logger()->info("using bearer github token for {$method} {$url}"); - } - } -} diff --git a/src/SPC/store/DirDiff.php b/src/SPC/store/DirDiff.php deleted file mode 100644 index 8cd5c1d40..000000000 --- a/src/SPC/store/DirDiff.php +++ /dev/null @@ -1,95 +0,0 @@ -reset(); - } - - /** - * Reset the baseline to current state. - */ - public function reset(): void - { - $this->before = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; - - if ($this->track_content_changes) { - $this->before_file_hashes = []; - foreach ($this->before as $file) { - $this->before_file_hashes[$file] = md5_file($this->dir . DIRECTORY_SEPARATOR . $file); - } - } - } - - /** - * Get the list of incremented files. - * - * @param bool $relative Return relative paths or absolute paths - * @return array List of incremented files - */ - public function getIncrementFiles(bool $relative = false): array - { - $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; - $diff = array_diff($after, $this->before); - if ($relative) { - return $diff; - } - return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $diff); - } - - /** - * Get the list of changed files (including new files). - * - * @param bool $relative Return relative paths or absolute paths - * @param bool $include_new_files Include new files as changed files - * @return array List of changed files - */ - public function getChangedFiles(bool $relative = false, bool $include_new_files = true): array - { - $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; - $changed = []; - foreach ($after as $file) { - if (isset($this->before_file_hashes[$file])) { - $after_hash = md5_file($this->dir . DIRECTORY_SEPARATOR . $file); - if ($after_hash !== $this->before_file_hashes[$file]) { - $changed[] = $file; - } - } elseif ($include_new_files) { - // New file, consider as changed - $changed[] = $file; - } - } - if ($relative) { - return $changed; - } - return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $changed); - } - - /** - * Get the list of removed files. - * - * @param bool $relative Return relative paths or absolute paths - * @return array List of removed files - */ - public function getRemovedFiles(bool $relative = false): array - { - $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; - $removed = array_diff($this->before, $after); - if ($relative) { - return $removed; - } - return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $removed); - } -} diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php deleted file mode 100644 index ccf61dd8d..000000000 --- a/src/SPC/store/Downloader.php +++ /dev/null @@ -1,711 +0,0 @@ - [url, filename] - */ - public static function getPIEInfo(string $name, array $source): array - { - $packagist_url = "https://repo.packagist.org/p2/{$source['repo']}.json"; - logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); - $data = json_decode(self::curlExec( - url: $packagist_url, - retries: self::getRetryAttempts() - ), true); - if (!isset($data['packages'][$source['repo']]) || !is_array($data['packages'][$source['repo']])) { - throw new DownloaderException("failed to find {$name} repo info from packagist"); - } - // get the first version - $first = $data['packages'][$source['repo']][0] ?? []; - // check 'type' => 'php-ext' or contains 'php-ext' key - if (!isset($first['php-ext'])) { - throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); - } - // get download link from dist - $dist_url = $first['dist']['url'] ?? null; - $dist_type = $first['dist']['type'] ?? null; - if (!$dist_url || !$dist_type) { - throw new DownloaderException("failed to find {$name} dist info from packagist"); - } - $name = str_replace('/', '_', $source['repo']); - $version = $first['version'] ?? 'unknown'; - // file name use: $name-$version.$dist_type - return [$dist_url, "{$name}-{$version}.{$dist_type}"]; - } - - /** - * Get latest version from BitBucket tag - * - * @param string $name Source name - * @param array $source Source meta info: [repo] - * @return array [url, filename] - */ - public static function getLatestBitbucketTag(string $name, array $source): array - { - logger()->debug("finding {$name} source from bitbucket tag"); - $data = json_decode(self::curlExec( - url: "https://api.bitbucket.org/2.0/repositories/{$source['repo']}/refs/tags", - retries: self::getRetryAttempts() - ), true); - $ver = $data['values'][0]['name']; - if (!$ver) { - throw new DownloaderException("failed to find {$name} bitbucket source"); - } - $url = "https://bitbucket.org/{$source['repo']}/get/{$ver}.tar.gz"; - $headers = self::curlExec( - url: $url, - method: 'HEAD', - retries: self::getRetryAttempts() - ); - preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $headers, $matches); - if ($matches) { - $filename = $matches['filename']; - } else { - $filename = "{$name}-{$data['tag_name']}.tar.gz"; - } - - return [$url, $filename]; - } - - /** - * Get latest version from GitHub tarball - * - * @param string $name Source name - * @param array $source Source meta info: [repo] - * @param string $type Type of tarball, default is 'releases' - * @return array [url, filename] - */ - public static function getLatestGithubTarball(string $name, array $source, string $type = 'releases'): array - { - logger()->debug("finding {$name} source from github {$type} tarball"); - $source['query'] ??= ''; - $data = json_decode(self::curlExec( - url: "https://api.github.com/repos/{$source['repo']}/{$type}{$source['query']}", - hooks: [[CurlHook::class, 'setupGithubToken']], - retries: self::getRetryAttempts() - ), true, 512, JSON_THROW_ON_ERROR); - - $url = null; - foreach ($data as $rel) { - if (($rel['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) { - continue; - } - if (($rel['draft'] ?? false) === true && (($source['prefer-stable'] ?? false) || !$rel['tarball_url'])) { - continue; - } - if (!($source['match'] ?? null)) { - $url = $rel['tarball_url'] ?? null; - break; - } - if (preg_match('|' . $source['match'] . '|', $rel['tarball_url'])) { - $url = $rel['tarball_url']; - break; - } - } - if (!$url) { - throw new DownloaderException("failed to find {$name} source"); - } - $headers = self::curlExec( - url: $url, - method: 'HEAD', - hooks: [[CurlHook::class, 'setupGithubToken']], - retries: self::getRetryAttempts() - ); - preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $headers, $matches); - if ($matches) { - $filename = $matches['filename']; - } else { - $filename = "{$name}-" . ($type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz'; - } - - return [$url, $filename]; - } - - /** - * Get latest version from GitHub release (uploaded archive) - * - * @param string $name Source name - * @param array $source Source meta info: [repo, match] - * @param bool $match_result Whether to return matched result by `match` param (default: true) - * @return array When $match_result = true, and we matched, [url, filename]. Otherwise, [{asset object}. ...] - */ - public static function getLatestGithubRelease(string $name, array $source, bool $match_result = true): array - { - logger()->debug("finding {$name} from github releases assets"); - $data = json_decode(self::curlExec( - url: "https://api.github.com/repos/{$source['repo']}/releases", - hooks: [[CurlHook::class, 'setupGithubToken']], - retries: self::getRetryAttempts() - ), true); - $url = null; - $filename = null; - foreach ($data as $release) { - if (($source['prefer-stable'] ?? false) === true && $release['prerelease'] === true) { - continue; - } - logger()->debug("Found {$release['name']} releases assets"); - if (!$match_result) { - return $release['assets']; - } - foreach ($release['assets'] as $asset) { - if (preg_match('|' . $source['match'] . '|', $asset['name'])) { - $url = "https://api.github.com/repos/{$source['repo']}/releases/assets/{$asset['id']}"; - $filename = $asset['name']; - break 2; - } - } - } - - if (!$url || !$filename) { - throw new DownloaderException("failed to find {$name} release metadata"); - } - - return [$url, $filename]; - } - - /** - * Get latest version from file list (regex based crawler) - * - * @param string $name Source name - * @param array $source Source meta info: [filelist] - * @return array [url, filename] - */ - public static function getFromFileList(string $name, array $source): array - { - logger()->debug("finding {$name} source from file list"); - $page = self::curlExec($source['url'], retries: self::getRetryAttempts()); - preg_match_all($source['regex'], $page, $matches); - if (!$matches) { - throw new DownloaderException("Failed to get {$name} version"); - } - $versions = []; - foreach ($matches['version'] as $i => $version) { - $lowerVersion = strtolower($version); - foreach ([ - 'alpha', - 'beta', - 'rc', - 'pre', - 'nightly', - 'snapshot', - 'dev', - ] as $betaVersion) { - if (str_contains($lowerVersion, $betaVersion)) { - continue 2; - } - } - $versions[$version] = $matches['file'][$i]; - } - uksort($versions, 'version_compare'); - - return [$source['url'] . end($versions), end($versions), key($versions)]; - } - - /** - * Download file from URL - * - * @param string $name Download name - * @param string $url Download URL - * @param string $filename Target filename - * @param null|string $move_path Optional move path after download - * @param int $download_as Download type constant - * @param array $headers Optional HTTP headers - * @param array $hooks Optional curl hooks - */ - public static function downloadFile(string $name, string $url, string $filename, ?string $move_path = null, int $download_as = SPC_DOWNLOAD_SOURCE, array $headers = [], array $hooks = []): void - { - logger()->debug("Downloading {$url}"); - $cancel_func = function () use ($filename) { - if (file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename))) { - logger()->warning('Deleting download file: ' . $filename); - unlink(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename)); - } - }; - keyboard_interrupt_register($cancel_func); - self::curlDown(url: $url, path: FileSystem::convertPath(DOWNLOAD_PATH . "/{$filename}"), headers: $headers, hooks: $hooks, retries: self::getRetryAttempts()); - keyboard_interrupt_unregister(); - logger()->debug("Locking {$filename}"); - if ($download_as === SPC_DOWNLOAD_PRE_BUILT) { - $name = self::getPreBuiltLockName($name); - } - LockFile::lockSource($name, ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => $filename, 'move_path' => $move_path, 'lock_as' => $download_as]); - } - - /** - * Download Git repository - * - * @param string $name Repository name - * @param string $url Git repository URL - * @param string $branch Branch to checkout - * @param null|array $submodules Optional submodules to initialize - * @param null|string $move_path Optional move path after download - * @param int $retries Number of retry attempts - * @param int $lock_as Lock type constant - */ - public static function downloadGit(string $name, string $url, string $branch, ?array $submodules = null, ?string $move_path = null, int $retries = 0, int $lock_as = SPC_DOWNLOAD_SOURCE): void - { - $download_path = FileSystem::convertPath(DOWNLOAD_PATH . "/{$name}"); - if (file_exists($download_path)) { - FileSystem::removeDir($download_path); - } - logger()->debug("cloning {$name} source"); - - $quiet = !defined('DEBUG_MODE') ? '-q --quiet' : ''; - $git = SPC_GIT_EXEC; - $shallow = defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : ''; - $recursive = ($submodules === null && defined('GIT_SHALLOW_CLONE')) ? '--recursive --shallow-submodules' : null; - $recursive ??= $submodules === null ? '--recursive' : ''; - - try { - self::registerCancelEvent(function () use ($download_path) { - if (is_dir($download_path)) { - logger()->warning('Removing path ' . $download_path); - FileSystem::removeDir($download_path); - } - }); - f_passthru("{$git} clone {$quiet} --config core.autocrlf=false --branch \"{$branch}\" {$shallow} {$recursive} \"{$url}\" \"{$download_path}\""); - if ($submodules !== null) { - $depth_flag = defined('GIT_SHALLOW_CLONE') ? '--depth 1' : ''; - foreach ($submodules as $submodule) { - f_passthru("cd \"{$download_path}\" && {$git} submodule update --init {$depth_flag} " . escapeshellarg($submodule)); - } - } - } catch (SPCException $e) { - if (is_dir($download_path)) { - FileSystem::removeDir($download_path); - } - if ($e->getCode() === 2 || $e->getCode() === -1073741510) { - throw new InterruptException('Keyboard interrupted, download failed !'); - } - if ($retries > 0) { - self::downloadGit($name, $url, $branch, $submodules, $move_path, $retries - 1, $lock_as); - return; - } - throw $e; - } finally { - self::unregisterCancelEvent(); - } - // Lock - logger()->debug("Locking git source {$name}"); - LockFile::lockSource($name, ['source_type' => SPC_SOURCE_GIT, 'dirname' => $name, 'move_path' => $move_path, 'lock_as' => $lock_as]); - - /* - // 复制目录过去 - if ($path !== $download_path) { - $dst_path = FileSystem::convertPath($path); - $src_path = FileSystem::convertPath($download_path); - switch (PHP_OS_FAMILY) { - case 'Windows': - f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i'); - break; - case 'Linux': - case 'Darwin': - f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"'); - break; - } - }*/ - } - - /** - * @param string $name Package name - * @param null|array{ - * type: string, - * repo: ?string, - * url: ?string, - * rev: ?string, - * path: ?string, - * filename: ?string, - * match: ?string, - * prefer-stable: ?bool, - * extract-files: ?array - * } $pkg Package config - * @param bool $force Download all the time even if it exists - */ - public static function downloadPackage(string $name, ?array $pkg = null, bool $force = false): void - { - if ($pkg === null) { - $pkg = Config::getPkg($name); - } - if ($pkg === null) { - logger()->warning('Package {name} unknown. Skipping.', ['name' => $name]); - return; - } - - if (!is_dir(DOWNLOAD_PATH)) { - FileSystem::createDir(DOWNLOAD_PATH); - } - - if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) { - return; - } - self::downloadByType($pkg['type'], $name, $pkg, $force, SPC_DOWNLOAD_PACKAGE); - } - - /** - * Download source - * - * @param string $name source name - * @param null|array{ - * type: string, - * repo: ?string, - * url: ?string, - * rev: ?string, - * path: ?string, - * filename: ?string, - * match: ?string, - * prefer-stable: ?bool, - * provide-pre-built: ?bool, - * license: array{ - * type: string, - * path: ?string, - * text: ?string - * } - * } $source source meta info: [type, path, rev, url, filename, regex, license] - * @param bool $force Whether to force download (default: false) - * @param int $download_as Lock source type (default: SPC_LOCK_SOURCE) - */ - public static function downloadSource(string $name, ?array $source = null, bool $force = false, int $download_as = SPC_DOWNLOAD_SOURCE): void - { - if ($source === null) { - $source = Config::getSource($name); - } - if ($source === null) { - logger()->warning('Source {name} unknown. Skipping.', ['name' => $name]); - return; - } - - if (!is_dir(DOWNLOAD_PATH)) { - FileSystem::createDir(DOWNLOAD_PATH); - } - - // load lock file - if (self::isAlreadyDownloaded($name, $force, $download_as)) { - return; - } - - self::downloadByType($source['type'], $name, $source, $force, $download_as); - } - - /** - * Use curl command to get http response - * - * @param string $url Target URL - * @param string $method HTTP method (GET, POST, etc.) - * @param array $headers HTTP headers - * @param array $hooks Curl hooks - * @param int $retries Number of retry attempts - * @return string Response body - */ - public static function curlExec(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): string - { - foreach ($hooks as $hook) { - $hook($method, $url, $headers); - } - - FileSystem::findCommandPath('curl'); - - $methodArg = match ($method) { - 'GET' => '', - 'HEAD' => '-I', - default => "-X \"{$method}\"", - }; - $headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); - $retry = $retries > 0 ? "--retry {$retries}" : ''; - $cmd = SPC_CURL_EXEC . " -sfSL {$retry} {$methodArg} {$headerArg} \"{$url}\""; - if (getenv('CACHE_API_EXEC') === 'yes') { - if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'))) { - $cache = []; - } else { - $cache = json_decode(file_get_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache')), true); - } - if (isset($cache[$cmd]) && $cache[$cmd]['expire'] >= time()) { - return $cache[$cmd]['cache']; - } - f_exec($cmd, $output, $ret); - if ($ret === 2 || $ret === -1073741510) { - throw new InterruptException(sprintf('Canceled fetching "%s"', $url)); - } - if ($ret !== 0) { - throw new DownloaderException(sprintf('Failed to fetch "%s"', $url)); - } - $cache[$cmd]['cache'] = implode("\n", $output); - $cache[$cmd]['expire'] = time() + 3600; - file_put_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'), json_encode($cache)); - return $cache[$cmd]['cache']; - } - f_exec($cmd, $output, $ret); - if ($ret === 2 || $ret === -1073741510) { - throw new InterruptException(sprintf('Canceled fetching "%s"', $url)); - } - if ($ret !== 0) { - throw new DownloaderException(sprintf('Failed to fetch "%s"', $url)); - } - return implode("\n", $output); - } - - /** - * Use curl to download sources from url - * - * @param string $url Download URL - * @param string $path Target file path - * @param string $method HTTP method - * @param array $headers HTTP headers - * @param array $hooks Curl hooks - * @param int $retries Number of retry attempts - */ - public static function curlDown(string $url, string $path, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): void - { - $used_headers = $headers; - foreach ($hooks as $hook) { - $hook($method, $url, $used_headers); - } - - $methodArg = match ($method) { - 'GET' => '', - 'HEAD' => '-I', - default => "-X \"{$method}\"", - }; - $headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $used_headers)); - $check = !defined('DEBUG_MODE') ? 's' : '#'; - $retry = $retries > 0 ? "--retry {$retries}" : ''; - $cmd = SPC_CURL_EXEC . " -{$check}fSL {$retry} -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\""; - try { - f_passthru($cmd); - } catch (\Throwable $e) { - if ($e->getCode() === 2 || $e->getCode() === -1073741510) { - throw new InterruptException('Keyboard interrupted, download failed !'); - } - throw $e; - } - } - - /** - * Get pre-built lock name from source - * - * @param string $source Source name - * @return string Lock name - */ - public static function getPreBuiltLockName(string $source): string - { - $os_family = PHP_OS_FAMILY; - $gnu_arch = getenv('GNU_ARCH') ?: 'unknown'; - $libc = SPCTarget::getLibc(); - $libc_version = SPCTarget::getLibcVersion() ?? 'default'; - - return "{$source}-{$os_family}-{$gnu_arch}-{$libc}-{$libc_version}"; - } - - /** - * Get default alternative source - * - * @param string $source_name Source name - * @return array Alternative source configuration - */ - public static function getDefaultAlternativeSource(string $source_name): array - { - return [ - 'type' => 'custom', - 'func' => function (bool $force, array $source, int $download_as) use ($source_name) { - logger()->debug("Fetching alternative source for {$source_name}"); - // get from dl.static-php.dev - $url = "https://dl.static-php.dev/static-php-cli/deps/spc-download-mirror/{$source_name}/?format=json"; - $json = json_decode(Downloader::curlExec(url: $url, retries: intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0)), true); - if (!is_array($json)) { - throw new DownloaderException('failed http fetch'); - } - $item = $json[0] ?? null; - if ($item === null) { - throw new DownloaderException('failed to parse json'); - } - $full_url = 'https://dl.static-php.dev' . $item['full_path']; - $filename = basename($item['full_path']); - Downloader::downloadFile($source_name, $full_url, $filename, $source['path'] ?? null, $download_as); - }, - ]; - } - - /** - * Register CTRL+C event for different OS. - * - * @param callable $callback callback function - */ - private static function registerCancelEvent(callable $callback): void - { - if (PHP_OS_FAMILY === 'Windows') { - sapi_windows_set_ctrl_handler($callback); - } elseif (extension_loaded('pcntl')) { - pcntl_signal(2, $callback); - } else { - logger()->debug('You have not enabled `pcntl` extension, cannot prevent download file corruption when Ctrl+C'); - } - } - - /** - * Unegister CTRL+C event for different OS. - */ - private static function unregisterCancelEvent(): void - { - if (PHP_OS_FAMILY === 'Windows') { - sapi_windows_set_ctrl_handler(null); - } elseif (extension_loaded('pcntl')) { - pcntl_signal(2, SIG_IGN); - } - } - - private static function getRetryAttempts(): int - { - return intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0); - } - - private static function isAlreadyDownloaded(string $name, bool $force, int $download_as = SPC_DOWNLOAD_SOURCE): bool - { - // If the lock file exists, skip downloading for source mode - $lock_item = LockFile::get($name); - if (!$force && $download_as === SPC_DOWNLOAD_SOURCE && $lock_item !== null) { - if (file_exists($path = LockFile::getLockFullPath($lock_item))) { - logger()->notice("Source [{$name}] already downloaded: {$path}"); - return true; - } - } - $lock_name = self::getPreBuiltLockName($name); - $lock_item = LockFile::get($lock_name); - if (!$force && $download_as === SPC_DOWNLOAD_PRE_BUILT && $lock_item !== null) { - // lock name with env - if (file_exists($path = LockFile::getLockFullPath($lock_item))) { - logger()->notice("Pre-built content [{$name}] already downloaded: {$path}"); - return true; - } - } - if (!$force && $download_as === SPC_DOWNLOAD_PACKAGE && $lock_item !== null) { - if (file_exists($path = LockFile::getLockFullPath($lock_item))) { - logger()->notice("Source [{$name}] already downloaded: {$path}"); - return true; - } - } - return false; - } - - /** - * Download by type. - * - * @param string $type Types - * @param string $name Download item name - * @param array{ - * url?: string, - * repo?: string, - * rev?: string, - * path?: string, - * filename?: string, - * dirname?: string, - * match?: string, - * prefer-stable?: bool, - * extract?: string, - * submodules?: array, - * provide-pre-built?: bool, - * func?: ?callable, - * license?: array - * } $conf Download item config - * @param bool $force Force download - * @param int $download_as Lock source type - */ - private static function downloadByType(string $type, string $name, array $conf, bool $force, int $download_as): void - { - try { - switch ($type) { - case 'pie': // Packagist - [$url, $filename] = self::getPIEInfo($name, $conf); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $conf); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $conf); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $conf, 'tags'); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $conf); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $conf); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); - break; - case 'url': // Direct download URL - $url = $conf['url']; - $filename = $conf['filename'] ?? basename($conf['url']); - self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); - break; - case 'git': // Git repo - self::downloadGit($name, $conf['url'], $conf['rev'], $conf['submodules'] ?? null, $conf['path'] ?? $conf['extract'] ?? null, self::getRetryAttempts(), $download_as); - break; - case 'local': // Local directory, do nothing, just lock it - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $conf['dirname'], - 'move_path' => $conf['path'] ?? $conf['extract'] ?? null, - 'lock_as' => $download_as, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - if (isset($conf['func'])) { - $conf['name'] = $name; - $conf['func']($force, $conf, $download_as); - break; - } - $classes = [ - ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'), - ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'), - ]; - foreach ($classes as $class) { - if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { - (new $class())->fetch($force, $conf, $download_as); - break; - } - if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { - $cls = new $class(); - if (in_array($name, $cls->getSupportName())) { - (new $class())->fetch($name, $force, $conf); - break; - } - } - } - break; - default: - throw new DownloaderException("Unknown download type: {$type}"); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning("Deleting download file: {$filename}"); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException("Download failed: {$e->getMessage()}"); - } - } -} diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php deleted file mode 100644 index d2d44b51f..000000000 --- a/src/SPC/store/FileSystem.php +++ /dev/null @@ -1,761 +0,0 @@ -debug('Reading file: ' . $filename); - $r = file_get_contents(self::convertPath($filename)); - if ($r === false) { - throw new FileSystemException('Reading file ' . $filename . ' failed'); - } - return $r; - } - - /** - * Replace string content in file - * - * @param string $filename The file path - * @param mixed $search The search string - * @param mixed $replace The replacement string - * @return false|int Number of replacements or false on failure - */ - public static function replaceFileStr(string $filename, mixed $search = null, mixed $replace = null): false|int - { - return self::replaceFile($filename, REPLACE_FILE_STR, $search, $replace); - } - - /** - * Replace content in file using regex - * - * @param string $filename The file path - * @param mixed $search The regex pattern - * @param mixed $replace The replacement string - * @return false|int Number of replacements or false on failure - */ - public static function replaceFileRegex(string $filename, mixed $search = null, mixed $replace = null): false|int - { - return self::replaceFile($filename, REPLACE_FILE_PREG, $search, $replace); - } - - /** - * Replace content in file using custom callback - * - * @param string $filename The file path - * @param mixed $callback The callback function - * @return false|int Number of replacements or false on failure - */ - public static function replaceFileUser(string $filename, mixed $callback = null): false|int - { - return self::replaceFile($filename, REPLACE_FILE_USER, $callback); - } - - /** - * Get file extension from filename - * - * @param string $fn The filename - * @return string The file extension (without dot) - */ - public static function extname(string $fn): string - { - $parts = explode('.', basename($fn)); - if (count($parts) < 2) { - return ''; - } - return array_pop($parts); - } - - /** - * Find command path in system PATH (similar to which command) - * - * @param string $name The command name - * @param array $paths Optional array of paths to search - * @return null|string The full path to the command or null if not found - */ - public static function findCommandPath(string $name, array $paths = []): ?string - { - if (!$paths) { - $paths = explode(PATH_SEPARATOR, getenv('PATH')); - } - if (PHP_OS_FAMILY === 'Windows') { - foreach ($paths as $path) { - foreach (['.exe', '.bat', '.cmd'] as $suffix) { - if (file_exists($path . DIRECTORY_SEPARATOR . $name . $suffix)) { - return $path . DIRECTORY_SEPARATOR . $name . $suffix; - } - } - } - return null; - } - foreach ($paths as $path) { - if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { - return $path . DIRECTORY_SEPARATOR . $name; - } - } - return null; - } - - /** - * Copy directory recursively - * - * @param string $from Source directory path - * @param string $to Destination directory path - */ - public static function copyDir(string $from, string $to): void - { - logger()->debug("Copying directory from {$from} to {$to}"); - $dst_path = FileSystem::convertPath($to); - $src_path = FileSystem::convertPath($from); - switch (PHP_OS_FAMILY) { - case 'Windows': - f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/y/i'); - break; - case 'Linux': - case 'Darwin': - case 'BSD': - f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"'); - break; - } - } - - /** - * Copy file from one location to another. - * This method will throw an exception if the copy operation fails. - * - * @param string $from Source file path - * @param string $to Destination file path - */ - public static function copy(string $from, string $to): void - { - logger()->debug("Copying file from {$from} to {$to}"); - $dst_path = FileSystem::convertPath($to); - $src_path = FileSystem::convertPath($from); - if ($src_path === $dst_path) { - return; - } - if (!copy($src_path, $dst_path)) { - throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path); - } - } - - /** - * Extract package archive to specified directory - * - * @param string $name Package name - * @param string $source_type Archive type (tar.gz, zip, etc.) - * @param string $filename Archive filename - * @param null|string $extract_path Optional extraction path - */ - public static function extractPackage(string $name, string $source_type, string $filename, ?string $extract_path = null): void - { - if ($extract_path !== null) { - // replace - $extract_path = self::replacePathVariable($extract_path); - $extract_path = self::isRelativePath($extract_path) ? (WORKING_DIR . '/' . $extract_path) : $extract_path; - } else { - $extract_path = PKG_ROOT_PATH . '/' . $name; - } - logger()->info("Extracting {$name} package to {$extract_path} ..."); - $target = self::convertPath($extract_path); - - if (!is_dir($dir = dirname($target))) { - self::createDir($dir); - } - try { - // extract wrapper command - self::extractWithType($source_type, $filename, $extract_path); - } catch (SPCException $e) { - if (PHP_OS_FAMILY === 'Windows') { - f_passthru('rmdir /s /q ' . $target); - } else { - f_passthru('rm -rf ' . $target); - } - throw new FileSystemException("Cannot extract package {$name}", $e->getCode(), $e); - } - } - - /** - * Extract source archive to source directory - * - * @param string $name Source name - * @param string $source_type Archive type (tar.gz, zip, etc.) - * @param string $filename Archive filename - * @param null|string $move_path Optional move path - */ - public static function extractSource(string $name, string $source_type, string $filename, ?string $move_path = null): void - { - // if source hook is empty, load it - if (self::$_extract_hook === []) { - SourcePatcher::init(); - } - $move_path = match ($move_path) { - null => SOURCE_PATH . '/' . $name, - default => self::isRelativePath($move_path) ? (SOURCE_PATH . '/' . $move_path) : $move_path, - }; - $target = self::convertPath($move_path); - logger()->info("Extracting {$name} source to {$target}" . ' ...'); - if (!is_dir($dir = dirname($target))) { - self::createDir($dir); - } - try { - self::extractWithType($source_type, $filename, $move_path); - self::emitSourceExtractHook($name, $target); - } catch (SPCException $e) { - if (PHP_OS_FAMILY === 'Windows') { - f_passthru('rmdir /s /q ' . $target); - } else { - f_passthru('rm -rf ' . $target); - } - throw new FileSystemException('Cannot extract source ' . $name . ': ' . $e->getMessage(), $e->getCode(), $e); - } - } - - /** - * Convert path to system-specific format - * - * @param string $path The path to convert - * @return string The converted path - */ - public static function convertPath(string $path): string - { - if (str_starts_with($path, 'phar://')) { - return $path; - } - return str_replace('/', DIRECTORY_SEPARATOR, $path); - } - - /** - * Convert Windows path to MinGW format - * - * @param string $path The Windows path - * @return string The MinGW format path - */ - public static function convertWinPathToMinGW(string $path): string - { - if (preg_match('/^[A-Za-z]:/', $path)) { - $path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2)); - } - return $path; - } - - /** - * Scan directory files recursively - * - * @param string $dir Directory to scan - * @param bool $recursive Whether to scan recursively - * @param bool|string $relative Whether to return relative paths - * @param bool $include_dir Whether to include directories in result - * @return array|false Array of files or false on failure - */ - public static function scanDirFiles(string $dir, bool $recursive = true, bool|string $relative = false, bool $include_dir = false): array|false - { - $dir = self::convertPath($dir); - if (!is_dir($dir)) { - return false; - } - logger()->debug('scanning directory ' . $dir); - $scan_list = scandir($dir); - if ($scan_list === false) { - logger()->warning('Scan dir failed, cannot scan directory: ' . $dir); - return false; - } - $list = []; - // 将 relative 置为相对目录的前缀 - if ($relative === true) { - $relative = $dir; - } - // 遍历目录 - foreach ($scan_list as $v) { - // Unix 系统排除这俩目录 - if ($v == '.' || $v == '..') { - continue; - } - $sub_file = self::convertPath($dir . '/' . $v); - if (is_dir($sub_file) && $recursive) { - # 如果是 目录 且 递推 , 则递推添加下级文件 - $sub_list = self::scanDirFiles($sub_file, $recursive, $relative); - if (is_array($sub_list)) { - foreach ($sub_list as $item) { - $list[] = $item; - } - } - } elseif (is_file($sub_file) || (is_dir($sub_file) && !$recursive && $include_dir)) { - # 如果是 文件 或 (是 目录 且 不递推 且 包含目录) - if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) { - $list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\'); - } elseif ($relative === false) { - $list[] = $sub_file; - } - } - } - return $list; - } - - /** - * Get PSR-4 classes from directory - * - * @param string $dir Directory to scan - * @param string $base_namespace Base namespace - * @param mixed $rule Optional filtering rule - * @param bool|string $return_path_value Whether to return path as value - * @return array Array of class names or class=>path pairs - */ - public static function getClassesPsr4(string $dir, string $base_namespace, mixed $rule = null, bool|string $return_path_value = false): array - { - $classes = []; - $files = FileSystem::scanDirFiles($dir, true, true); - if ($files === false) { - throw new FileSystemException('Cannot scan dir files during get classes psr-4 from dir: ' . $dir); - } - foreach ($files as $v) { - $pathinfo = pathinfo($v); - if (($pathinfo['extension'] ?? '') == 'php') { - if ($rule === null) { - if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) { - continue; - } - if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') { - continue; - } - } elseif (is_callable($rule) && !$rule($dir, $pathinfo)) { - continue; - } - $dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\'); - $class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename']; - if (is_string($return_path_value)) { - $classes[$class_name] = $return_path_value . '/' . $v; - } else { - $classes[] = $class_name; - } - } - } - return $classes; - } - - /** - * Remove directory recursively - * - * @param string $dir Directory to remove - * @return bool Success status - */ - public static function removeDir(string $dir): bool - { - $dir = FileSystem::convertPath($dir); - logger()->debug('Removing path recursively: "' . $dir . '"'); - if (!file_exists($dir)) { - logger()->debug('Scan dir failed, no such file or directory.'); - return false; - } - if (!is_dir($dir)) { - logger()->warning('Scan dir failed, not directory.'); - return false; - } - logger()->debug('scanning directory ' . $dir); - // 套上 zm_dir - $scan_list = scandir($dir); - if ($scan_list === false) { - logger()->warning('Scan dir failed, cannot scan directory: ' . $dir); - return false; - } - // 遍历目录 - foreach ($scan_list as $v) { - // Unix 系统排除这俩目录 - if ($v == '.' || $v == '..') { - continue; - } - $sub_file = self::convertPath($dir . '/' . $v); - if (is_link($sub_file) || is_file($sub_file)) { - if (!unlink($sub_file)) { - return false; - } - } elseif (is_dir($sub_file)) { - # 如果是 目录 且 递推 , 则递推添加下级文件 - if (!self::removeDir($sub_file)) { - return false; - } - } - } - if (is_link($dir)) { - return unlink($dir); - } - return rmdir($dir); - } - - /** - * Create directory recursively - * - * @param string $path Directory path to create - */ - public static function createDir(string $path): void - { - if (!is_dir($path) && !f_mkdir($path, 0755, true) && !is_dir($path)) { - throw new FileSystemException(sprintf('Unable to create dir: %s', $path)); - } - } - - /** - * Write content to file - * - * @param string $path File path - * @param mixed $content Content to write - * @param mixed ...$args Additional arguments passed to file_put_contents - * @return bool|int|string Result of file writing operation - */ - public static function writeFile(string $path, mixed $content, ...$args): bool|int|string - { - $dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME); - if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) { - throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir); - } - return file_put_contents($path, $content, ...$args); - } - - /** - * Reset directory by removing and recreating it - * - * @param string $dir_name Directory name - */ - public static function resetDir(string $dir_name): void - { - $dir_name = self::convertPath($dir_name); - if (is_dir($dir_name)) { - self::removeDir($dir_name); - } - self::createDir($dir_name); - } - - /** - * Add source extraction hook - * - * @param string $name Source name - * @param callable $callback Callback function - */ - public static function addSourceExtractHook(string $name, callable $callback): void - { - self::$_extract_hook[$name][] = $callback; - } - - /** - * Check if path is relative - * - * @param string $path Path to check - * @return bool True if path is relative - */ - public static function isRelativePath(string $path): bool - { - if (DIRECTORY_SEPARATOR === '\\') { - return !(strlen($path) > 2 && ctype_alpha($path[0]) && $path[1] === ':'); - } - return strlen($path) > 0 && $path[0] !== '/'; - } - - /** - * Replace path variables with actual values - * - * @param string $path Path with variables - * @return string Path with replaced variables - */ - public static function replacePathVariable(string $path): string - { - $replacement = [ - '{pkg_root_path}' => PKG_ROOT_PATH, - '{php_sdk_path}' => getenv('PHP_SDK_PATH') ? getenv('PHP_SDK_PATH') : WORKING_DIR . '/php-sdk-binary-tools', - '{working_dir}' => WORKING_DIR, - '{download_path}' => DOWNLOAD_PATH, - '{source_path}' => SOURCE_PATH, - ]; - return str_replace(array_keys($replacement), array_values($replacement), $path); - } - - /** - * Create backup of file - * - * @param string $path File path - * @return string Backup file path - */ - public static function backupFile(string $path): string - { - copy($path, $path . '.bak'); - return $path . '.bak'; - } - - /** - * Restore file from backup - * - * @param string $path Original file path - */ - public static function restoreBackupFile(string $path): void - { - if (!file_exists($path . '.bak')) { - throw new FileSystemException("Backup restore failed: Cannot find bak file for {$path}"); - } - copy($path . '.bak', $path); - unlink($path . '.bak'); - } - - /** - * Remove file if it exists - * - * @param string $string File path - */ - public static function removeFileIfExists(string $string): void - { - $string = self::convertPath($string); - if (file_exists($string)) { - unlink($string); - } - } - - /** - * Replace line in file that contains specific string - * - * @param string $file File path - * @param string $find String to find in line - * @param string $line New line content - * @return false|int Number of replacements or false on failure - */ - public static function replaceFileLineContainsString(string $file, string $find, string $line): false|int - { - $lines = file($file); - if ($lines === false) { - throw new FileSystemException('Cannot read file: ' . $file); - } - foreach ($lines as $key => $value) { - if (str_contains($value, $find)) { - $lines[$key] = $line . PHP_EOL; - } - } - return file_put_contents($file, implode('', $lines)); - } - - /** - * Move file or directory, handling cross-device scenarios - * Uses rename() if possible, falls back to copy+delete for cross-device moves - * - * @param string $source Source path - * @param string $dest Destination path - */ - public static function moveFileOrDir(string $source, string $dest): void - { - $source = self::convertPath($source); - $dest = self::convertPath($dest); - - // Check if source and dest are on the same device to avoid cross-device rename errors - $source_stat = @stat($source); - $dest_parent = dirname($dest); - $dest_stat = @stat($dest_parent); - - // Only use rename if on same device - if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) { - if (@rename($source, $dest)) { - return; - } - } - - // Fall back to copy + delete for cross-device moves or if rename failed - if (is_dir($source)) { - self::copyDir($source, $dest); - self::removeDir($source); - } else { - if (!copy($source, $dest)) { - throw new FileSystemException("Failed to copy file from {$source} to {$dest}"); - } - if (!unlink($source)) { - throw new FileSystemException("Failed to remove source file: {$source}"); - } - } - } - - private static function extractArchive(string $filename, string $target): void - { - // Create base dir - if (f_mkdir(directory: $target, recursive: true) !== true) { - throw new FileSystemException('create ' . $target . ' dir failed'); - } - if (!file_exists($filename)) { - throw new FileSystemException('File not exists'); - } - - if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'BSD'])) { - match (self::extname($filename)) { - 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), - 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), - 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), - 'zip' => self::unzipWithStrip($filename, $target), - default => throw new FileSystemException('unknown archive format: ' . $filename), - }; - } elseif (PHP_OS_FAMILY === 'Windows') { - // use php-sdk-binary-tools/bin/7za.exe - $_7z = self::convertPath(getenv('PHP_SDK_PATH') . '/bin/7za.exe'); - - // Windows notes: I hate windows tar....... - // When extracting .tar.gz like libxml2, it shows a symlink error and returns code[1]. - // Related posts: https://answers.microsoft.com/en-us/windows/forum/all/tar-on-windows-fails-to-extract-archive-containing/0ee9a7ea-9b1f-4fef-86a9-5d9dc35cea2f - // And MinGW tar.exe cannot work on temporarily storage ??? (GitHub Actions hosted runner) - // Yeah, I will be an MS HATER ! - match (self::extname($filename)) { - 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), - 'xz', 'txz', 'gz', 'tgz', 'bz2' => cmd()->execWithResult("\"{$_7z}\" x -so {$filename} | tar -f - -x -C \"{$target}\" --strip-components 1"), - 'zip' => self::unzipWithStrip($filename, $target), - default => throw new FileSystemException("unknown archive format: {$filename}"), - }; - } - } - - private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int - { - logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename); - $file = self::readFile($filename); - switch ($replace_type) { - case REPLACE_FILE_STR: - default: - $file = str_replace($callback_or_search, $to_replace, $file); - break; - case REPLACE_FILE_PREG: - $file = preg_replace($callback_or_search, $to_replace, $file); - break; - case REPLACE_FILE_USER: - $file = $callback_or_search($file); - break; - } - return file_put_contents($filename, $file); - } - - private static function emitSourceExtractHook(string $name, string $target): void - { - foreach ((self::$_extract_hook[$name] ?? []) as $hook) { - if ($hook($name, $target) === true) { - logger()->info('Patched source [' . $name . '] after extracted'); - } - } - } - - private static function extractWithType(string $source_type, string $filename, string $extract_path): void - { - logger()->debug("Extracting source [{$source_type}]: {$filename}"); - /* @phpstan-ignore-next-line */ - match ($source_type) { - SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path), - SPC_SOURCE_GIT => self::copyDir(self::convertPath($filename), $extract_path), - // soft link to the local source - SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path), - }; - } - - /** - * Unzip file with stripping top-level directory - */ - private static function unzipWithStrip(string $zip_file, string $extract_path): void - { - $temp_dir = self::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); - $zip_file = self::convertPath($zip_file); - $extract_path = self::convertPath($extract_path); - - // extract to temp dir - self::createDir($temp_dir); - - if (PHP_OS_FAMILY === 'Windows') { - $mute = defined('DEBUG_MODE') ? '' : ' > NUL'; - // use php-sdk-binary-tools/bin/7za.exe - $_7z = self::convertPath(getenv('PHP_SDK_PATH') . '/bin/7za.exe'); - f_passthru("\"{$_7z}\" x {$zip_file} -o{$temp_dir} -y{$mute}"); - } else { - $mute = defined('DEBUG_MODE') ? '' : ' > /dev/null'; - f_passthru("unzip \"{$zip_file}\" -d \"{$temp_dir}\"{$mute}"); - } - // scan first level dirs (relative, not recursive, include dirs) - $contents = self::scanDirFiles($temp_dir, false, true, true); - if ($contents === false) { - throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); - } - // if extract path already exists, remove it - if (is_dir($extract_path)) { - self::removeDir($extract_path); - } - // if only one dir, move its contents to extract_path - $subdir = self::convertPath("{$temp_dir}/{$contents[0]}"); - if (count($contents) === 1 && is_dir($subdir)) { - self::moveFileOrDir($subdir, $extract_path); - } else { - // else, if it contains only one dir, strip dir and copy other files - $dircount = 0; - $dir = []; - $top_files = []; - foreach ($contents as $item) { - if (is_dir(self::convertPath("{$temp_dir}/{$item}"))) { - ++$dircount; - $dir[] = $item; - } else { - $top_files[] = $item; - } - } - // extract dir contents to extract_path - self::createDir($extract_path); - // extract move dir - if ($dircount === 1) { - $sub_contents = self::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true); - if ($sub_contents === false) { - throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); - } - foreach ($sub_contents as $sub_item) { - self::moveFileOrDir(self::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}")); - } - } else { - foreach ($dir as $item) { - self::moveFileOrDir(self::convertPath("{$temp_dir}/{$item}"), self::convertPath("{$extract_path}/{$item}")); - } - } - // move top-level files to extract_path - foreach ($top_files as $top_file) { - self::moveFileOrDir(self::convertPath("{$temp_dir}/{$top_file}"), self::convertPath("{$extract_path}/{$top_file}")); - } - } - - // Clean up temp directory - self::removeDir($temp_dir); - } -} diff --git a/src/SPC/store/LockFile.php b/src/SPC/store/LockFile.php deleted file mode 100644 index 88ecd6cb0..000000000 --- a/src/SPC/store/LockFile.php +++ /dev/null @@ -1,215 +0,0 @@ -warning("Lock entry for '{$lock_name}' has 'source_type' set to 'dir', which is deprecated. Please re-download your dependencies."); - $result['source_type'] = SPC_SOURCE_GIT; - } - - return $result; - } - - /** - * Check if a lock file exists for a given lock name. - * - * @param string $lock_name Lock name to check - */ - public static function isLockFileExists(string $lock_name): bool - { - return match (self::get($lock_name)['source_type'] ?? null) { - SPC_SOURCE_ARCHIVE => file_exists(DOWNLOAD_PATH . '/' . (self::get($lock_name)['filename'] ?? '.never-exist-file')), - SPC_SOURCE_GIT, SPC_SOURCE_LOCAL => is_dir(DOWNLOAD_PATH . '/' . (self::get($lock_name)['dirname'] ?? '.never-exist-dir')), - default => false, - }; - } - - /** - * Put a lock entry into the lock file. - * - * @param string $lock_name Lock name to set or remove - * @param null|array $lock_content lock content to set, or null to remove the lock entry - */ - public static function put(string $lock_name, ?array $lock_content): void - { - self::init(); - - if ($lock_content === null && isset(self::$lock_file_content[$lock_name])) { - self::removeLockFileIfExists(self::$lock_file_content[$lock_name]); - unset(self::$lock_file_content[$lock_name]); - } else { - self::$lock_file_content[$lock_name] = $lock_content; - } - - // Write the updated lock data back to the file - FileSystem::createDir(dirname(self::LOCK_FILE)); - file_put_contents(self::LOCK_FILE, json_encode(self::$lock_file_content, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } - - /** - * Get the full path of a lock file or directory based on the lock options. - * - * @param array $lock_options lock item options, must contain 'source_type', 'filename' or 'dirname' - * @return string the absolute path to the lock file or directory - */ - public static function getLockFullPath(array $lock_options): string - { - return match ($lock_options['source_type']) { - SPC_SOURCE_ARCHIVE => FileSystem::isRelativePath($lock_options['filename']) ? (DOWNLOAD_PATH . '/' . $lock_options['filename']) : $lock_options['filename'], - SPC_SOURCE_GIT, SPC_SOURCE_LOCAL => FileSystem::isRelativePath($lock_options['dirname']) ? (DOWNLOAD_PATH . '/' . $lock_options['dirname']) : $lock_options['dirname'], - default => throw new WrongUsageException("Unknown source type: {$lock_options['source_type']}"), - }; - } - - public static function getExtractPath(string $lock_name, string $default_path): ?string - { - $lock = self::get($lock_name); - if ($lock === null) { - return null; - } - - // If move_path is set, use it; otherwise, use the default extract directory - if (isset($lock['move_path'])) { - if (FileSystem::isRelativePath($lock['move_path'])) { - // If move_path is relative, prepend the default extract directory - return match ($lock['lock_as']) { - SPC_DOWNLOAD_SOURCE, SPC_DOWNLOAD_PRE_BUILT => FileSystem::convertPath(SOURCE_PATH . '/' . $lock['move_path']), - SPC_DOWNLOAD_PACKAGE => FileSystem::convertPath(PKG_ROOT_PATH . '/' . $lock['move_path']), - default => throw new WrongUsageException("Unknown lock type: {$lock['lock_as']}"), - }; - } - return FileSystem::convertPath($lock['move_path']); - } - return FileSystem::convertPath($default_path); - } - - /** - * Get the hash of the lock source based on the lock options. - * - * @param array $lock_options Lock options - * @return string Hash of the lock source - */ - public static function getLockSourceHash(array $lock_options): string - { - $result = match ($lock_options['source_type']) { - SPC_SOURCE_ARCHIVE => sha1_file(DOWNLOAD_PATH . '/' . $lock_options['filename']), - SPC_SOURCE_GIT => exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $lock_options['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD'), - SPC_SOURCE_LOCAL => 'LOCAL HASH IS ALWAYS DIFFERENT', - default => filter_var(getenv('SPC_IGNORE_BAD_HASH'), FILTER_VALIDATE_BOOLEAN) ? '' : throw new SPCInternalException("Unknown source type: {$lock_options['source_type']}"), - }; - if ($result === false && !filter_var(getenv('SPC_IGNORE_BAD_HASH'), FILTER_VALIDATE_BOOLEAN)) { - throw new SPCInternalException("Failed to get hash for source: {$lock_options['source_type']}"); - } - return $result ?: ''; - } - - /** - * @param array $lock_options Lock options - * @param string $destination Target directory - */ - public static function putLockSourceHash(array $lock_options, string $destination): void - { - $hash = LockFile::getLockSourceHash($lock_options); - if ($lock_options['source_type'] === SPC_SOURCE_LOCAL) { - logger()->debug("Source [{$lock_options['dirname']}] is local, no hash will be written."); - return; - } - FileSystem::writeFile("{$destination}/.spc-hash", $hash); - } - - /** - * Try to lock source with hash. - * - * @param string $name Source name - * @param array{ - * source_type: string, - * dirname?: ?string, - * filename?: ?string, - * move_path: ?string, - * lock_as: int - * } $data Source data - */ - public static function lockSource(string $name, array $data): void - { - // calculate hash - $hash = LockFile::getLockSourceHash($data); - $data['hash'] = $hash; - self::put($name, $data); - } - - private static function init(): void - { - if (self::$lock_file_content === null) { - // Initialize the lock file content if it hasn't been loaded yet - if (!file_exists(self::LOCK_FILE)) { - logger()->debug('Lock file does not exist: ' . self::LOCK_FILE . ', initializing empty lock file.'); - self::$lock_file_content = []; - file_put_contents(self::LOCK_FILE, json_encode(self::$lock_file_content, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - } else { - $file_content = file_get_contents(self::LOCK_FILE); - self::$lock_file_content = json_decode($file_content, true); - if (self::$lock_file_content === null) { - throw new SPCInternalException('Failed to decode lock file: ' . self::LOCK_FILE); - } - } - } - } - - /** - * Remove the lock file or directory if it exists. - * - * @param array $lock_options lock item options, must contain 'source_type', 'filename' or 'dirname' - */ - private static function removeLockFileIfExists(array $lock_options): void - { - if ($lock_options['source_type'] === SPC_SOURCE_ARCHIVE) { - $path = self::getLockFullPath($lock_options); - if (file_exists($path)) { - logger()->info('Removing file ' . $path); - unlink($path); - } else { - logger()->debug("Lock file [{$lock_options['filename']}] not found, skip removing file."); - } - } else { - $path = self::getLockFullPath($lock_options); - if (is_dir($path)) { - logger()->info('Removing directory ' . $path); - FileSystem::removeDir($path); - } else { - logger()->debug("Lock directory [{$lock_options['dirname']}] not found, skip removing directory."); - } - } - } -} diff --git a/src/SPC/store/PackageManager.php b/src/SPC/store/PackageManager.php deleted file mode 100644 index dc662a5be..000000000 --- a/src/SPC/store/PackageManager.php +++ /dev/null @@ -1,110 +0,0 @@ - 'linux', - 'Windows' => 'win', - 'BSD' => 'freebsd', - 'Darwin' => 'macos', - default => throw new WrongUsageException('Unsupported OS!'), - }; - $config = Config::getPkg("{$pkg_name}-{$arch}-{$os}"); - $pkg_name = "{$pkg_name}-{$arch}-{$os}"; - } - if ($config === null) { - throw new WrongUsageException("Package [{$pkg_name}] does not exist, please check the name and correct it !"); - } - - // Download package - try { - Downloader::downloadPackage($pkg_name, $config, $force); - } catch (\Throwable $e) { - if (!$allow_alt) { - throw new DownloaderException("Download package {$pkg_name} failed: " . $e->getMessage()); - } - // if download failed, we will try to download alternative packages - logger()->warning("Download package {$pkg_name} failed: " . $e->getMessage()); - $alt = $config['alt'] ?? null; - if ($alt === null) { - logger()->warning("No alternative package found for {$pkg_name}, using default mirror."); - $alt_config = array_merge($config, Downloader::getDefaultAlternativeSource($pkg_name)); - } elseif ($alt === false) { - logger()->error("No alternative package found for {$pkg_name}."); - throw $e; - } else { - logger()->notice("Trying alternative package for {$pkg_name}."); - $alt_config = array_merge($config, $alt); - } - Downloader::downloadPackage($pkg_name, $alt_config, $force); - } - if (!$extract) { - logger()->info("Package [{$pkg_name}] downloaded, but extraction is skipped."); - return; - } - if (Config::getPkg($pkg_name)['type'] === 'custom') { - // Custom extract function - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); - foreach ($classes as $class) { - if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { - $cls = new $class(); - if (in_array($pkg_name, $cls->getSupportName())) { - (new $class())->extract($pkg_name); - break; - } - } - } - return; - } - // After download, read lock file name - $lock = LockFile::get($pkg_name); - $source_type = $lock['source_type']; - $filename = LockFile::getLockFullPath($lock); - $extract = LockFile::getExtractPath($pkg_name, PKG_ROOT_PATH . '/' . $pkg_name); - - FileSystem::extractPackage($pkg_name, $source_type, $filename, $extract); - - // if contains extract-files, we just move this file to destination, and remove extract dir - if (is_array($config['extract-files'] ?? null) && is_assoc_array($config['extract-files'])) { - $scandir = FileSystem::scanDirFiles($extract, true, true); - foreach ($config['extract-files'] as $file => $target) { - $target = FileSystem::convertPath(FileSystem::replacePathVariable($target)); - if (!is_dir($dir = dirname($target))) { - f_mkdir($dir, 0755, true); - } - logger()->debug("Moving package [{$pkg_name}] file {$file} to {$target}"); - // match pattern, needs to scan dir - $file = FileSystem::convertPath($file); - $found = false; - foreach ($scandir as $item) { - if (match_pattern($file, $item)) { - $file = $item; - $found = true; - break; - } - } - if ($found === false) { - throw new FileSystemException('Unable to find extract-files item: ' . $file); - } - rename(FileSystem::convertPath($extract . '/' . $file), $target); - } - FileSystem::removeDir($extract); - } - } -} diff --git a/src/SPC/store/SourceManager.php b/src/SPC/store/SourceManager.php deleted file mode 100644 index 35728e22c..000000000 --- a/src/SPC/store/SourceManager.php +++ /dev/null @@ -1,97 +0,0 @@ - $item) { - if (Config::getSource($source) === null) { - throw new WrongUsageException("Source [{$source}] does not exist, please check the name and correct it !"); - } - // check source downloaded - $pre_built_name = Downloader::getPreBuiltLockName($source); - if ($source_only || LockFile::get($pre_built_name) === null) { - if (LockFile::get($source) === null) { - throw new WrongUsageException("Source [{$source}] not downloaded or not locked, you should download it first !"); - } - $lock_name = $source; - } else { - $lock_name = $pre_built_name; - } - - $lock_content = LockFile::get($lock_name); - - // check source dir exist - $check = LockFile::getExtractPath($lock_name, SOURCE_PATH . '/' . $source); - // $check = $lock[$lock_name]['move_path'] === null ? (SOURCE_PATH . '/' . $source) : (SOURCE_PATH . '/' . $lock[$lock_name]['move_path']); - if (!is_dir($check)) { - logger()->debug("Extracting source [{$source}] to {$check} ..."); - $filename = LockFile::getLockFullPath($lock_content); - FileSystem::extractSource($source, $lock_content['source_type'], $filename, $check); - LockFile::putLockSourceHash($lock_content, $check); - continue; - } - // if a lock file does not have hash, calculate with the current source (backward compatibility) - if (!isset($lock_content['hash'])) { - $hash = LockFile::getLockSourceHash($lock_content); - } else { - $hash = $lock_content['hash']; - } - - // when source already extracted, detect if the extracted source hash is the same as the lock file one - if (file_exists("{$check}/.spc-hash") && FileSystem::readFile("{$check}/.spc-hash") === $hash) { - logger()->debug("Source [{$source}] already extracted in {$check}, skip !"); - continue; - } - - // ext imap was included in php < 8.4 which we should not extract, - // but since it's not simple to compare php version, for now we just skip it - if ($source === 'ext-imap') { - logger()->debug("Source [ext-imap] already extracted in {$check}, skip !"); - continue; - } - - // if not, remove the source dir and extract again - logger()->notice("Source [{$source}] hash mismatch, removing old source dir and extracting again ..."); - FileSystem::removeDir($check); - $filename = LockFile::getLockFullPath($lock_content); - $move_path = LockFile::getExtractPath($lock_name, SOURCE_PATH . '/' . $source); - FileSystem::extractSource($source, $lock_content['source_type'], $filename, $move_path); - LockFile::putLockSourceHash($lock_content, $check); - } - } -} diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php deleted file mode 100644 index 43b750c43..000000000 --- a/src/SPC/store/SourcePatcher.php +++ /dev/null @@ -1,685 +0,0 @@ -getExts() as $ext) { - if ($ext->patchBeforeBuildconf() === true) { - logger()->info("Extension [{$ext->getName()}] patched before buildconf"); - } - } - foreach ($builder->getLibs() as $lib) { - if ($lib->patchBeforeBuildconf() === true) { - logger()->info("Library [{$lib->getName()}] patched before buildconf"); - } - } - // patch windows php 8.1 bug - if (PHP_OS_FAMILY === 'Windows' && $builder->getPHPVersionID() >= 80100 && $builder->getPHPVersionID() < 80200) { - logger()->info('Patching PHP 8.1 windows Fiber bug'); - FileSystem::replaceFileStr( - SOURCE_PATH . '\php-src\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( - SOURCE_PATH . '\php-src\win32\build\config.w32', - "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", - '' - ); - } - - // Fix PHP VS version - if ($builder instanceof WindowsBuilder) { - // get vs version - $vc = \SPC\builder\windows\SystemUtil::findVisualStudio(); - $vc_matches = match ($vc['version']) { - 'vs17' => ['VS17', 'Visual C++ 2022'], - 'vs16' => ['VS16', 'Visual C++ 2019'], - default => ['unknown', 'unknown'], - }; - // patch php-src/win32/build/confutils.js - FileSystem::replaceFileStr( - SOURCE_PATH . '\php-src\win32\build\confutils.js', - 'var name = "unknown";', - "var name = short ? \"{$vc_matches[0]}\" : \"{$vc_matches[1]}\";return name;" - ); - } - - // patch configure.ac - $musl = SPCTarget::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') - ); - if (getenv('SPC_LIBC') === false && ($libc = SPCTarget::getLibc()) !== null) { - putenv("SPC_LIBC={$libc}"); - } - - // patch php-src/build/php.m4 PKG_CHECK_MODULES -> PKG_CHECK_MODULES_STATIC - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/build/php.m4', 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); - - if ($builder->getOption('enable-micro-win32')) { - self::patchMicroWin32(); - } else { - self::unpatchMicroWin32(); - } - } - - /** - * Source patcher runner before configure - * - * @param BuilderBase $builder Builder - */ - public static function patchBeforeConfigure(BuilderBase $builder): void - { - foreach ($builder->getExts() as $ext) { - $patch = $builder instanceof WindowsBuilder ? $ext->patchBeforeWindowsConfigure() : $ext->patchBeforeConfigure(); - if ($patch === true) { - logger()->info("Extension [{$ext->getName()}] patched before configure"); - } - } - foreach ($builder->getLibs() as $lib) { - if ($lib->patchBeforeConfigure() === true) { - logger()->info("Library [{$lib->getName()}] patched before configure"); - } - } - // patch capstone - if (is_unix()) { - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/configure', '/have_capstone="yes"/', 'have_capstone="no"'); - } - - if (file_exists(SOURCE_PATH . '/php-src/configure.ac.bak')) { - // restore configure.ac - FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/configure.ac'); - } - } - - public static function patchMicro(?array $items = null): bool - { - if (!file_exists(SOURCE_PATH . '/php-src/sapi/micro/php_micro.c')) { - return false; - } - $ver_file = SOURCE_PATH . '/php-src/main/php_version.h'; - if (!file_exists($ver_file)) { - throw new FileSystemException('Patch failed, cannot find php source files'); - } - $version_h = FileSystem::readFile(SOURCE_PATH . '/php-src/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(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}.patch")) { - $patches[] = "sapi/micro/patches/{$patchName}.patch"; - continue; - } - for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) { - $tryMajMin = $serial[$i]; - if (!file_exists(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}_{$tryMajMin}.patch")) { - continue; - } - $patches[] = "sapi/micro/patches/{$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(SOURCE_PATH . "/php-src/{$patch}", SOURCE_PATH . '/php-src'); - } - - return true; - } - - /** - * Use existing patch file for patching - * - * @param string $patch_name Patch file name in src/globals/patch/ or absolute path - * @param string $cwd Working directory for patch command - * @param bool $reverse Reverse patches (default: False) - */ - public static function patchFile(string $patch_name, string $cwd, bool $reverse = false): bool - { - try { - if (FileSystem::isRelativePath($patch_name)) { - $patch_file = ROOT_DIR . "/src/globals/patch/{$patch_name}"; - } else { - $patch_file = $patch_name; - } - if (!file_exists($patch_file)) { - return false; - } - - $patch_str = FileSystem::convertPath($patch_file); - if (!file_exists($patch_str)) { - throw new PatchException($patch_name, "Patch file [{$patch_str}] does not exist"); - } - - // Copy patch from phar - if (str_starts_with($patch_str, 'phar://')) { - $filename = pathinfo($patch_file, PATHINFO_BASENAME); - file_put_contents(SOURCE_PATH . "/{$filename}", file_get_contents($patch_file)); - $patch_str = FileSystem::convertPath(SOURCE_PATH . "/{$filename}"); - } - - // detect - $detect_reverse = !$reverse; - $detect_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' - . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) - . ' | patch --dry-run -p1 -s -f ' . ($detect_reverse ? '-R' : '') - . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; - exec($detect_cmd, $output, $detect_status); - - if ($detect_status === 0) { - return true; - } - - // apply patch - $apply_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' - . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) - . ' | patch -p1 ' . ($reverse ? '-R' : ''); - - f_passthru($apply_cmd); - return true; - } catch (ExecutionException $e) { - // If patch failed, throw exception - throw new PatchException($patch_name, "Patch file [{$patch_name}] failed to apply", previous: $e); - } - } - - public static function patchOpenssl11Darwin(): bool - { - if (PHP_OS_FAMILY === 'Darwin' && !file_exists(SOURCE_PATH . '/openssl/VERSION.dat') && file_exists(SOURCE_PATH . '/openssl/test/v3ext.c')) { - FileSystem::replaceFileStr(SOURCE_PATH . '/openssl/test/v3ext.c', '#include ', '#include ' . PHP_EOL . '#include '); - return true; - } - return false; - } - - public static function patchSwoole(): bool - { - // swoole hook needs pdo/pdo.h - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/swoole/config.m4', - 'PHP_ADD_INCLUDE([$ext_srcdir])', - "PHP_ADD_INCLUDE( [\$ext_srcdir] )\n PHP_ADD_INCLUDE([\$abs_srcdir/ext])" - ); - - // swoole 5.1.3 build fix - // get swoole version first - $file = SOURCE_PATH . '/php-src/ext/swoole/include/swoole_version.h'; - // Match #define SWOOLE_VERSION "5.1.3" - $pattern = '/#define SWOOLE_VERSION "(.+)"/'; - if (preg_match($pattern, file_get_contents($file), $matches)) { - $version = $matches[1]; - } else { - $version = '1.0.0'; - } - if ($version === '5.1.3') { - self::patchFile('spc_fix_swoole_50513.patch', SOURCE_PATH . '/php-src/ext/swoole'); - } - if (version_compare($version, '6.0.0', '>=') && version_compare($version, '6.1.0', '<')) { - // remove when https://github.com/swoole/swoole-src/pull/5848 is merged - self::patchFile('swoole_fix_date_time.patch', SOURCE_PATH . '/php-src/ext/swoole'); - // remove when https://github.com/swoole/swoole-src/pull/5847 is merged - self::patchFile('swoole_fix_odbclibs.patch', SOURCE_PATH . '/php-src/ext/swoole'); - } - return true; - } - - public static function patchBeforeMake(BuilderBase $builder): void - { - if ($builder instanceof UnixBuilderBase) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'install-micro', ''); - } - if (!SPCTarget::isStatic() && SPCTarget::getLibc() === 'musl') { - // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } else { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } - - // no asan - // if (strpos(file_get_contents(SOURCE_PATH . '/php-src/Makefile'), 'CFLAGS_CLEAN = -g') === false) { - // FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'CFLAGS_CLEAN = ', 'CFLAGS_CLEAN = -g -fsanitize=address '); - // } - - // call extension patch before make - foreach ($builder->getExts() as $ext) { - if ($ext->patchBeforeMake() === true) { - logger()->info("Extension [{$ext->getName()}] patched before make"); - } - } - foreach ($builder->getLibs() as $lib) { - if ($lib->patchBeforeMake() === true) { - logger()->info("Library [{$lib->getName()}] patched before make"); - } - } - - if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef CONFIGURE_COMMAND', - '#ifdef NO_CONFIGURE_COMMAND', - ); - } else { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef NO_CONFIGURE_COMMAND', - '#ifdef CONFIGURE_COMMAND', - ); - } - } - - public static function patchHardcodedINI(array $ini = []): bool - { - $cli_c = SOURCE_PATH . '/php-src/sapi/cli/php_cli.c'; - $cli_c_bak = SOURCE_PATH . '/php-src/sapi/cli/php_cli.c.bak'; - $micro_c = SOURCE_PATH . '/php-src/sapi/micro/php_micro.c'; - $micro_c_bak = SOURCE_PATH . '/php-src/sapi/micro/php_micro.c.bak'; - $embed_c = SOURCE_PATH . '/php-src/sapi/embed/php_embed.c'; - $embed_c_bak = SOURCE_PATH . '/php-src/sapi/embed/php_embed.c.bak'; - - // Try to reverse backup file - $find_str = 'const char HARDCODED_INI[] ='; - $patch_str = ''; - foreach ($ini as $key => $value) { - $patch_str .= "\"{$key}={$value}\\n\"\n"; - } - $patch_str = "const char HARDCODED_INI[] =\n{$patch_str}"; - - // Detect backup, if we have backup, it means we need to reverse first - if (file_exists($cli_c_bak) || file_exists($micro_c_bak) || file_exists($embed_c_bak)) { - self::unpatchHardcodedINI(); - } - - // Backup it - $result = file_put_contents($cli_c_bak, file_get_contents($cli_c)); - $result = $result && file_put_contents($micro_c_bak, file_get_contents($micro_c)); - $result = $result && file_put_contents($embed_c_bak, file_get_contents($embed_c)); - if ($result === false) { - return false; - } - - // Patch it - FileSystem::replaceFileStr($cli_c, $find_str, $patch_str); - FileSystem::replaceFileStr($micro_c, $find_str, $patch_str); - FileSystem::replaceFileStr($embed_c, $find_str, $patch_str); - return true; - } - - public static function unpatchHardcodedINI(): bool - { - $cli_c = SOURCE_PATH . '/php-src/sapi/cli/php_cli.c'; - $cli_c_bak = SOURCE_PATH . '/php-src/sapi/cli/php_cli.c.bak'; - $micro_c = SOURCE_PATH . '/php-src/sapi/micro/php_micro.c'; - $micro_c_bak = SOURCE_PATH . '/php-src/sapi/micro/php_micro.c.bak'; - $embed_c = SOURCE_PATH . '/php-src/sapi/embed/php_embed.c'; - $embed_c_bak = SOURCE_PATH . '/php-src/sapi/embed/php_embed.c.bak'; - if (!file_exists($cli_c_bak) && !file_exists($micro_c_bak) && !file_exists($embed_c_bak)) { - return false; - } - $result = file_put_contents($cli_c, file_get_contents($cli_c_bak)); - $result = $result && file_put_contents($micro_c, file_get_contents($micro_c_bak)); - $result = $result && file_put_contents($embed_c, file_get_contents($embed_c_bak)); - @unlink($cli_c_bak); - @unlink($micro_c_bak); - @unlink($embed_c_bak); - return $result; - } - - public static function patchMicroPhar(int $version_id): void - { - FileSystem::backupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/phar/phar.c', - 'static zend_op_array *phar_compile_file', - "char *micro_get_filename(void);\n\nstatic zend_op_array *phar_compile_file" - ); - if ($version_id < 80100) { - // PHP 8.0.x - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/phar/phar.c', - 'if (strstr(file_handle->filename, ".phar") && !strstr(file_handle->filename, "://")) {', - 'if ((strstr(file_handle->filename, micro_get_filename()) || strstr(file_handle->filename, ".phar")) && !strstr(file_handle->filename, "://")) {' - ); - } else { - // PHP >= 8.1 - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/ext/phar/phar.c', - 'if (strstr(ZSTR_VAL(file_handle->filename), ".phar") && !strstr(ZSTR_VAL(file_handle->filename), "://")) {', - 'if ((strstr(ZSTR_VAL(file_handle->filename), micro_get_filename()) || strstr(ZSTR_VAL(file_handle->filename), ".phar")) && !strstr(ZSTR_VAL(file_handle->filename), "://")) {' - ); - } - } - - public static function unpatchMicroPhar(): void - { - FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); - } - - /** - * Fix the compilation issue of sqlsrv and pdo_sqlsrv on Windows (/sdl check is too strict and will cause Zend compilation to fail) - */ - public static function patchSQLSRVWin32(string $source_name): bool - { - $source_name = preg_replace('/[^a-z_]/', '', $source_name); - if (file_exists(SOURCE_PATH . '/php-src/ext/' . $source_name . '/config.w32')) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/' . $source_name . '/config.w32', '/sdl', ''); - return true; - } - return false; - } - - /** - * Fix the compilation issue of pdo_sqlsrv with php 8.5 - */ - public static function patchSQLSRVPhp85(): bool - { - $source_dir = SOURCE_PATH . '/php-src/ext/pdo_sqlsrv'; - if (!file_exists($source_dir . '/config.m4') && is_dir($source_dir . '/source/pdo_sqlsrv')) { - FileSystem::moveFileOrDir($source_dir . '/LICENSE', $source_dir . '/source/pdo_sqlsrv/LICENSE'); - FileSystem::moveFileOrDir($source_dir . '/source/shared', $source_dir . '/source/pdo_sqlsrv/shared'); - FileSystem::moveFileOrDir($source_dir . '/source/pdo_sqlsrv', SOURCE_PATH . '/pdo_sqlsrv'); - FileSystem::removeDir($source_dir); - FileSystem::moveFileOrDir(SOURCE_PATH . '/pdo_sqlsrv', $source_dir); - return true; - } - return false; - } - - public static function patchYamlWin32(): bool - { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/yaml/config.w32', "lib.substr(lib.length - 6, 6) == '_a.lib'", "lib.substr(lib.length - 6, 6) == '_a.lib' || 'yes' == 'yes'"); - return true; - } - - public static function patchLibYaml(string $name, string $target): bool - { - if (!file_exists("{$target}/cmake/config.h.in")) { - FileSystem::createDir("{$target}/cmake"); - copy(ROOT_DIR . '/src/globals/extra/libyaml_config.h.in', "{$target}/cmake/config.h.in"); - } - if (!file_exists("{$target}/YamlConfig.cmake.in")) { - copy(ROOT_DIR . '/src/globals/extra/libyaml_yamlConfig.cmake.in', "{$target}/yamlConfig.cmake.in"); - } - return true; - } - - /** - * Patch imap license file for PHP < 8.4 - */ - public static function patchImapLicense(): bool - { - if (!file_exists(SOURCE_PATH . '/php-src/ext/imap/LICENSE') && is_dir(SOURCE_PATH . '/php-src/ext/imap')) { - file_put_contents(SOURCE_PATH . '/php-src/ext/imap/LICENSE', file_get_contents(ROOT_DIR . '/src/globals/extra/Apache_LICENSE')); - return true; - } - return false; - } - - /** - * Patch imagick for PHP 8.4 - */ - public static function patchImagickWith84(): bool - { - // match imagick version id - $file = SOURCE_PATH . '/php-src/ext/imagick/php_imagick.h'; - if (!file_exists($file)) { - return false; - } - $content = file_get_contents($file); - if (preg_match('/#define PHP_IMAGICK_EXTNUM\s+(\d+)/', $content, $match) === 0) { - return false; - } - $extnum = intval($match[1]); - if ($extnum < 30800) { - self::patchFile('imagick_php84_before_30800.patch', SOURCE_PATH . '/php-src/ext/imagick'); - return true; - } - return false; - } - - public static function patchFfiCentos7FixO3strncmp(): bool - { - if (!($ver = SPCTarget::getLibcVersion()) || version_compare($ver, '2.17', '>')) { - return false; - } - if (!file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - return false; - } - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0 && intval($match[1]) < 80316) { - return false; - } - self::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); - return true; - } - - public static function patchPkgConfigForGcc15(): bool - { - self::patchFile('pkg-config_gcc15.patch', SOURCE_PATH . '/pkg-config'); - return true; - } - - public static function patchLibaomForAlpine(): bool - { - if (PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist()) { - self::patchFile('libaom_posix_implict.patch', SOURCE_PATH . '/libaom'); - return true; - } - return false; - } - - public static function patchAttrForAlpine(): bool - { - if (PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist() || PHP_OS_FAMILY === 'Darwin') { - self::patchFile('attr_alpine_gethostname.patch', SOURCE_PATH . '/attr'); - return true; - } - return false; - } - - /** - * Patch cli SAPI Makefile for Windows. - */ - public static function patchWindowsCLITarget(): void - { - // search Makefile code line contains "$(BUILD_DIR)\php.exe:" - $content = FileSystem::readFile(SOURCE_PATH . '/php-src/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(SOURCE_PATH . '/php-src/Makefile', implode("\r\n", $lines)); - } - - /** - * Patch cgi SAPI Makefile for Windows. - */ - public static function patchWindowsCGITarget(): void - { - // search Makefile code line contains "$(BUILD_DIR)\php-cgi.exe:" - $content = FileSystem::readFile(SOURCE_PATH . '/php-src/Makefile'); - $lines = explode("\r\n", $content); - $line_num = 0; - $found = false; - foreach ($lines as $v) { - if (str_contains($v, '$(BUILD_DIR)\php-cgi.exe:')) { - $found = $line_num; - break; - } - ++$line_num; - } - if ($found === false) { - throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); - } - // cli: $(BUILD_DIR)\php.exe: $(DEPS_CLI) $(CLI_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest - // $lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(CLI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest'; - // cgi: $(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest - $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; - - // cli: @"$(LINK)" /nologo $(CGI_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) - $lines[$line_num + 1] = "\t" . '@"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; - FileSystem::writeFile(SOURCE_PATH . '/php-src/Makefile', implode("\r\n", $lines)); - - // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() - FileSystem::replaceFileRegex(SOURCE_PATH . '\php-src\sapi\cgi\cgi_main.c', '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); - } - - public static function patchPhpLibxml212(): bool - { - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { - $ver_id = intval($match[1]); - if ($ver_id < 80000) { - self::patchFile('spc_fix_alpine_build_php80.patch', SOURCE_PATH . '/php-src'); - return true; - } - if ($ver_id < 80100) { - self::patchFile('spc_fix_libxml2_12_php80.patch', SOURCE_PATH . '/php-src'); - self::patchFile('spc_fix_alpine_build_php80.patch', SOURCE_PATH . '/php-src'); - return true; - } - if ($ver_id < 80200) { - // self::patchFile('spc_fix_libxml2_12_php81.patch', SOURCE_PATH . '/php-src'); - self::patchFile('spc_fix_alpine_build_php80.patch', SOURCE_PATH . '/php-src'); - return true; - } - return false; - } - return false; - } - - public static function patchGDWin32(): bool - { - $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); - if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { - $ver_id = intval($match[1]); - if ($ver_id < 80200) { - // see: https://github.com/php/php-src/commit/243966177e39eb71822935042c3f13fa6c5b9eed - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gd/libgd/gdft.c', '#ifndef MSWIN32', '#ifndef _WIN32'); - } - // custom config.w32, because official config.w32 is hard-coded many things - if ($ver_id >= 80500) { - $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_85.w32'); - } elseif ($ver_id >= 80100) { - $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32'); - } else { - $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32'); - } - file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32.bak', file_get_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32')); - return file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32', $origin) !== false; - } - return false; - } - - /** - * Add additional `static-php-cli.version` ini value for PHP source. - */ - public static function patchSPCVersionToPHP(string $version = 'unknown'): void - { - // detect patch (remove this when 8.3 deprecated) - $file = FileSystem::readFile(SOURCE_PATH . '/php-src/main/main.c'); - if (!str_contains($file, 'static-php-cli.version')) { - logger()->debug('Inserting static-php-cli.version to php-src'); - $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); - FileSystem::writeFile(SOURCE_PATH . '/php-src/main/main.c', $file); - } - } - - public static function patchMicroWin32(): void - { - // patch micro win32 - if (!file_exists(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak')) { - copy(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c', SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak'); - FileSystem::replaceFileStr(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c', '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1"); - } - } - - public static function unpatchMicroWin32(): void - { - if (file_exists(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak')) { - rename(SOURCE_PATH . '\php-src\sapi\micro\php_micro.c.win32bak', SOURCE_PATH . '\php-src\sapi\micro\php_micro.c'); - } - } - - public static function patchGMSSL(): void - { - FileSystem::replaceFileStr(SOURCE_PATH . '/gmssl/src/hex.c', 'unsigned char *OPENSSL_hexstr2buf(const char *str, size_t *len)', 'unsigned char *GMSSL_hexstr2buf(const char *str, size_t *len)'); - FileSystem::replaceFileStr(SOURCE_PATH . '/gmssl/src/hex.c', 'OPENSSL_hexchar2int', 'GMSSL_hexchar2int'); - } -} diff --git a/src/SPC/store/pkg/CustomPackage.php b/src/SPC/store/pkg/CustomPackage.php deleted file mode 100644 index eb972d749..000000000 --- a/src/SPC/store/pkg/CustomPackage.php +++ /dev/null @@ -1,49 +0,0 @@ - 'amd64', - 'aarch64' => 'arm64', - default => throw new \InvalidArgumentException('Unsupported architecture: ' . $name), - }; - $os = match (explode('-', $name)[3]) { - 'linux' => 'linux', - 'macos' => 'darwin', - default => throw new \InvalidArgumentException('Unsupported OS: ' . $name), - }; - [$go_version] = explode("\n", Downloader::curlExec('https://go.dev/VERSION?m=text')); - $config = [ - 'type' => 'url', - 'url' => "https://go.dev/dl/{$go_version}.{$os}-{$arch}.tar.gz", - ]; - Downloader::downloadPackage($name, $config, $force); - } - - public function extract(string $name): void - { - $pkgroot = PKG_ROOT_PATH; - $go_exec = "{$pkgroot}/go-xcaddy/bin/go"; - $xcaddy_exec = "{$pkgroot}/go-xcaddy/bin/xcaddy"; - if (file_exists($go_exec) && file_exists($xcaddy_exec)) { - return; - } - $lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true); - $source_type = $lock[$name]['source_type']; - $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); - $extract = $lock[$name]['move_path'] ?? "{$pkgroot}/go-xcaddy"; - - FileSystem::extractPackage($name, $source_type, $filename, $extract); - - $sanitizedPath = getenv('PATH'); - if (PHP_OS_FAMILY === 'Linux' && !SystemUtil::isMuslDist()) { - $sanitizedPath = preg_replace('#(:?/?[^:]*musl[^:]*)#', '', $sanitizedPath); - $sanitizedPath = preg_replace('#^:|:$|::#', ':', $sanitizedPath); // clean up colons - } - - // install xcaddy without using musl tools, xcaddy build requires dynamic linking - shell() - ->appendEnv([ - 'PATH' => "{$pkgroot}/go-xcaddy/bin:" . $sanitizedPath, - 'GOROOT' => "{$pkgroot}/go-xcaddy", - 'GOBIN' => "{$pkgroot}/go-xcaddy/bin", - 'GOPATH' => "{$pkgroot}/go", - ]) - ->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); - } - - public static function getEnvironment(): array - { - $packageName = 'go-xcaddy'; - $pkgroot = PKG_ROOT_PATH; - return [ - 'GOROOT' => "{$pkgroot}/{$packageName}", - 'GOBIN' => "{$pkgroot}/{$packageName}/bin", - 'GOPATH' => "{$pkgroot}/go", - ]; - } - - public static function getPath(): ?string - { - return PKG_ROOT_PATH . '/go-xcaddy/bin'; - } -} diff --git a/src/SPC/store/pkg/Zig.php b/src/SPC/store/pkg/Zig.php deleted file mode 100644 index c2a81c0da..000000000 --- a/src/SPC/store/pkg/Zig.php +++ /dev/null @@ -1,163 +0,0 @@ - "{$pkgroot}/{$name}/zig.exe", - default => "{$pkgroot}/{$name}/zig", - }; - - if ($force) { - FileSystem::removeDir("{$pkgroot}/{$name}"); - } - - if (file_exists($zig_exec)) { - return; - } - - $parts = explode('-', $name); - $arch = $parts[1]; - $os = $parts[2]; - - $zig_arch = match ($arch) { - 'x86_64', 'aarch64' => $arch, - default => throw new WrongUsageException('Unsupported architecture: ' . $arch), - }; - - $zig_os = match ($os) { - 'linux' => 'linux', - 'macos' => 'macos', - 'win' => 'windows', - default => throw new WrongUsageException('Unsupported OS: ' . $os), - }; - - $index_json = json_decode(Downloader::curlExec('https://ziglang.org/download/index.json', hooks: [[CurlHook::class, 'setupGithubToken']]), true); - - $latest_version = null; - foreach ($index_json as $version => $data) { - // Skip the master branch, get the latest stable release - if ($version !== 'master') { - $latest_version = $version; - break; - } - } - - if (!$latest_version) { - throw new DownloaderException('Could not determine latest Zig version'); - } - - logger()->info("Installing Zig version {$latest_version}"); - - $platform_key = "{$zig_arch}-{$zig_os}"; - if (!isset($index_json[$latest_version][$platform_key])) { - throw new DownloaderException("No download available for {$platform_key} in Zig version {$latest_version}"); - } - - $download_info = $index_json[$latest_version][$platform_key]; - $url = $download_info['tarball']; - $filename = basename($url); - - $pkg = [ - 'type' => 'url', - 'url' => $url, - 'filename' => $filename, - ]; - - Downloader::downloadPackage($name, $pkg, $force); - } - - public function extract(string $name): void - { - $pkgroot = PKG_ROOT_PATH; - $zig_bin_dir = "{$pkgroot}/zig"; - - $files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy']; - $all_exist = true; - foreach ($files as $file) { - if (!file_exists("{$zig_bin_dir}/{$file}")) { - $all_exist = false; - break; - } - } - if ($all_exist) { - return; - } - - $lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true); - $source_type = $lock[$name]['source_type']; - $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); - $extract = "{$pkgroot}/zig"; - - FileSystem::extractPackage($name, $source_type, $filename, $extract); - - $this->createZigCcScript($zig_bin_dir); - } - - public static function getEnvironment(): array - { - return []; - } - - public static function getPath(): ?string - { - return PKG_ROOT_PATH . '/zig'; - } - - private function createZigCcScript(string $bin_dir): void - { - $script_path = __DIR__ . '/../scripts/zig-cc.sh'; - $script_content = file_get_contents($script_path); - - file_put_contents("{$bin_dir}/zig-cc", $script_content); - chmod("{$bin_dir}/zig-cc", 0755); - - $script_content = str_replace('zig cc', 'zig c++', $script_content); - file_put_contents("{$bin_dir}/zig-c++", $script_content); - file_put_contents("{$bin_dir}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@"); - file_put_contents("{$bin_dir}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@"); - file_put_contents("{$bin_dir}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@"); - file_put_contents("{$bin_dir}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@"); - chmod("{$bin_dir}/zig-c++", 0755); - chmod("{$bin_dir}/zig-ar", 0755); - chmod("{$bin_dir}/zig-ld.lld", 0755); - chmod("{$bin_dir}/zig-ranlib", 0755); - chmod("{$bin_dir}/zig-objcopy", 0755); - } -} diff --git a/src/SPC/store/scripts/zig-cc.sh b/src/SPC/store/scripts/zig-cc.sh deleted file mode 100644 index 6b340bc1d..000000000 --- a/src/SPC/store/scripts/zig-cc.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" -BUILDROOT_ABS="$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)" -PARSED_ARGS=() - -while [[ $# -gt 0 ]]; do - case "$1" in - -isystem) - shift - ARG="$1" - shift - ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" - [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG") - ;; - -isystem*) - ARG="${1#-isystem}" - shift - ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" - [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") - ;; - -march=*|-mcpu=*) - OPT_NAME="${1%%=*}" - OPT_VALUE="${1#*=}" - # Skip armv8- flags entirely as Zig doesn't support them - if [[ "$OPT_VALUE" == armv8-* ]]; then - shift - continue - fi - # replace -march=x86-64 with -march=x86_64 - OPT_VALUE="${OPT_VALUE//-/_}" - PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}") - shift - ;; - *) - PARSED_ARGS+=("$1") - shift - ;; - esac -done - -[[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET="" - -if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then - output=$(zig cc $TARGET $SPC_COMPILER_EXTRA "${PARSED_ARGS[@]}" 2>&1) - status=$? - - if [[ $status -eq 0 ]]; then - echo "$output" - exit 0 - fi - - if echo "$output" | grep -qE "version '.*' in target triple"; then - filtered_output=$(echo "$output" | grep -vE "version '.*' in target triple") - echo "$filtered_output" - exit 0 - fi -fi - -exec zig cc $TARGET $SPC_COMPILER_EXTRA "${PARSED_ARGS[@]}" diff --git a/src/SPC/store/source/CustomSourceBase.php b/src/SPC/store/source/CustomSourceBase.php deleted file mode 100644 index 2f02fa7d3..000000000 --- a/src/SPC/store/source/CustomSourceBase.php +++ /dev/null @@ -1,28 +0,0 @@ - 'git', 'url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $force); - } else { - Downloader::downloadSource('php-src', $this->getLatestPHPInfo($major), $force); - } - } - - /** - * 获取 PHP x.y 的具体版本号,例如通过 8.1 来获取 8.1.10 - */ - #[ArrayShape(['type' => 'string', 'path' => 'string', 'rev' => 'string', 'url' => 'string'])] - public function getLatestPHPInfo(string $major_version): array - { - foreach (self::WEB_PHP_DOMAINS as $domain) { - try { - $info = json_decode(Downloader::curlExec( - url: "{$domain}/releases/index.php?json&version={$major_version}", - retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 - ), true); - if (!isset($info['version'])) { - throw new DownloaderException("Version {$major_version} not found."); - } - $version = $info['version']; - return [ - 'type' => 'url', - 'url' => "{$domain}/distributions/php-{$version}.tar.xz", - ]; - } catch (SPCException) { - logger()->warning('Failed to fetch latest PHP version for major version {$major_version} from {$domain}, trying next mirror if available.'); - continue; - } - } - // exception if all mirrors failed - throw new DownloaderException("Failed to fetch latest PHP version for major version {$major_version} from all tried mirrors."); - } -} diff --git a/src/SPC/store/source/PostgreSQLSource.php b/src/SPC/store/source/PostgreSQLSource.php deleted file mode 100644 index ec1d3357e..000000000 --- a/src/SPC/store/source/PostgreSQLSource.php +++ /dev/null @@ -1,29 +0,0 @@ - 'https://www.postgresql.org/ftp/source/', - 'regex' => '/href="(?v(?[^"]+)\/)"/', - ]); - return [ - 'type' => 'url', - 'url' => "https://ftp.postgresql.org/pub/source/{$filename}postgresql-{$version}.tar.gz", - ]; - } -} diff --git a/src/SPC/toolchain/ClangNativeToolchain.php b/src/SPC/toolchain/ClangNativeToolchain.php deleted file mode 100644 index a57b2e8ba..000000000 --- a/src/SPC/toolchain/ClangNativeToolchain.php +++ /dev/null @@ -1,53 +0,0 @@ - LinuxSystemUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), - 'Darwin' => MacOSSystemUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), - 'BSD' => FreeBSDSystemUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), - default => throw new EnvironmentException(__CLASS__ . ' is not supported on ' . PHP_OS_FAMILY . '.'), - }; - } - } - - public function getCompilerInfo(): ?string - { - $compiler = getenv('CC') ?: 'clang'; - $version = shell(false)->execWithResult("{$compiler} --version", false); - $head = pathinfo($compiler, PATHINFO_BASENAME); - if ($version[0] === 0 && preg_match('/clang version (\d+\.\d+\.\d+)/', $version[1][0], $match)) { - return "{$head} {$match[1]}"; - } - return $head; - } -} diff --git a/src/SPC/toolchain/GccNativeToolchain.php b/src/SPC/toolchain/GccNativeToolchain.php deleted file mode 100644 index d8eb8d1aa..000000000 --- a/src/SPC/toolchain/GccNativeToolchain.php +++ /dev/null @@ -1,50 +0,0 @@ - LinuxSystemUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), - 'Darwin' => MacOSSystemUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), - 'BSD' => FreeBSDSystemUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), - default => throw new EnvironmentException(__CLASS__ . ' is not supported on ' . PHP_OS_FAMILY . '.'), - }; - } - } - - public function getCompilerInfo(): ?string - { - $compiler = getenv('CC') ?: 'gcc'; - $version = shell(false)->execWithResult("{$compiler} --version", false); - $head = pathinfo($compiler, PATHINFO_BASENAME); - if ($version[0] === 0 && preg_match('/gcc.*?(\d+\.\d+\.\d+)/', $version[1][0], $match)) { - return "{$head} {$match[1]}"; - } - return $head; - } -} diff --git a/src/SPC/toolchain/MSVCToolchain.php b/src/SPC/toolchain/MSVCToolchain.php deleted file mode 100644 index 55559e8a1..000000000 --- a/src/SPC/toolchain/MSVCToolchain.php +++ /dev/null @@ -1,17 +0,0 @@ -execWithResult("{$compiler} --version", false); - $head = pathinfo($compiler, PATHINFO_BASENAME); - if ($version[0] === 0 && preg_match('/linux-musl-cc.*(\d+.\d+.\d+)/', $version[1][0], $match)) { - return "{$head} {$match[1]}"; - } - return $head; - } -} diff --git a/src/SPC/toolchain/ToolchainInterface.php b/src/SPC/toolchain/ToolchainInterface.php deleted file mode 100644 index a08fb8691..000000000 --- a/src/SPC/toolchain/ToolchainInterface.php +++ /dev/null @@ -1,38 +0,0 @@ - ZigToolchain::class, - 'Windows' => MSVCToolchain::class, - 'Darwin' => ClangNativeToolchain::class, - 'BSD' => ClangNativeToolchain::class, - ]; - - public static function getToolchainClass(): string - { - if ($tc = getenv('SPC_TOOLCHAIN')) { - return $tc; - } - $libc = getenv('SPC_LIBC'); - if ($libc && !getenv('SPC_TARGET')) { - // trigger_error('Setting SPC_LIBC is deprecated, please use SPC_TARGET instead.', E_USER_DEPRECATED); - return match ($libc) { - 'musl' => SystemUtil::isMuslDist() ? GccNativeToolchain::class : MuslToolchain::class, - 'glibc' => !SystemUtil::isMuslDist() ? GccNativeToolchain::class : throw new WrongUsageException('SPC_LIBC must be musl for musl dist.'), - default => throw new WrongUsageException('Unsupported SPC_LIBC value: ' . $libc), - }; - } - - return self::OS_DEFAULT_TOOLCHAIN[PHP_OS_FAMILY]; - } - - public static function initToolchain(): void - { - $toolchainClass = self::getToolchainClass(); - /* @var ToolchainInterface $toolchainClass */ - (new $toolchainClass())->initEnv(); - GlobalEnvManager::putenv("SPC_TOOLCHAIN={$toolchainClass}"); - } - - public static function afterInitToolchain(): void - { - if (!getenv('SPC_TOOLCHAIN')) { - throw new WrongUsageException('SPC_TOOLCHAIN was not properly set. Please contact the developers.'); - } - $musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m')); - if (SPCTarget::getLibc() === 'musl' && !SPCTarget::isStatic() && !file_exists($musl_wrapper_lib)) { - throw new WrongUsageException('You are linking against musl libc dynamically, but musl libc is not installed. Please use `bin/spc doctor` to install it.'); - } - if (SPCTarget::getLibc() === 'glibc' && SystemUtil::isMuslDist()) { - throw new WrongUsageException('You are linking against glibc dynamically, which is only supported on glibc distros.'); - } - - // init pkg-config for unix - if (is_unix()) { - if (($found = PkgConfigUtil::findPkgConfig()) === null) { - throw new WrongUsageException('Cannot find pkg-config executable. Please run `doctor` to fix this.'); - } - GlobalEnvManager::putenv("PKG_CONFIG={$found}"); - } - - $toolchain = getenv('SPC_TOOLCHAIN'); - /* @var ToolchainInterface $toolchain */ - $instance = new $toolchain(); - $instance->afterInit(); - if (getenv('PHP_BUILD_COMPILER') === false && ($compiler_info = $instance->getCompilerInfo())) { - GlobalEnvManager::putenv("PHP_BUILD_COMPILER={$compiler_info}"); - } - } -} diff --git a/src/SPC/toolchain/ZigToolchain.php b/src/SPC/toolchain/ZigToolchain.php deleted file mode 100644 index 1b7cc70dc..000000000 --- a/src/SPC/toolchain/ZigToolchain.php +++ /dev/null @@ -1,80 +0,0 @@ -/dev/null | grep -v '/32/' | head -n 1"); - $line = trim((string) $output); - if ($line !== '') { - $located = $line; - break; - } - } - if ($located) { - $found[] = $located; - } - } - GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found)); - } - - public function afterInit(): void - { - if (!Zig::isInstalled()) { - throw new EnvironmentException('You are building with zig, but zig is not installed, please install zig first. (You can use `doctor` command to install it)'); - } - GlobalEnvManager::addPathIfNotExists(Zig::getPath()); - f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough - $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: ''; - $cxxflags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: ''; - $extraCflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; - $cflags = trim($cflags . ' -Wno-date-time'); - $cxxflags = trim($cxxflags . ' -Wno-date-time'); - $extraCflags = trim($extraCflags . ' -Wno-date-time'); - GlobalEnvManager::putenv("SPC_DEFAULT_C_FLAGS={$cflags}"); - GlobalEnvManager::putenv("SPC_DEFAULT_CXX_FLAGS={$cxxflags}"); - GlobalEnvManager::putenv("SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS={$extraCflags}"); - GlobalEnvManager::putenv('RANLIB=zig-ranlib'); - GlobalEnvManager::putenv('OBJCOPY=zig-objcopy'); - $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; - if (!str_contains($extra_libs, '-lunwind')) { - // Add unwind library if not already present - $extra_libs = trim($extra_libs . ' -lunwind'); - GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); - } - $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; - $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); - if (!$has_avx512) { - $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; - GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no {$extra_vars}"); - } - } - - public function getCompilerInfo(): ?string - { - $version = shell(false)->execWithResult('zig version', false)[1][0] ?? ''; - return trim("zig {$version}"); - } -} diff --git a/src/SPC/util/AttributeMapper.php b/src/SPC/util/AttributeMapper.php deleted file mode 100644 index 4eafc6dd5..000000000 --- a/src/SPC/util/AttributeMapper.php +++ /dev/null @@ -1,133 +0,0 @@ - $extensions The mapping of extension names to their classes */ - private static array $ext_attr_map = []; - - /** @var array> $doctor_map The mapping of doctor modules */ - private static array $doctor_map = [ - 'check' => [], - 'fix' => [], - ]; - - public static function init(): void - { - // Load CustomExt attributes from extension classes - self::loadExtensionAttributes(); - - // Load doctor check items - self::loadDoctorAttributes(); - - // TODO: 3.0, refactor library loader and vendor loader here - } - - /** - * Get the class name of an extension by its attributed name. - * - * @param string $name The name of the extension (attributed name) - * @return null|string Returns the class name of the extension if it exists, otherwise null - */ - public static function getExtensionClassByName(string $name): ?string - { - return self::$ext_attr_map[$name] ?? null; - } - - /** - * @internal - */ - public static function getDoctorCheckMap(): array - { - return self::$doctor_map['check']; - } - - /** - * @internal - */ - public static function getDoctorFixMap(): array - { - return self::$doctor_map['fix']; - } - - private static function loadExtensionAttributes(): void - { - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\builder\extension'); - foreach ($classes as $class) { - $reflection = new \ReflectionClass($class); - foreach ($reflection->getAttributes(CustomExt::class) as $attribute) { - /** @var CustomExt $instance */ - $instance = $attribute->newInstance(); - self::$ext_attr_map[$instance->ext_name] = $class; - } - } - } - - private static function loadDoctorAttributes(): void - { - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/doctor/item', 'SPC\doctor\item'); - foreach ($classes as $class) { - $optional_passthrough = null; - $ref = new \ReflectionClass($class); - // #[OptionalCheck] - $optional = $ref->getAttributes(OptionalCheck::class)[0] ?? null; - if ($optional !== null) { - /** @var OptionalCheck $instance */ - $instance = $optional->newInstance(); - if (is_callable($instance->check)) { - $optional_passthrough = $instance->check; - } - } - $check_items = []; - $fix_items = []; - // load check items and fix items - foreach ($ref->getMethods() as $method) { - $optional_passthrough_single = $optional_passthrough ?? null; - // #[OptionalCheck] - foreach ($method->getAttributes(OptionalCheck::class) as $method_attr) { - $optional_check = $method_attr->newInstance(); - if (is_callable($optional_check->check)) { - $optional_passthrough_single = $optional_check->check; - } - } - // #[AsCheckItem] - foreach ($method->getAttributes(AsCheckItem::class) as $method_attr) { - // [{AsCheckItem object}, {OptionalCheck callable or null}] - $obj = $method_attr->newInstance(); - $obj->callback = [new $class(), $method->getName()]; - $check_items[] = [$obj, $optional_passthrough_single]; - } - // #[AsFixItem] - $fix_item = $method->getAttributes(AsFixItem::class)[0] ?? null; - if ($fix_item !== null) { - // [{AsFixItem object}, {OptionalCheck callable or null}] - $obj = $fix_item->newInstance(); - $fix_items[$obj->name] = [new $class(), $method->getName()]; - } - } - - // add to doctor map - self::$doctor_map['check'] = array_merge(self::$doctor_map['check'], $check_items); - self::$doctor_map['fix'] = array_merge(self::$doctor_map['fix'], $fix_items); - } - - // sort check items by level - usort(self::$doctor_map['check'], fn (array $a, array $b) => $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1)); - } -} diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php deleted file mode 100644 index d9f751ef9..000000000 --- a/src/SPC/util/ConfigValidator.php +++ /dev/null @@ -1,688 +0,0 @@ - 'string', // url - 'regex' => 'string', // regex pattern - 'rev' => 'string', // revision/branch - 'repo' => 'string', // repository name - 'match' => 'string', // match pattern (aaa*bbb) - 'filename' => 'string', // filename - 'path' => 'string', // copy path - 'extract' => 'string', // copy path (alias of path) - 'dirname' => 'string', // directory name for local source - 'source' => 'string', // the source name that this item uses - 'match-pattern-linux' => 'string', // pre-built match pattern for linux - 'match-pattern-macos' => 'string', // pre-built match pattern for macos - 'match-pattern-windows' => 'string', // pre-built match pattern for windows - - // Boolean fields - 'prefer-stable' => 'bool', // prefer stable releases - 'provide-pre-built' => 'bool', // provide pre-built binaries - 'notes' => 'bool', // whether to show notes in docs - 'cpp-library' => 'bool', // whether this is a C++ library - 'cpp-extension' => 'bool', // whether this is a C++ extension - 'build-with-php' => 'bool', // whether if this extension can be built to shared with PHP source together - 'zend-extension' => 'bool', // whether this is a zend extension - 'unix-only' => 'bool', // whether this extension is only for unix-like systems - - // Array fields - 'submodules' => 'array', // git submodules list (for git source type) - 'lib-depends' => 'list', - 'lib-suggests' => 'list', - 'ext-depends' => 'list', - 'ext-suggests' => 'list', - 'static-libs' => 'list', - 'pkg-configs' => 'list', // required pkg-config files without suffix (e.g. [libwebp]) - 'headers' => 'list', // required header files - 'bin' => 'list', // required binary files - 'frameworks' => 'list', // shared library frameworks (macOS) - - // Object/assoc array fields - 'support' => 'object', // extension OS support docs - 'extract-files' => 'object', // pkg.json extract files mapping with match pattern - 'alt' => 'object|bool', // alternative source/package - 'license' => 'object|array', // license information - 'target' => 'array', // extension build targets (default: [static], alternate: [shared] or both) - - // Special/mixed fields - 'func' => 'callable', // custom download function for custom source/package type - 'type' => 'string', // type field (validated separately) - ]; - - /** - * Source/Package download type validation rules - * Maps type names to [required_props, optional_props] - */ - private const array SOURCE_TYPE_FIELDS = [ - 'filelist' => [['url', 'regex'], []], - 'git' => [['url', 'rev'], ['path', 'extract', 'submodules']], - 'ghtagtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], - 'ghtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], - 'ghrel' => [['repo', 'match'], ['path', 'extract', 'prefer-stable']], - 'url' => [['url'], ['filename', 'path', 'extract']], - 'bitbuckettag' => [['repo'], ['path', 'extract']], - 'local' => [['dirname'], ['path', 'extract']], - 'pie' => [['repo'], ['path']], - 'custom' => [[], ['func']], - ]; - - /** - * Source.json specific fields [field_name => required] - * Note: 'type' is validated separately in validateSourceTypeConfig - * Field types are defined in FIELD_TYPES constant - */ - private const array SOURCE_FIELDS = [ - 'type' => true, // source type (must be SOURCE_TYPE_FIELDS key) - 'provide-pre-built' => false, // whether to provide pre-built binaries - 'alt' => false, // alternative source configuration - 'license' => false, // license information for source - // ... other fields are validated based on source type - ]; - - /** - * Lib.json specific fields [field_name => required] - * Field types are defined in FIELD_TYPES constant - */ - private const array LIB_FIELDS = [ - 'type' => false, // lib type (lib/package/target/root) - 'source' => false, // the source name that this lib uses - 'lib-depends' => false, // required libraries - 'lib-suggests' => false, // suggested libraries - 'static-libs' => false, // Generated static libraries - 'pkg-configs' => false, // Generated pkg-config files - 'cpp-library' => false, // whether this is a C++ library - 'headers' => false, // Generated header files - 'bin' => false, // Generated binary files - 'frameworks' => false, // Used shared library frameworks (macOS) - ]; - - /** - * Ext.json specific fields [field_name => required] - * Field types are defined in FIELD_TYPES constant - */ - private const array EXT_FIELDS = [ - 'type' => true, // extension type (builtin/external/addon/wip) - 'source' => false, // the source name that this extension uses - 'support' => false, // extension OS support docs - 'notes' => false, // whether to show notes in docs - 'cpp-extension' => false, // whether this is a C++ extension - 'build-with-php' => false, // whether if this extension can be built to shared with PHP source together - 'target' => false, // extension build targets (default: [static], alternate: [shared] or both) - 'lib-depends' => false, - 'lib-suggests' => false, - 'ext-depends' => false, - 'ext-suggests' => false, - 'frameworks' => false, - 'zend-extension' => false, // whether this is a zend extension - 'unix-only' => false, // whether this extension is only for unix-like systems - ]; - - /** - * Pkg.json specific fields [field_name => required] - * Field types are defined in FIELD_TYPES constant - */ - private const array PKG_FIELDS = [ - 'type' => true, // package type (same as source type) - 'extract-files' => false, // files to extract mapping (source pattern => target path) - ]; - - /** - * Pre-built.json specific fields [field_name => required] - * Field types are defined in FIELD_TYPES constant - */ - private const array PRE_BUILT_FIELDS = [ - 'repo' => true, // repository name for pre-built binaries - 'prefer-stable' => false, // prefer stable releases - 'match-pattern-linux' => false, // pre-built match pattern for linux - 'match-pattern-macos' => false, // pre-built match pattern for macos - 'match-pattern-windows' => false, // pre-built match pattern for windows - ]; - - /** - * Validate source.json - * - * @param array $data source.json data array - */ - public static function validateSource(array $data): void - { - foreach ($data as $name => $src) { - // Validate basic source type configuration - self::validateSourceTypeConfig($src, $name, 'source'); - - // Validate all source-specific fields using unified method - self::validateConfigFields($src, $name, 'source', self::SOURCE_FIELDS); - - // Check for unknown fields - self::validateAllowedFields($src, $name, 'source', self::SOURCE_FIELDS); - - // check if alt is valid - if (isset($src['alt']) && is_assoc_array($src['alt'])) { - // validate alt source recursively - self::validateSource([$name . '_alt' => $src['alt']]); - } - - // check if license is valid - if (isset($src['license'])) { - if (is_assoc_array($src['license'])) { - self::checkSingleLicense($src['license'], $name); - } elseif (is_list_array($src['license'])) { - foreach ($src['license'] as $license) { - if (!is_assoc_array($license)) { - throw new ValidationException("source {$name} license must be an object or array"); - } - self::checkSingleLicense($license, $name); - } - } - } - } - } - - public static function validateLibs(mixed $data, array $source_data = []): void - { - // check if it is an array - if (!is_array($data)) { - throw new ValidationException('lib.json is broken'); - } - - foreach ($data as $name => $lib) { - if (!is_assoc_array($lib)) { - throw new ValidationException("lib {$name} is not an object"); - } - // check if lib has valid type - if (!in_array($lib['type'] ?? 'lib', ['lib', 'package', 'target', 'root'])) { - throw new ValidationException("lib {$name} type is invalid"); - } - // check if lib and package has source - if (in_array($lib['type'] ?? 'lib', ['lib', 'package']) && !isset($lib['source'])) { - throw new ValidationException("lib {$name} does not assign any source"); - } - // check if source is valid - if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) { - throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}"); - } - - // Validate basic fields using unified method - self::validateConfigFields($lib, $name, 'lib', self::LIB_FIELDS); - - // Validate list array fields with suffixes - $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - $fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; - self::validateListArrayFields($lib, $name, 'lib', $fields, $suffixes); - - // Validate frameworks (special case without suffix) - if (isset($lib['frameworks'])) { - self::validateFieldType('frameworks', $lib['frameworks'], $name, 'lib'); - } - - // Check for unknown fields - self::validateAllowedFields($lib, $name, 'lib', self::LIB_FIELDS); - } - } - - public static function validateExts(mixed $data): void - { - if (!is_array($data)) { - throw new ValidationException('ext.json is broken'); - } - - foreach ($data as $name => $ext) { - if (!is_assoc_array($ext)) { - throw new ValidationException("ext {$name} is not an object"); - } - - if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) { - throw new ValidationException("ext {$name} type is invalid"); - } - - // Check source field requirement - if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) { - throw new ValidationException("ext {$name} does not assign any source"); - } - - // Validate basic fields using unified method - self::validateConfigFields($ext, $name, 'ext', self::EXT_FIELDS); - - // Validate list array fields with suffixes - $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - $fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; - self::validateListArrayFields($ext, $name, 'ext', $fields, $suffixes); - - // Validate arg-type fields - self::validateArgTypeFields($ext, $name, $suffixes); - - // Check for unknown fields - self::validateAllowedFields($ext, $name, 'ext', self::EXT_FIELDS); - } - } - - public static function validatePkgs(mixed $data): void - { - if (!is_array($data)) { - throw new ValidationException('pkg.json is broken'); - } - // check each package - foreach ($data as $name => $pkg) { - // check if pkg is an assoc array - if (!is_assoc_array($pkg)) { - throw new ValidationException("pkg {$name} is not an object"); - } - - // Validate basic source type configuration (reuse from source validation) - self::validateSourceTypeConfig($pkg, $name, 'pkg'); - - // Validate all pkg-specific fields using unified method - self::validateConfigFields($pkg, $name, 'pkg', self::PKG_FIELDS); - - // Validate extract-files content (object validation is done by validateFieldType) - if (isset($pkg['extract-files'])) { - // check each extract file mapping - foreach ($pkg['extract-files'] as $source => $target) { - if (!is_string($source) || !is_string($target)) { - throw new ValidationException("pkg {$name} extract-files mapping must be string to string"); - } - } - } - - // Check for unknown fields - self::validateAllowedFields($pkg, $name, 'pkg', self::PKG_FIELDS); - } - } - - /** - * Validate pre-built.json configuration - * - * @param mixed $data pre-built.json loaded data - */ - public static function validatePreBuilt(mixed $data): void - { - if (!is_array($data)) { - throw new ValidationException('pre-built.json is broken'); - } - - // Validate all fields using unified method - self::validateConfigFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); - - // Check for unknown fields - self::validateAllowedFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); - - // Check match pattern fields (at least one must exist) - $pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows']; - $has_pattern = false; - - foreach ($pattern_fields as $field) { - if (isset($data[$field])) { - $has_pattern = true; - // Validate pattern contains required placeholders - if (!str_contains($data[$field], '{name}')) { - throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder"); - } - if (!str_contains($data[$field], '{arch}')) { - throw new ValidationException("pre-built.json [{$field}] must contain {arch} placeholder"); - } - if (!str_contains($data[$field], '{os}')) { - throw new ValidationException("pre-built.json [{$field}] must contain {os} placeholder"); - } - - // Linux pattern should have libc-related placeholders - if ($field === 'match-pattern-linux') { - if (!str_contains($data[$field], '{libc}')) { - throw new ValidationException('pre-built.json [match-pattern-linux] must contain {libc} placeholder'); - } - if (!str_contains($data[$field], '{libcver}')) { - throw new ValidationException('pre-built.json [match-pattern-linux] must contain {libcver} placeholder'); - } - } - } - } - - if (!$has_pattern) { - throw new ValidationException('pre-built.json must have at least one match-pattern field'); - } - } - - /** - * @param mixed $craft_file craft.yml path - * @param Command $command craft command instance - * @return array{ - * php-version?: string, - * extensions: array, - * shared-extensions?: array, - * libs?: array, - * sapi: array, - * debug?: bool, - * clean-build?: bool, - * build-options?: array, - * download-options?: array, - * extra-env?: array, - * craft-options?: array{ - * doctor?: bool, - * download?: bool, - * build?: bool - * } - * } - */ - public static function validateAndParseCraftFile(mixed $craft_file, Command $command): array - { - $build_options = $command->getApplication()->find('build')->getDefinition()->getOptions(); - $download_options = $command->getApplication()->find('download')->getDefinition()->getOptions(); - - try { - $craft = Yaml::parse(file_get_contents($craft_file)); - } catch (ParseException $e) { - throw new ValidationException('Craft file is broken: ' . $e->getMessage()); - } - if (!is_assoc_array($craft)) { - throw new ValidationException('Craft file is broken'); - } - // check php-version - if (isset($craft['php-version'])) { - // validate version, accept 8.x, 7.x, 8.x.x, 7.x.x, 8, 7 - $version = strval($craft['php-version']); - if (!preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', $version, $matches)) { - throw new ValidationException('Craft file php-version is invalid'); - } - } - // check extensions - if (!isset($craft['extensions'])) { - throw new ValidationException('Craft file must have extensions'); - } - if (is_string($craft['extensions'])) { - $craft['extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['extensions']))); - } - if (!isset($craft['shared-extensions'])) { - $craft['shared-extensions'] = []; - } - if (is_string($craft['shared-extensions'] ?? [])) { - $craft['shared-extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['shared-extensions']))); - } - // check libs - if (isset($craft['libs']) && is_string($craft['libs'])) { - $craft['libs'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['libs']))); - } elseif (!isset($craft['libs'])) { - $craft['libs'] = []; - } - // check sapi - if (!isset($craft['sapi'])) { - throw new ValidationException('Craft file must have sapi'); - } - if (is_string($craft['sapi'])) { - $craft['sapi'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['sapi']))); - } - // debug as boolean - if (isset($craft['debug'])) { - $craft['debug'] = filter_var($craft['debug'], FILTER_VALIDATE_BOOLEAN); - } else { - $craft['debug'] = false; - } - // check clean-build - $craft['clean-build'] ??= false; - // check build-options - if (isset($craft['build-options'])) { - if (!is_assoc_array($craft['build-options'])) { - throw new ValidationException('Craft file build-options must be an object'); - } - foreach ($craft['build-options'] as $key => $value) { - if (!isset($build_options[$key])) { - throw new ValidationException("Craft file build-options {$key} is invalid"); - } - // check an array - if ($build_options[$key]->isArray() && !is_array($value)) { - throw new ValidationException("Craft file build-options {$key} must be an array"); - } - } - } else { - $craft['build-options'] = []; - } - // check download options - if (isset($craft['download-options'])) { - if (!is_assoc_array($craft['download-options'])) { - throw new ValidationException('Craft file download-options must be an object'); - } - foreach ($craft['download-options'] as $key => $value) { - if (!isset($download_options[$key])) { - throw new ValidationException("Craft file download-options {$key} is invalid"); - } - // check an array - if ($download_options[$key]->isArray() && !is_array($value)) { - throw new ValidationException("Craft file download-options {$key} must be an array"); - } - } - } else { - $craft['download-options'] = []; - } - // check extra-env - if (isset($craft['extra-env'])) { - if (!is_assoc_array($craft['extra-env'])) { - throw new ValidationException('Craft file extra-env must be an object'); - } - } else { - $craft['extra-env'] = []; - } - // check craft-options - $craft['craft-options']['doctor'] ??= true; - $craft['craft-options']['download'] ??= true; - $craft['craft-options']['build'] ??= true; - return $craft; - } - - /** - * Validate a field based on its global type definition - * - * @param string $field Field name - * @param mixed $value Field value - * @param string $name Item name (for error messages) - * @param string $type Item type (for error messages) - * @return bool Returns true if validation passes - */ - private static function validateFieldType(string $field, mixed $value, string $name, string $type): bool - { - // Check if field exists in FIELD_TYPES - if (!isset(self::FIELD_TYPES[$field])) { - // Try to strip suffix and check base field name - $suffixes = ['-windows', '-unix', '-macos', '-linux']; - $base_field = $field; - foreach ($suffixes as $suffix) { - if (str_ends_with($field, $suffix)) { - $base_field = substr($field, 0, -strlen($suffix)); - break; - } - } - - if (!isset(self::FIELD_TYPES[$base_field])) { - // Unknown field is not allowed - strict validation - throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); - } - - // Use base field type for validation - $expected_type = self::FIELD_TYPES[$base_field]; - } else { - $expected_type = self::FIELD_TYPES[$field]; - } - - return match ($expected_type) { - 'string' => is_string($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be string"), - 'bool' => is_bool($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be boolean"), - 'array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be array"), - 'list' => is_list_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be a list"), - 'object' => is_assoc_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object"), - 'object|bool' => (is_assoc_array($value) || is_bool($value)) ?: throw new ValidationException("{$type} {$name} [{$field}] must be object or boolean"), - 'object|array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object or array"), - 'callable' => true, // Skip validation for callable - }; - } - - private static function checkSingleLicense(array $license, string $name): void - { - if (!is_assoc_array($license)) { - throw new ValidationException("source {$name} license must be an object"); - } - if (!isset($license['type'])) { - throw new ValidationException("source {$name} license must have type"); - } - if (!in_array($license['type'], ['file', 'text'])) { - throw new ValidationException("source {$name} license type is invalid"); - } - if ($license['type'] === 'file' && !isset($license['path'])) { - throw new ValidationException("source {$name} license file must have path"); - } - if ($license['type'] === 'text' && !isset($license['text'])) { - throw new ValidationException("source {$name} license text must have text"); - } - } - - /** - * Validate source type configuration (shared between source.json and pkg.json) - * - * @param array $item The source/package item to validate - * @param string $name The name of the item for error messages - * @param string $config_type The type of config file ("source" or "pkg") - */ - private static function validateSourceTypeConfig(array $item, string $name, string $config_type): void - { - if (!isset($item['type'])) { - throw new ValidationException("{$config_type} {$name} must have prop: [type]"); - } - if (!is_string($item['type'])) { - throw new ValidationException("{$config_type} {$name} type prop must be string"); - } - - if (!isset(self::SOURCE_TYPE_FIELDS[$item['type']])) { - throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid"); - } - - [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; - - // Check required fields exist - foreach ($required as $prop) { - if (!isset($item[$prop])) { - $props = implode('] and [', $required); - throw new ValidationException("{$config_type} {$name} needs [{$props}] props"); - } - } - - // Validate field types using global field type definitions - foreach (array_merge($required, $optional) as $prop) { - if (isset($item[$prop])) { - self::validateFieldType($prop, $item[$prop], $name, $config_type); - } - } - } - - /** - * Validate that fields with suffixes are list arrays - */ - private static function validateListArrayFields(array $item, string $name, string $type, array $fields, array $suffixes): void - { - foreach ($fields as $field) { - foreach ($suffixes as $suffix) { - $key = $field . $suffix; - if (isset($item[$key])) { - self::validateFieldType($key, $item[$key], $name, $type); - } - } - } - } - - /** - * Validate arg-type fields with suffixes - */ - private static function validateArgTypeFields(array $item, string $name, array $suffixes): void - { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - - foreach (array_merge([''], $suffixes) as $suffix) { - $key = 'arg-type' . $suffix; - if (isset($item[$key]) && !in_array($item[$key], $valid_arg_types)) { - throw new ValidationException("ext {$name} {$key} is invalid"); - } - } - } - - /** - * Unified method to validate config fields based on field definitions - * - * @param array $item Item data to validate - * @param string $name Item name for error messages - * @param string $type Config type (source, lib, ext, pkg, pre-built) - * @param array $field_definitions Field definitions [field_name => required (bool)] - */ - private static function validateConfigFields(array $item, string $name, string $type, array $field_definitions): void - { - foreach ($field_definitions as $field => $required) { - if ($required && !isset($item[$field])) { - throw new ValidationException("{$type} {$name} must have [{$field}] field"); - } - - if (isset($item[$field])) { - self::validateFieldType($field, $item[$field], $name, $type); - } - } - } - - /** - * Validate that item only contains allowed fields - * This method checks for unknown fields based on the config type - * - * @param array $item Item data to validate - * @param string $name Item name for error messages - * @param string $type Config type (source, lib, ext, pkg, pre-built) - * @param array $field_definitions Field definitions [field_name => required (bool)] - */ - private static function validateAllowedFields(array $item, string $name, string $type, array $field_definitions): void - { - // For source and pkg types, we need to check SOURCE_TYPE_FIELDS as well - $allowed_fields = array_keys($field_definitions); - - // For source/pkg, add allowed fields from SOURCE_TYPE_FIELDS based on the type - if (in_array($type, ['source', 'pkg']) && isset($item['type'], self::SOURCE_TYPE_FIELDS[$item['type']])) { - [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; - $allowed_fields = array_merge($allowed_fields, $required, $optional); - } - - // For lib and ext types, add fields with suffixes - if (in_array($type, ['lib', 'ext'])) { - $suffixes = ['-windows', '-unix', '-macos', '-linux']; - $base_fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; - if ($type === 'ext') { - $base_fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; - // Add arg-type fields - foreach (array_merge([''], $suffixes) as $suffix) { - $allowed_fields[] = 'arg-type' . $suffix; - } - } - foreach ($base_fields as $field) { - foreach ($suffixes as $suffix) { - $allowed_fields[] = $field . $suffix; - } - } - // frameworks is lib-only - if ($type === 'lib') { - $allowed_fields[] = 'frameworks'; - } - } - - // Check each field in item - foreach (array_keys($item) as $field) { - if (!in_array($field, $allowed_fields)) { - throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); - } - } - } -} diff --git a/src/SPC/util/CustomExt.php b/src/SPC/util/CustomExt.php deleted file mode 100644 index 04cc645f8..000000000 --- a/src/SPC/util/CustomExt.php +++ /dev/null @@ -1,24 +0,0 @@ -, suggests: array}> - */ - public static function platExtToLibs(): array - { - $exts = Config::getExts(); - $libs = Config::getLibs(); - $dep_list = []; - foreach ($exts as $ext_name => $ext) { - // convert ext-depends value to ext@xxx - $ext_depends = Config::getExt($ext_name, 'ext-depends', []); - $ext_depends = array_map(fn ($x) => "ext@{$x}", $ext_depends); - // convert ext-suggests value to ext@xxx - $ext_suggests = Config::getExt($ext_name, 'ext-suggests', []); - $ext_suggests = array_map(fn ($x) => "ext@{$x}", $ext_suggests); - // merge ext-depends with lib-depends - $lib_depends = Config::getExt($ext_name, 'lib-depends', []); - $depends = array_merge($ext_depends, $lib_depends, ['php']); - // merge ext-suggests with lib-suggests - $lib_suggests = Config::getExt($ext_name, 'lib-suggests', []); - $suggests = array_merge($ext_suggests, $lib_suggests); - $dep_list["ext@{$ext_name}"] = [ - 'depends' => $depends, - 'suggests' => $suggests, - ]; - } - foreach ($libs as $lib_name => $lib) { - $dep_list[$lib_name] = [ - 'depends' => array_merge(Config::getLib($lib_name, 'lib-depends', []), ['lib-base']), - 'suggests' => Config::getLib($lib_name, 'lib-suggests', []), - ]; - } - // here is an array that only contains dependency map - return $dep_list; - } - - /** - * Get library dependencies in correct order - * - * @param array $libs Array of library names - * @return array Ordered array of library names - */ - public static function getLibs(array $libs, bool $include_suggested_libs = false): array - { - $dep_list = self::platExtToLibs(); - - if ($include_suggested_libs) { - foreach ($dep_list as $name => $obj) { - $del_list = []; - foreach ($obj['suggests'] as $id => $suggest) { - if (!str_starts_with($suggest, 'ext@')) { - $dep_list[$name]['depends'][] = $suggest; - $del_list[] = $id; - } - } - foreach ($del_list as $id) { - unset($dep_list[$name]['suggests'][$id]); - } - $dep_list[$name]['suggests'] = array_values($dep_list[$name]['suggests']); - } - } - - $final = self::doVisitPlat($libs, $dep_list); - - $libs_final = []; - foreach ($final as $item) { - if (!str_starts_with($item, 'ext@')) { - $libs_final[] = $item; - } - } - return $libs_final; - } - - /** - * Get extension dependencies in correct order - * - * @param array $exts Array of extension names - * @param array $additional_libs Array of additional libraries - * @return array Ordered array of extension names - */ - public static function getExtsAndLibs(array $exts, array $additional_libs = [], bool $include_suggested_exts = false, bool $include_suggested_libs = false): array - { - $dep_list = self::platExtToLibs(); - - // include suggested extensions - if ($include_suggested_exts) { - // check every deps suggests contains ext@ - foreach ($dep_list as $name => $obj) { - $del_list = []; - foreach ($obj['suggests'] as $id => $suggest) { - if (str_starts_with($suggest, 'ext@')) { - $dep_list[$name]['depends'][] = $suggest; - $del_list[] = $id; - } - } - foreach ($del_list as $id) { - unset($dep_list[$name]['suggests'][$id]); - } - $dep_list[$name]['suggests'] = array_values($dep_list[$name]['suggests']); - } - } - - // include suggested libraries - if ($include_suggested_libs) { - // check every deps suggests - foreach ($dep_list as $name => $obj) { - $del_list = []; - foreach ($obj['suggests'] as $id => $suggest) { - if (!str_starts_with($suggest, 'ext@')) { - $dep_list[$name]['depends'][] = $suggest; - $del_list[] = $id; - } - } - foreach ($del_list as $id) { - unset($dep_list[$name]['suggests'][$id]); - } - $dep_list[$name]['suggests'] = array_values($dep_list[$name]['suggests']); - } - } - - // convert ext_name to ext@ext_name - $origin_exts = $exts; - $exts = array_map(fn ($x) => "ext@{$x}", $exts); - $exts = array_merge($exts, $additional_libs); - - $final = self::doVisitPlat($exts, $dep_list); - - // revert array - $exts_final = []; - $libs_final = []; - $not_included_final = []; - foreach ($final as $item) { - if (str_starts_with($item, 'ext@')) { - $tmp = substr($item, 4); - if (!in_array($tmp, $origin_exts)) { - $not_included_final[] = $tmp; - } - $exts_final[] = $tmp; - } else { - $libs_final[] = $item; - } - } - return [$exts_final, $libs_final, $not_included_final]; - } - - private static function doVisitPlat(array $deps, array $dep_list): array - { - // default: get extension exts and libs sorted by dep_list - $sorted = []; - $visited = []; - foreach ($deps as $ext_name) { - if (!isset($dep_list[$ext_name])) { - $ext_name = str_starts_with($ext_name, 'ext@') ? ('Extension [' . substr($ext_name, 4) . ']') : ('Library [' . $ext_name . ']'); - throw new WrongUsageException("{$ext_name} not exist !"); - } - if (!isset($visited[$ext_name])) { - self::visitPlatDeps($ext_name, $dep_list, $visited, $sorted); - } - } - $sorted_suggests = []; - $visited_suggests = []; - $final = []; - foreach ($deps as $ext_name) { - if (!isset($visited_suggests[$ext_name])) { - self::visitPlatAllDeps($ext_name, $dep_list, $visited_suggests, $sorted_suggests); - } - } - foreach ($sorted_suggests as $suggest) { - if (in_array($suggest, $sorted)) { - $final[] = $suggest; - } - } - return $final; - } - - private static function visitPlatAllDeps(string $lib_name, array $dep_list, array &$visited, array &$sorted): void - { - // 如果已经识别到了,那就不管 - if (isset($visited[$lib_name])) { - return; - } - $visited[$lib_name] = true; - // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) - foreach (array_merge($dep_list[$lib_name]['depends'], $dep_list[$lib_name]['suggests']) as $dep) { - self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted); - } - $sorted[] = $lib_name; - } - - private static function visitPlatDeps(string $lib_name, array $dep_list, array &$visited, array &$sorted): void - { - // 如果已经识别到了,那就不管 - if (isset($visited[$lib_name])) { - return; - } - $visited[$lib_name] = true; - // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) - if (!isset($dep_list[$lib_name])) { - throw new WrongUsageException("{$lib_name} not exist !"); - } - foreach ($dep_list[$lib_name]['depends'] as $dep) { - self::visitPlatDeps($dep, $dep_list, $visited, $sorted); - } - $sorted[] = $lib_name; - } -} diff --git a/src/SPC/util/GlobalEnvManager.php b/src/SPC/util/GlobalEnvManager.php deleted file mode 100644 index 4ce3384dd..000000000 --- a/src/SPC/util/GlobalEnvManager.php +++ /dev/null @@ -1,177 +0,0 @@ - $v) { - if (getenv($k) === false) { - $default_put_list[$k] = $v; - self::putenv("{$k}={$v}"); - } - } - $os_ini = match (PHP_OS_FAMILY) { - 'Windows' => $ini['windows'] ?? [], - 'Darwin' => $ini['macos'] ?? [], - 'Linux' => $ini['linux'] ?? [], - 'BSD' => $ini['freebsd'] ?? [], - default => [], - }; - foreach ($os_ini as $k => $v) { - if (getenv($k) === false) { - $default_put_list[$k] = $v; - self::putenv("{$k}={$v}"); - } - } - - ToolchainManager::initToolchain(); - - // apply second time - $ini2 = self::readIniFile(); - - foreach ($ini2['global'] as $k => $v) { - if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) { - self::putenv("{$k}={$v}"); - } - } - $os_ini2 = match (PHP_OS_FAMILY) { - 'Windows' => $ini2['windows'] ?? [], - 'Darwin' => $ini2['macos'] ?? [], - 'Linux' => $ini2['linux'] ?? [], - 'BSD' => $ini2['freebsd'] ?? [], - default => [], - }; - foreach ($os_ini2 as $k => $v) { - if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) { - self::putenv("{$k}={$v}"); - } - } - self::$initialized = true; - } - - public static function putenv(string $val): void - { - f_putenv($val); - self::$env_cache[] = $val; - } - - public static function addPathIfNotExists(string $path): void - { - if (is_unix() && !str_contains(getenv('PATH'), $path)) { - self::putenv("PATH={$path}:" . getenv('PATH')); - } - } - - /** - * Initialize the toolchain after the environment variables are set. - * The toolchain or environment availability check is done here. - */ - public static function afterInit(): void - { - if (!filter_var(getenv('SPC_SKIP_TOOLCHAIN_CHECK'), FILTER_VALIDATE_BOOL)) { - ToolchainManager::afterInitToolchain(); - } - // test bison - if (PHP_OS_FAMILY === 'Darwin') { - if ($bison = SystemUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) { - self::putenv("BISON={$bison}"); - } - if ($yacc = SystemUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) { - self::putenv("YACC={$yacc}"); - } - } - } - - private static function readIniFile(): array - { - // Init env.ini file, read order: - // WORKING_DIR/config/env.ini - // ROOT_DIR/config/env.ini - $ini_files = [ - WORKING_DIR . '/config/env.ini', - ROOT_DIR . '/config/env.ini', - ]; - $ini_custom = [ - WORKING_DIR . '/config/env.custom.ini', - ROOT_DIR . '/config/env.custom.ini', - ]; - $ini = null; - foreach ($ini_files as $ini_file) { - if (file_exists($ini_file)) { - $ini = parse_ini_file($ini_file, true); - break; - } - } - if ($ini === null) { - throw new WrongUsageException('env.ini not found'); - } - if ($ini === false || !isset($ini['global'])) { - throw new WrongUsageException('Failed to parse ' . $ini_file); - } - // apply custom env - foreach ($ini_custom as $ini_file) { - if (file_exists($ini_file)) { - $ini_custom = parse_ini_file($ini_file, true); - if ($ini_custom !== false) { - $ini['global'] = array_merge($ini['global'], $ini_custom['global'] ?? []); - match (PHP_OS_FAMILY) { - 'Windows' => $ini['windows'] = array_merge($ini['windows'], $ini_custom['windows'] ?? []), - 'Darwin' => $ini['macos'] = array_merge($ini['macos'], $ini_custom['macos'] ?? []), - 'Linux' => $ini['linux'] = array_merge($ini['linux'], $ini_custom['linux'] ?? []), - 'BSD' => $ini['freebsd'] = array_merge($ini['freebsd'], $ini_custom['freebsd'] ?? []), - default => null, - }; - } - break; - } - } - return $ini; - } -} diff --git a/src/SPC/util/GlobalValueTrait.php b/src/SPC/util/GlobalValueTrait.php deleted file mode 100644 index d815b1f09..000000000 --- a/src/SPC/util/GlobalValueTrait.php +++ /dev/null @@ -1,28 +0,0 @@ -exts = array_merge($exts, $this->exts); - return $this; - } - - public function addLibs(array $libs): LicenseDumper - { - $this->libs = array_merge($libs, $this->libs); - return $this; - } - - public function addSources(array $sources): LicenseDumper - { - $this->sources = array_merge($sources, $this->sources); - return $this; - } - - /** - * Dump source licenses to target directory - * - * @param string $target_dir Target directory - * @return bool Success or not - */ - public function dump(string $target_dir): bool - { - // mkdir first - if (is_dir($target_dir) && !FileSystem::removeDir($target_dir)) { - logger()->warning('Target dump directory is not empty, be aware!'); - } - FileSystem::createDir($target_dir); - foreach ($this->exts as $ext) { - if (Config::getExt($ext, 'type') !== 'external') { - continue; - } - - $source_name = Config::getExt($ext, 'source'); - foreach ($this->getSourceLicenses($source_name) as $index => $license) { - $result = file_put_contents("{$target_dir}/ext_{$ext}_{$index}.txt", $license); - if ($result === false) { - return false; - } - } - } - - foreach ($this->libs as $lib) { - if (Config::getLib($lib, 'type', 'lib') !== 'lib') { - continue; - } - $source_name = Config::getLib($lib, 'source'); - foreach ($this->getSourceLicenses($source_name) as $index => $license) { - $result = file_put_contents("{$target_dir}/lib_{$lib}_{$index}.txt", $license); - if ($result === false) { - return false; - } - } - } - - foreach ($this->sources as $source) { - foreach ($this->getSourceLicenses($source) as $index => $license) { - $result = file_put_contents("{$target_dir}/src_{$source}_{$index}.txt", $license); - if ($result === false) { - return false; - } - } - } - return true; - } - - /** - * Returns an iterable of source licenses for a given source name. - * - * @param string $source_name Source name - * @return string[] String iterable of source licenses - */ - private function getSourceLicenses(string $source_name): iterable - { - $licenses = Config::getSource($source_name)['license'] ?? []; - if ($licenses === []) { - throw new SPCInternalException("source [{$source_name}] license meta not exist"); - } - - if (!array_is_list($licenses)) { - $licenses = [$licenses]; - } - - foreach ($licenses as $index => $license) { - yield $index => match ($license['type']) { - 'text' => $license['text'], - 'file' => $this->loadSourceFile($source_name, $index, $license['path'], Config::getSource($source_name)['path'] ?? null), - default => throw new SPCInternalException("source [{$source_name}] license type is not allowed"), - }; - } - } - - /** - * Loads a source license file from the specified path. - */ - private function loadSourceFile(string $source_name, int $index, array|string|null $in_path, ?string $custom_base_path = null): string - { - if (is_null($in_path)) { - throw new SPCInternalException("source [{$source_name}] license file is not set, please check config/source.json"); - } - - if (!is_array($in_path)) { - $in_path = [$in_path]; - } - - foreach ($in_path as $item) { - if (file_exists(SOURCE_PATH . '/' . ($custom_base_path ?? $source_name) . '/' . $item)) { - return file_get_contents(SOURCE_PATH . '/' . ($custom_base_path ?? $source_name) . '/' . $item); - } - } - - if (file_exists(BUILD_ROOT_PATH . '/source-licenses/' . $source_name . '/' . $index . '.txt')) { - return file_get_contents(BUILD_ROOT_PATH . '/source-licenses/' . $source_name . '/' . $index . '.txt'); - } - - throw new SPCInternalException("Cannot find any license file in source [{$source_name}] directory!"); - } -} diff --git a/src/SPC/util/PkgConfigUtil.php b/src/SPC/util/PkgConfigUtil.php deleted file mode 100644 index 25e0e1f2d..000000000 --- a/src/SPC/util/PkgConfigUtil.php +++ /dev/null @@ -1,124 +0,0 @@ -builder = $builder; // BuilderProvider::makeBuilderByInput($input ?? new ArgvInput()); - } - $this->no_php = $options['no_php'] ?? false; - $this->libs_only_deps = $options['libs_only_deps'] ?? false; - $this->absolute_libs = $options['absolute_libs'] ?? false; - } - - /** - * Generate configuration for building PHP extensions. - * - * @param array $extensions Extension name list - * @param array $libraries Additional library name list - * @param bool $include_suggest_ext Include suggested extensions - * @param bool $include_suggest_lib Include suggested libraries - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function config(array $extensions = [], array $libraries = [], bool $include_suggest_ext = false, bool $include_suggest_lib = false): array - { - logger()->debug('config extensions: ' . implode(',', $extensions)); - logger()->debug('config libs: ' . implode(',', $libraries)); - logger()->debug('config suggest for [ext, lib]: ' . ($include_suggest_ext ? 'true' : 'false') . ',' . ($include_suggest_lib ? 'true' : 'false')); - $extra_exts = []; - foreach ($extensions as $ext) { - $extra_exts = array_merge($extra_exts, Config::getExt($ext, 'ext-suggests', [])); - } - foreach ($extra_exts as $ext) { - if ($this->builder?->getExt($ext) && !in_array($ext, $extensions)) { - $extensions[] = $ext; - } - } - [$extensions, $libraries] = DependencyUtil::getExtsAndLibs($extensions, $libraries, $include_suggest_ext, $include_suggest_lib); - - ob_start(); - if ($this->builder === null) { - $this->builder = BuilderProvider::makeBuilderByInput(new ArgvInput()); - $this->builder->proveLibs($libraries); - $this->builder->proveExts($extensions, skip_extract: true); - } - ob_get_clean(); - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($libraries); - $libs = $this->getLibsString($libraries, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - if ($extra_libs = SPCTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } - $extra_env = getenv('SPC_EXTRA_LIBS'); - if (is_string($extra_env) && !empty($extra_env)) { - $libs .= " {$extra_env}"; - } - // extension frameworks - if (SPCTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($extensions)}"; - } - if ($this->hasCpp($extensions, $libraries)) { - $libcpp = SPCTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } - - if ($this->libs_only_deps) { - // mimalloc must come first - if ($this->builder->getLib('mimalloc') && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); - } - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), - ]; - } - - // embed - if (!$this->no_php) { - $libs = "-lphp {$libs} -lc"; - } - - $allLibs = getenv('LIBS') . ' ' . $libs; - - // mimalloc must come first - if ($this->builder->getLib('mimalloc') && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); - } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; - } - - /** - * [Helper function] - * Get configuration for a specific extension(s) dependencies. - * - * @param Extension|Extension[] $extension Extension instance or list - * @param bool $include_suggest_ext Whether to include suggested extensions - * @param bool $include_suggest_lib Whether to include suggested libraries - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array - { - if (!is_array($extension)) { - $extension = [$extension]; - } - $libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension))); - return $this->config( - extensions: array_map(fn ($x) => $x->getName(), $extension), - libraries: $libs, - include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false, - include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, - ); - } - - /** - * [Helper function] - * Get configuration for a specific library(s) dependencies. - * - * @param LibraryBase|LibraryBase[] $lib Library instance or list - * @param bool $include_suggest_lib Whether to include suggested libraries - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array - { - if (!is_array($lib)) { - $lib = [$lib]; - } - $save_no_php = $this->no_php; - $this->no_php = true; - $save_libs_only_deps = $this->libs_only_deps; - $this->libs_only_deps = true; - $ret = $this->config( - libraries: array_map(fn ($x) => $x->getName(), $lib), - include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, - ); - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - return $ret; - } - - private function hasCpp(array $extensions, array $libraries): bool - { - // judge cpp-extension - $builderExtNames = array_keys($this->builder->getExts(false)); - $exts = array_unique([...$builderExtNames, ...$extensions]); - - foreach ($exts as $ext) { - if (Config::getExt($ext, 'cpp-extension', false) === true) { - return true; - } - } - $builderLibNames = array_keys($this->builder->getLibs()); - $libs = array_unique([...$builderLibNames, ...$libraries]); - foreach ($libs as $lib) { - if (Config::getLib($lib, 'cpp-library', false) === true) { - return true; - } - } - return false; - } - - private function getIncludesString(array $libraries): string - { - $base = BUILD_INCLUDE_PATH; - $includes = ["-I{$base}"]; - - // link with libphp - if (!$this->no_php) { - $includes = [ - ...$includes, - "-I{$base}/php", - "-I{$base}/php/main", - "-I{$base}/php/TSRM", - "-I{$base}/php/Zend", - "-I{$base}/php/ext", - ]; - } - - // parse pkg-configs - foreach ($libraries as $library) { - $pc = Config::getLib($library, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)); - foreach ($pc as $file) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$file}.pc")) { - $found = true; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist. Please build it first."); - } - } - $pc_cflags = implode(' ', $pc); - if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { - $arr = explode(' ', $pc_cflags); - $arr = array_unique($arr); - $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); - $pc_cflags = implode(' ', $arr); - $includes[] = $pc_cflags; - } - } - $includes = array_unique($includes); - return implode(' ', $includes); - } - - private function getLdflagsString(): string - { - return '-L' . BUILD_LIB_PATH; - } - - private function getLibsString(array $libraries, bool $use_short_libs = true): string - { - $lib_names = []; - $frameworks = []; - - foreach ($libraries as $library) { - // add pkg-configs libs - $pkg_configs = Config::getLib($library, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)); - foreach ($pkg_configs as $file) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$file}.pc")) { - $found = true; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist. Please build it first."); - } - } - $pkg_configs = implode(' ', $pkg_configs); - if ($pkg_configs !== '') { - // static libs with dependencies come in reverse order, so reverse this too - $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); - $lib_names = [...$lib_names, ...$pc_libs]; - } - // convert all static-libs to short names - $libs = array_reverse(Config::getLib($library, 'static-libs', [])); - foreach ($libs as $lib) { - // check file existence - if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { - throw new WrongUsageException("Library file '{$lib}' for lib [{$library}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); - } - $lib_names[] = $this->getShortLibName($lib); - } - // add frameworks for macOS - if (SPCTarget::getTargetOS() === 'Darwin') { - $frameworks = array_merge($frameworks, Config::getLib($library, 'frameworks', [])); - } - } - - // post-process - $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); - $lib_names = array_reverse(array_unique($lib_names)); - $frameworks = array_unique($frameworks); - - // process frameworks to short_name - if (SPCTarget::getTargetOS() === 'Darwin') { - foreach ($frameworks as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $lib_names)) { - $lib_names[] = $ks; - } - } - } - - if (in_array('imap', $libraries) && SPCTarget::getLibc() === 'glibc') { - $lib_names[] = '-lcrypt'; - } - if (!$use_short_libs) { - $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); - } - return implode(' ', $lib_names); - } - - private function getShortLibName(string $lib): string - { - if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { - return BUILD_LIB_PATH . '/' . $lib; - } - // get short name - return '-l' . substr($lib, 3, -2); - } - - private function getFullLibName(string $lib) - { - if (!str_starts_with($lib, '-l')) { - return $lib; - } - $libname = substr($lib, 2); - $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; - if (file_exists($staticLib)) { - return $staticLib; - } - return $lib; - } - - private function getFrameworksString(array $extensions): string - { - $list = []; - foreach ($extensions as $extension) { - foreach (Config::getExt($extension, 'frameworks', []) as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $list)) { - $list[] = $ks; - } - } - } - return implode(' ', $list); - } -} diff --git a/src/SPC/util/SPCTarget.php b/src/SPC/util/SPCTarget.php deleted file mode 100644 index 037c6d8c3..000000000 --- a/src/SPC/util/SPCTarget.php +++ /dev/null @@ -1,130 +0,0 @@ - 'Linux', - str_contains($target, '-macos') => 'Darwin', - str_contains($target, '-windows') => 'Windows', - str_contains($target, '-native') => PHP_OS_FAMILY, - default => PHP_OS_FAMILY, - }; - } -} diff --git a/src/SPC/util/executor/Executor.php b/src/SPC/util/executor/Executor.php deleted file mode 100644 index 05bb408cf..000000000 --- a/src/SPC/util/executor/Executor.php +++ /dev/null @@ -1,20 +0,0 @@ -initShell(); - $this->configure_args = $this->getDefaultConfigureArgs(); - } - - /** - * Run ./configure - */ - public function configure(...$args): static - { - $args = array_merge($args, $this->configure_args); - $configure_args = implode(' ', $args); - - return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}")); - } - - /** - * Run make - * - * @param string $target Build target - * @param false|string $with_install Run `make install` after building, or false to skip - * @param bool $with_clean Whether to clean before building - * @param array $after_env_vars Environment variables postfix - */ - public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static - { - return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) { - $shell = $this->shell; - if ($dir) { - $shell = $shell->cd($dir); - } - if ($with_clean) { - $shell->exec('make clean'); - } - $after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : ''; - $shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}"); - if ($with_install !== false) { - $shell->exec("make {$with_install}"); - } - return $shell; - }); - } - - public function exec(string $cmd): static - { - $this->shell->exec($cmd); - return $this; - } - - /** - * Add optional library configuration. - * This method checks if a library is available and adds the corresponding arguments to the CMake configuration. - * - * @param string $name library name to check - * @param \Closure|string $true_args arguments to use if the library is available (allow closure, returns string) - * @param string $false_args arguments to use if the library is not available - * @return $this - */ - public function optionalLib(string $name, \Closure|string $true_args, string $false_args = ''): static - { - if ($get = $this->library->getBuilder()->getLib($name)) { - logger()->info("Building library [{$this->library->getName()}] with {$name} support"); - $args = $true_args instanceof \Closure ? $true_args($get) : $true_args; - } else { - logger()->info("Building library [{$this->library->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->configure_args = array_diff($this->configure_args, $args); - return $this; - } - - public function setEnv(array $env): static - { - $this->shell->setEnv($env); - return $this; - } - - public function appendEnv(array $env): static - { - $this->shell->appendEnv($env); - return $this; - } - - /** - * Returns the default autoconf ./configure arguments - */ - private function getDefaultConfigureArgs(): array - { - return [ - '--enable-static', - '--disable-shared', - "--prefix={$this->library->getBuildRootPath()}", - '--with-pic', - '--enable-pic', - ]; - } - - /** - * Initialize UnixShell class. - */ - private function initShell(): void - { - $this->shell = shell()->cd($this->library->getSourceDir())->initializeEnv($this->library)->appendEnv([ - 'CFLAGS' => "-I{$this->library->getIncludeDir()}", - 'CXXFLAGS' => "-I{$this->library->getIncludeDir()}", - 'LDFLAGS' => "-L{$this->library->getLibDir()}", - ]); - } - - /** - * When an exception occurs, this method will check if the config log file exists. - */ - private function seekLogFileOnException(mixed $callable): static - { - try { - $callable(); - return $this; - } catch (SPCException $e) { - if (file_exists("{$this->library->getSourceDir()}/config.log")) { - logger()->debug("Config log file found: {$this->library->getSourceDir()}/config.log"); - $log_file = "lib.{$this->library->getName()}.console.log"; - logger()->debug('Saved config log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); - $e->addExtraLogFile("{$this->library->getName()} library config.log", $log_file); - copy("{$this->library->getSourceDir()}/config.log", SPC_LOGS_DIR . "/{$log_file}"); - } - throw $e; - } - } -} diff --git a/src/SPC/util/executor/UnixCMakeExecutor.php b/src/SPC/util/executor/UnixCMakeExecutor.php deleted file mode 100644 index d0241f567..000000000 --- a/src/SPC/util/executor/UnixCMakeExecutor.php +++ /dev/null @@ -1,230 +0,0 @@ -initShell(); - } - - public function build(string $build_pos = '..'): void - { - // set cmake dir - $this->initBuildDir(); - - if ($this->reset) { - FileSystem::resetDir($this->build_dir); - } - - $this->shell = $this->shell->cd($this->build_dir); - - // config - $this->steps >= 1 && $this->shell->exec("cmake {$this->getConfigureArgs()} {$this->getDefaultCMakeArgs()} {$build_pos}"); - - // make - $this->steps >= 2 && $this->shell->exec("cmake --build . -j {$this->library->getBuilder()->concurrency}"); - - // install - $this->steps >= 3 && $this->shell->exec('make install'); - } - - /** - * Add optional library configuration. - * This method checks if a library is available and adds the corresponding arguments to the CMake configuration. - * - * @param string $name library name to check - * @param \Closure|string $true_args arguments to use if the library is available (allow closure, returns string) - * @param string $false_args arguments to use if the library is not available - * @return $this - */ - public function optionalLib(string $name, \Closure|string $true_args, string $false_args = ''): static - { - if ($get = $this->library->getBuilder()->getLib($name)) { - logger()->info("Building library [{$this->library->getName()}] with {$name} support"); - $args = $true_args instanceof \Closure ? $true_args($get) : $true_args; - } else { - logger()->info("Building library [{$this->library->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; - } - - public function appendEnv(array $env): static - { - $this->shell->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 line. - */ - private function getConfigureArgs(): string - { - return implode(' ', $this->configure_args); - } - - private function getDefaultCMakeArgs(): string - { - return implode(' ', $this->custom_default_args ?? [ - '-DCMAKE_BUILD_TYPE=Release', - "-DCMAKE_INSTALL_PREFIX={$this->library->getBuildRootPath()}", - '-DCMAKE_INSTALL_BINDIR=bin', - '-DCMAKE_INSTALL_LIBDIR=lib', - '-DCMAKE_INSTALL_INCLUDEDIR=include', - '-DPOSITION_INDEPENDENT_CODE=ON', - '-DBUILD_SHARED_LIBS=OFF', - "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", - ]); - } - - /** - * Initialize the CMake build directory. - * If the directory is not set, it defaults to the library's source directory with '/build' appended. - */ - private function initBuildDir(): void - { - if ($this->build_dir === null) { - $this->build_dir = "{$this->library->getSourceDir()}/build"; - } - } - - /** - * Generate cmake toolchain file for current spc instance, and return the file path. - * - * @return string CMake toolchain file path - */ - private function makeCmakeToolchainFile(): string - { - static $created; - if (isset($created)) { - return $created; - } - $os = PHP_OS_FAMILY; - $target_arch = arch2gnu(php_uname('m')); - $cflags = getenv('SPC_DEFAULT_C_FLAGS'); - $cc = getenv('CC'); - $cxx = getenv('CCX'); - $include = BUILD_INCLUDE_PATH; - logger()->debug("making cmake tool chain file for {$os} {$target_arch} with CFLAGS='{$cflags}'"); - $root = BUILD_ROOT_PATH; - $pkgConfigExecutable = PkgConfigUtil::findPkgConfig(); - $ccLine = ''; - if ($cc) { - $ccLine = 'SET(CMAKE_C_COMPILER ' . $cc . ')'; - } - $cxxLine = ''; - if ($cxx) { - $cxxLine = 'SET(CMAKE_CXX_COMPILER ' . $cxx . ')'; - } - $toolchain = <<shell = shell()->initializeEnv($this->library); - } -} diff --git a/src/SPC/util/shell/Shell.php b/src/SPC/util/shell/Shell.php deleted file mode 100644 index bfa51b860..000000000 --- a/src/SPC/util/shell/Shell.php +++ /dev/null @@ -1,173 +0,0 @@ -debug = $debug ?? defined('DEBUG_MODE'); - $this->enable_log_file = $enable_log_file; - } - - /** - * Equivalent to `cd` command in shell. - * - * @param string $dir Directory to change to - */ - public function cd(string $dir): static - { - logger()->debug('Entering dir: ' . $dir); - $c = clone $this; - $c->cd = $dir; - return $c; - } - - public function setEnv(array $env): static - { - foreach ($env as $k => $v) { - if (trim($v) === '') { - continue; - } - $this->env[$k] = trim($v); - } - return $this; - } - - public function appendEnv(array $env): static - { - foreach ($env as $k => $v) { - if ($v === '') { - continue; - } - if (!isset($this->env[$k])) { - $this->env[$k] = $v; - } else { - $this->env[$k] = "{$v} {$this->env[$k]}"; - } - } - return $this; - } - - /** - * Executes a command in the shell. - */ - abstract public function exec(string $cmd): static; - - /** - * Returns the last executed command. - */ - 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 - */ - protected function passthru(string $cmd, bool $console_output = false, ?string $original_command = null): void - { - // write executed command to the log file using fwrite - $file_res = fopen(SPC_SHELL_LOG, 'a'); - if ($console_output) { - $console_res = STDOUT; - } - $descriptors = [ - 0 => ['file', 'php://stdin', 'r'], // stdin - 1 => ['pipe', 'w'], // stdout - 2 => ['pipe', 'w'], // stderr - ]; - $process = proc_open($cmd, $descriptors, $pipes); - - try { - if (!is_resource($process)) { - throw new ExecutionException( - cmd: $original_command ?? $cmd, - message: 'Failed to open process for command, proc_open() failed.', - code: -1, - cd: $this->cd, - env: $this->env - ); - } - // fclose($pipes[0]); - stream_set_blocking($pipes[1], false); - stream_set_blocking($pipes[2], false); - - while (true) { - $read = [$pipes[1], $pipes[2]]; - $write = null; - $except = null; - - $ready = stream_select($read, $write, $except, 0, 100000); - - if ($ready === false) { - $status = proc_get_status($process); - if (!$status['running']) { - break; - } - continue; - } - - if ($ready > 0) { - foreach ($read as $pipe) { - $chunk = fgets($pipe); - if ($chunk !== false) { - if ($console_output) { - fwrite($console_res, $chunk); - } - if ($this->enable_log_file) { - fwrite($file_res, $chunk); - } - } - } - } - - $status = proc_get_status($process); - if (!$status['running']) { - // check exit code - if ($status['exitcode'] !== 0) { - if ($this->enable_log_file) { - fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); - } - throw new ExecutionException( - cmd: $original_command ?? $cmd, - message: "Command exited with non-zero code: {$status['exitcode']}", - code: $status['exitcode'], - cd: $this->cd, - env: $this->env, - ); - } - break; - } - } - } finally { - fclose($pipes[1]); - fclose($pipes[2]); - fclose($file_res); - proc_close($process); - } - } - - /** - * Logs the command information to a log file. - */ - abstract protected function logCommandInfo(string $cmd): void; -} diff --git a/src/SPC/util/shell/UnixShell.php b/src/SPC/util/shell/UnixShell.php deleted file mode 100644 index 2fdc1b077..000000000 --- a/src/SPC/util/shell/UnixShell.php +++ /dev/null @@ -1,116 +0,0 @@ -info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - $original_command = $cmd; - $this->logCommandInfo($original_command); - $this->last_cmd = $cmd = $this->getExecString($cmd); - $this->passthru($cmd, $this->debug, $original_command); - return $this; - } - - /** - * Init the environment variable that common build will be used. - * - * @param BSDLibraryBase|LinuxLibraryBase|MacOSLibraryBase $library Library class - */ - public function initializeEnv(BSDLibraryBase|LinuxLibraryBase|MacOSLibraryBase $library): UnixShell - { - $this->setEnv([ - 'CFLAGS' => $library->getLibExtraCFlags(), - 'CXXFLAGS' => $library->getLibExtraCXXFlags(), - 'LDFLAGS' => $library->getLibExtraLdFlags(), - 'LIBS' => $library->getLibExtraLibs() . SPCTarget::getRuntimeLibs(), - ]); - return $this; - } - - public function execWithResult(string $cmd, bool $with_log = true): array - { - if ($with_log) { - /* @phpstan-ignore-next-line */ - logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); - } else { - /* @phpstan-ignore-next-line */ - logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd)); - } - $cmd = $this->getExecString($cmd); - exec($cmd, $out, $code); - return [$code, $out]; - } - - /** - * Returns unix-style environment variable string. - */ - public function getEnvString(): string - { - $str = ''; - foreach ($this->env as $k => $v) { - $str .= ' ' . $k . '="' . $v . '"'; - } - return trim($str); - } - - protected function logCommandInfo(string $cmd): void - { - // write executed command to log file using fwrite - $log_file = fopen(SPC_SHELL_LOG, 'a'); - fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); - fwrite($log_file, "> Executing command: {$cmd}\n"); - // get the backtrace to find the file and line number - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); - if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { - $file = $backtrace[1]['file']; - $line = $backtrace[1]['line']; - fwrite($log_file, "> Called from: {$file} at line {$line}\n"); - } - fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); - if ($this->cd !== null) { - fwrite($log_file, "> Working dir: {$this->cd}\n"); - } - fwrite($log_file, "\n"); - } - - private function getExecString(string $cmd): string - { - // logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); - $env_str = $this->getEnvString(); - if (!empty($env_str)) { - $cmd = "{$env_str} {$cmd}"; - } - if ($this->cd !== null) { - $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } - return $cmd; - } -} diff --git a/src/SPC/util/shell/WindowsCmd.php b/src/SPC/util/shell/WindowsCmd.php deleted file mode 100644 index aa558e63c..000000000 --- a/src/SPC/util/shell/WindowsCmd.php +++ /dev/null @@ -1,135 +0,0 @@ -info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - - $original_command = $cmd; - $this->logCommandInfo($original_command); - $this->last_cmd = $cmd = $this->getExecString($cmd); - // echo $cmd . PHP_EOL; - - $this->passthru($cmd, $this->debug, $original_command); - return $this; - } - - public function execWithWrapper(string $wrapper, string $args): WindowsCmd - { - return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); - } - - public function execWithResult(string $cmd, bool $with_log = true): array - { - if ($with_log) { - /* @phpstan-ignore-next-line */ - logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); - } else { - logger()->debug('Running command with result: ' . $cmd); - } - exec($cmd, $out, $code); - return [$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; - } - - protected function passthru(string $cmd, bool $console_output = false, ?string $original_command = null): void - { - $file_res = null; - if ($this->enable_log_file) { - $file_res = fopen(SPC_SHELL_LOG, 'a'); - } - - 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 ($console_output) { - echo $line; - } - fwrite($file_res, $line); - } - - $result_code = pclose($process); - - if ($result_code !== 0) { - if ($file_res) { - 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, - ); - } - } finally { - if ($file_res) { - fclose($file_res); - } - } - } - - protected function logCommandInfo(string $cmd): void - { - // write executed command to the log file using fwrite - $log_file = fopen(SPC_SHELL_LOG, 'a'); - fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); - fwrite($log_file, "> Executing command: {$cmd}\n"); - if ($this->cd !== null) { - fwrite($log_file, "> Working dir: {$this->cd}\n"); - } - fwrite($log_file, "\n"); - } - - private function getExecString(string $cmd): string - { - if ($this->cd !== null) { - $cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } - return $cmd; - } -} diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index b4c041df3..1f778a0d4 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -154,10 +154,9 @@ public function run(bool $disable_delay_msg = false): void $this->resolvePackages(); } - // show install or build options in terminal with beautiful output - $this->printInstallerInfo(); - if ($this->interactive && !$disable_delay_msg) { + // show install or build options in terminal with beautiful output + $this->printInstallerInfo(); InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL); sleep(2); } From 4671be623bd0732a57804a5536f6d36d9f2f0708 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:29:06 +0800 Subject: [PATCH 621/682] Add v3 artifact test --- .../StaticPHP/Artifact/ArtifactCacheTest.php | 548 +++++++++++++ .../Artifact/ArtifactDownloaderTest.php | 351 ++++++++ .../Artifact/ArtifactExtractorTest.php | 229 ++++++ tests/StaticPHP/Artifact/ArtifactTest.php | 750 ++++++++++++++++++ 4 files changed, 1878 insertions(+) create mode 100644 tests/StaticPHP/Artifact/ArtifactCacheTest.php create mode 100644 tests/StaticPHP/Artifact/ArtifactDownloaderTest.php create mode 100644 tests/StaticPHP/Artifact/ArtifactExtractorTest.php create mode 100644 tests/StaticPHP/Artifact/ArtifactTest.php diff --git a/tests/StaticPHP/Artifact/ArtifactCacheTest.php b/tests/StaticPHP/Artifact/ArtifactCacheTest.php new file mode 100644 index 000000000..36bcf0069 --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactCacheTest.php @@ -0,0 +1,548 @@ +tempDir = sys_get_temp_dir() . '/artifact_cache_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + $this->cacheFile = $this->tempDir . '/.cache.json'; + } + + protected function tearDown(): void + { + parent::tearDown(); + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + } + + // ==================== Constructor ==================== + + public function testConstructorCreatesFileWhenNotExists(): void + { + $this->assertFalse(file_exists($this->cacheFile)); + + new ArtifactCache($this->cacheFile); + + $this->assertTrue(file_exists($this->cacheFile)); + $this->assertSame('[]', file_get_contents($this->cacheFile)); + } + + public function testConstructorReadsExistingCacheFile(): void + { + $existing = ['openssl' => ['source' => null, 'binary' => []]]; + file_put_contents($this->cacheFile, json_encode($existing)); + + $cache = new ArtifactCache($this->cacheFile); + + $this->assertSame([], $cache->getAllBinaryInfo('openssl')); + } + + public function testConstructorHandlesEmptyExistingFile(): void + { + file_put_contents($this->cacheFile, ''); + + $cache = new ArtifactCache($this->cacheFile); + + $this->assertEmpty($cache->getCachedArtifactNames()); + } + + // ==================== isSourceDownloaded ==================== + + public function testIsSourceDownloadedReturnsFalseWhenNotCached(): void + { + $cache = new ArtifactCache($this->cacheFile); + + $this->assertFalse($cache->isSourceDownloaded('non-existent')); + } + + public function testIsSourceDownloadedReturnsFalseWhenCacheEntryHasNullSource(): void + { + file_put_contents($this->cacheFile, json_encode([ + 'my-pkg' => ['source' => null, 'binary' => []], + ])); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertFalse($cache->isSourceDownloaded('my-pkg')); + } + + public function testIsSourceDownloadedReturnsTrueForLocalTypeWhenDirExists(): void + { + $localDir = $this->tempDir . '/local-source'; + mkdir($localDir, 0755, true); + + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => [ + 'lock_type' => 'source', + 'cache_type' => 'local', + 'dirname' => $localDir, + 'extract' => null, + 'hash' => null, + 'time' => time(), + ], + 'binary' => [], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertTrue($cache->isSourceDownloaded('my-pkg')); + } + + public function testIsSourceDownloadedReturnsFalseForLocalTypeWhenDirNotExists(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => [ + 'lock_type' => 'source', + 'cache_type' => 'local', + 'dirname' => '/non/existent/path/xyz', + 'extract' => null, + 'hash' => null, + 'time' => time(), + ], + 'binary' => [], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertFalse($cache->isSourceDownloaded('my-pkg')); + } + + public function testIsSourceDownloadedReturnsFalseForArchiveTypeWhenFileNotExists(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => [ + 'lock_type' => 'source', + 'cache_type' => 'archive', + 'filename' => 'non-existent-file.tar.gz', + 'extract' => null, + 'hash' => 'abc123', + 'time' => time(), + ], + 'binary' => [], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertFalse($cache->isSourceDownloaded('my-pkg')); + } + + // ==================== isBinaryDownloaded ==================== + + public function testIsBinaryDownloadedReturnsFalseWhenNotCached(): void + { + $cache = new ArtifactCache($this->cacheFile); + + $this->assertFalse($cache->isBinaryDownloaded('non-existent', 'linux-x86_64')); + } + + public function testIsBinaryDownloadedReturnsFalseWhenPlatformNotCached(): void + { + $this->writeCacheData([ + 'my-pkg' => ['source' => null, 'binary' => []], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertFalse($cache->isBinaryDownloaded('my-pkg', 'linux-x86_64')); + } + + public function testIsBinaryDownloadedReturnsTrueForLocalTypeWhenDirExists(): void + { + $localDir = $this->tempDir . '/local-binary'; + mkdir($localDir, 0755, true); + + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => null, + 'binary' => [ + 'linux-x86_64' => [ + 'lock_type' => 'binary', + 'cache_type' => 'local', + 'dirname' => $localDir, + 'extract' => null, + 'hash' => null, + 'time' => time(), + ], + ], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertTrue($cache->isBinaryDownloaded('my-pkg', 'linux-x86_64')); + } + + // ==================== lock ==================== + + public function testLockWithLocalSourceType(): void + { + $localDir = $this->tempDir . '/local-pkg'; + mkdir($localDir, 0755, true); + + $cache = new ArtifactCache($this->cacheFile); + $downloadResult = DownloadResult::local($localDir, [], null, '1.0.0'); + + $cache->lock('my-pkg', 'source', $downloadResult); + + $info = $cache->getSourceInfo('my-pkg'); + $this->assertNotNull($info); + $this->assertSame('source', $info['lock_type']); + $this->assertSame('local', $info['cache_type']); + $this->assertSame($localDir, $info['dirname']); + } + + public function testLockWithLocalBinaryTypePersistsCorrectPlatform(): void + { + $localDir = $this->tempDir . '/local-bin'; + mkdir($localDir, 0755, true); + + $cache = new ArtifactCache($this->cacheFile); + $downloadResult = DownloadResult::local($localDir, [], null, '1.0.0'); + + $cache->lock('my-pkg', 'binary', $downloadResult, 'linux-x86_64'); + + $info = $cache->getBinaryInfo('my-pkg', 'linux-x86_64'); + $this->assertNotNull($info); + $this->assertSame('binary', $info['lock_type']); + $this->assertSame('linux-x86_64', $info['platform']); + } + + public function testLockWithBinaryTypeThrowsWhenPlatformIsNull(): void + { + $localDir = $this->tempDir . '/local-bin2'; + mkdir($localDir, 0755, true); + + $cache = new ArtifactCache($this->cacheFile); + $downloadResult = DownloadResult::local($localDir, [], null); + + $this->expectException(SPCInternalException::class); + $cache->lock('my-pkg', 'binary', $downloadResult, null); + } + + public function testLockPersistsCacheToFile(): void + { + $localDir = $this->tempDir . '/persist-test'; + mkdir($localDir, 0755, true); + + $cache = new ArtifactCache($this->cacheFile); + $downloadResult = DownloadResult::local($localDir, []); + + $cache->lock('my-pkg', 'source', $downloadResult); + + // Read file contents to verify persisted + $persisted = json_decode(file_get_contents($this->cacheFile), true); + $this->assertArrayHasKey('my-pkg', $persisted); + $this->assertNotNull($persisted['my-pkg']['source']); + } + + // ==================== getSourceInfo ==================== + + public function testGetSourceInfoReturnsNullWhenNotCached(): void + { + $cache = new ArtifactCache($this->cacheFile); + + $this->assertNull($cache->getSourceInfo('non-existent')); + } + + public function testGetSourceInfoReturnsNullWhenSourceIsNull(): void + { + $this->writeCacheData(['my-pkg' => ['source' => null, 'binary' => []]]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertNull($cache->getSourceInfo('my-pkg')); + } + + public function testGetSourceInfoReturnsData(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => ['lock_type' => 'source', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + 'binary' => [], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $info = $cache->getSourceInfo('my-pkg'); + $this->assertIsArray($info); + $this->assertSame('local', $info['cache_type']); + } + + // ==================== getBinaryInfo ==================== + + public function testGetBinaryInfoReturnsNullWhenNotCached(): void + { + $cache = new ArtifactCache($this->cacheFile); + + $this->assertNull($cache->getBinaryInfo('non-existent', 'linux-x86_64')); + } + + public function testGetBinaryInfoReturnsDataForPlatform(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => null, + 'binary' => [ + 'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + ], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $info = $cache->getBinaryInfo('my-pkg', 'linux-x86_64'); + $this->assertIsArray($info); + $this->assertSame('local', $info['cache_type']); + } + + public function testGetBinaryInfoReturnsNullForDifferentPlatform(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => null, + 'binary' => [ + 'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + ], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $this->assertNull($cache->getBinaryInfo('my-pkg', 'macos-aarch64')); + } + + // ==================== getAllBinaryInfo ==================== + + public function testGetAllBinaryInfoReturnsEmptyWhenNone(): void + { + $cache = new ArtifactCache($this->cacheFile); + + $this->assertSame([], $cache->getAllBinaryInfo('non-existent')); + } + + public function testGetAllBinaryInfoReturnsAllPlatforms(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => null, + 'binary' => [ + 'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + 'macos-aarch64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + ], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $all = $cache->getAllBinaryInfo('my-pkg'); + $this->assertCount(2, $all); + $this->assertArrayHasKey('linux-x86_64', $all); + $this->assertArrayHasKey('macos-aarch64', $all); + } + + // ==================== getCacheFullPath ==================== + + public function testGetCacheFullPathForArchiveType(): void + { + $cache = new ArtifactCache($this->cacheFile); + $info = ['cache_type' => 'archive', 'filename' => 'openssl-3.0.tar.gz']; + + $expected = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . 'openssl-3.0.tar.gz'; + $this->assertSame($expected, $cache->getCacheFullPath($info)); + } + + public function testGetCacheFullPathForGitType(): void + { + $cache = new ArtifactCache($this->cacheFile); + $info = ['cache_type' => 'git', 'dirname' => 'my-repo']; + + $expected = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . 'my-repo'; + $this->assertSame($expected, $cache->getCacheFullPath($info)); + } + + public function testGetCacheFullPathForLocalType(): void + { + $cache = new ArtifactCache($this->cacheFile); + $info = ['cache_type' => 'local', 'dirname' => '/absolute/path/to/dir']; + + $this->assertSame('/absolute/path/to/dir', $cache->getCacheFullPath($info)); + } + + public function testGetCacheFullPathForFileType(): void + { + $cache = new ArtifactCache($this->cacheFile); + $info = ['cache_type' => 'file', 'filename' => 'some-tool.exe']; + + $expected = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . 'some-tool.exe'; + $this->assertSame($expected, $cache->getCacheFullPath($info)); + } + + public function testGetCacheFullPathThrowsForUnknownType(): void + { + $cache = new ArtifactCache($this->cacheFile); + $info = ['cache_type' => 'unknown-type']; + + $this->expectException(SPCInternalException::class); + $cache->getCacheFullPath($info); + } + + // ==================== removeSource ==================== + + public function testRemoveSourceIsNoOpWhenNotCached(): void + { + $cache = new ArtifactCache($this->cacheFile); + + // Should not throw + $cache->removeSource('non-existent'); + $this->assertNull($cache->getSourceInfo('non-existent')); + } + + public function testRemoveSourceRemovesCacheEntry(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => ['lock_type' => 'source', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + 'binary' => [], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $cache->removeSource('my-pkg'); + + $this->assertNull($cache->getSourceInfo('my-pkg')); + } + + public function testRemoveSourcePersistsToFile(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => ['lock_type' => 'source', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + 'binary' => [], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $cache->removeSource('my-pkg'); + + $persisted = json_decode(file_get_contents($this->cacheFile), true); + $this->assertNull($persisted['my-pkg']['source']); + } + + // ==================== removeBinary ==================== + + public function testRemoveBinaryIsNoOpWhenNotCached(): void + { + $cache = new ArtifactCache($this->cacheFile); + + // Should not throw + $cache->removeBinary('non-existent', 'linux-x86_64'); + $this->assertNull($cache->getBinaryInfo('non-existent', 'linux-x86_64')); + } + + public function testRemoveBinaryRemovesPlatformEntry(): void + { + $this->writeCacheData([ + 'my-pkg' => [ + 'source' => null, + 'binary' => [ + 'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + 'macos-aarch64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0], + ], + ], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $cache->removeBinary('my-pkg', 'linux-x86_64'); + + $this->assertNull($cache->getBinaryInfo('my-pkg', 'linux-x86_64')); + // Other platform should still be there + $this->assertNotNull($cache->getBinaryInfo('my-pkg', 'macos-aarch64')); + } + + // ==================== getCachedArtifactNames ==================== + + public function testGetCachedArtifactNamesReturnsEmptyWhenNoCacheFile(): void + { + $cache = new ArtifactCache($this->cacheFile); + + $this->assertSame([], $cache->getCachedArtifactNames()); + } + + public function testGetCachedArtifactNamesReturnsAllNames(): void + { + $this->writeCacheData([ + 'openssl' => ['source' => null, 'binary' => []], + 'zlib' => ['source' => null, 'binary' => []], + 'brotli' => ['source' => null, 'binary' => []], + ]); + $cache = new ArtifactCache($this->cacheFile); + + $names = $cache->getCachedArtifactNames(); + $this->assertCount(3, $names); + $this->assertContains('openssl', $names); + $this->assertContains('zlib', $names); + $this->assertContains('brotli', $names); + } + + // ==================== save ==================== + + public function testSavePersistsInMemoryCacheToFile(): void + { + $localDir = $this->tempDir . '/save-test-dir'; + mkdir($localDir, 0755, true); + + $cache = new ArtifactCache($this->cacheFile); + // Lock an artifact so cache has data + $downloadResult = DownloadResult::local($localDir, []); + $cache->lock('my-pkg', 'source', $downloadResult); + + // Overwrite cache file to simulate external change + file_put_contents($this->cacheFile, json_encode([])); + + // Save should re-write in-memory state + $cache->save(); + + $persisted = json_decode(file_get_contents($this->cacheFile), true); + $this->assertArrayHasKey('my-pkg', $persisted); + } + + // ==================== Helpers ==================== + + private function writeCacheData(array $data): void + { + file_put_contents($this->cacheFile, json_encode($data)); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php b/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php new file mode 100644 index 000000000..919452d6c --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php @@ -0,0 +1,351 @@ +getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue(null, []); + + $loaderReflection = new \ReflectionClass(ArtifactLoader::class); + $loaderProperty = $loaderReflection->getProperty('artifacts'); + $loaderProperty->setAccessible(true); + $loaderProperty->setValue(null, null); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue(null, []); + + $loaderReflection = new \ReflectionClass(ArtifactLoader::class); + $loaderProperty = $loaderReflection->getProperty('artifacts'); + $loaderProperty->setAccessible(true); + $loaderProperty->setValue(null, null); + } + + // ==================== DOWNLOADERS constant ==================== + + public function testDownloadersConstantHasExpectedKeys(): void + { + $expectedKeys = ['bitbuckettag', 'filelist', 'git', 'ghrel', 'ghtar', 'ghtagtar', 'local', 'pie', 'pecl', 'url', 'php-release', 'hosted']; + + foreach ($expectedKeys as $key) { + $this->assertArrayHasKey($key, ArtifactDownloader::DOWNLOADERS, "Missing downloader key: {$key}"); + } + } + + // ==================== Constructor options ==================== + + public function testConstructWithDefaultOptions(): void + { + $downloader = new ArtifactDownloader([], false); + + $this->assertSame(0, $downloader->getRetry()); + $this->assertFalse($downloader->interactive); + $this->assertEmpty($downloader->getArtifacts()); + } + + public function testConstructWithParallelOption(): void + { + $downloader = new ArtifactDownloader(['parallel' => 4], false); + + // parallel is internal but setParallel/getArtifacts reveals behavior; check via setParallel chainability + // Indirect verification: setParallel with same value returns $this + $this->assertSame($downloader, $downloader->setParallel(4)); + } + + public function testConstructWithRetryOption(): void + { + $downloader = new ArtifactDownloader(['retry' => 3], false); + + $this->assertSame(3, $downloader->getRetry()); + } + + public function testConstructWithNegativeRetryClampedToZero(): void + { + $downloader = new ArtifactDownloader(['retry' => -5], false); + + $this->assertSame(0, $downloader->getRetry()); + } + + public function testConstructWithPreferSourceBoolOption(): void + { + // prefer-source=true sets default to FETCH_PREFER_SOURCE (0) + $downloader = new ArtifactDownloader(['prefer-source' => true], false); + + $this->assertSame(0, $downloader->getRetry()); // sanity check, object created fine + } + + public function testConstructWithPreferBinaryBoolOption(): void + { + $downloader = new ArtifactDownloader(['prefer-binary' => true], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithPreferPreBuiltBoolOption(): void + { + $downloader = new ArtifactDownloader(['prefer-pre-built' => true], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithSourceOnlyBoolOption(): void + { + $downloader = new ArtifactDownloader(['source-only' => true], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithBinaryOnlyBoolOption(): void + { + $downloader = new ArtifactDownloader(['binary-only' => true], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithIgnoreCacheBoolOption(): void + { + $downloader = new ArtifactDownloader(['ignore-cache' => true], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithIgnoreCacheStringOptionParsesNames(): void + { + $downloader = new ArtifactDownloader(['ignore-cache' => 'openssl,zlib'], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithIgnoreCacheSourcesBackwardCompat(): void + { + $downloader = new ArtifactDownloader(['ignore-cache-sources' => true], false); + + $this->assertNotNull($downloader); + } + + public function testConstructWithCustomUrlOptionAddsToIgnoreCache(): void + { + $downloader = new ArtifactDownloader( + ['custom-url' => ['openssl:https://custom.example.com/openssl.tar.gz']], + false + ); + + $this->assertNotNull($downloader); + } + + public function testConstructWithCustomGitOption(): void + { + $downloader = new ArtifactDownloader( + ['custom-git' => ['php-src:master:https://github.com/php/php-src.git']], + false + ); + + $this->assertNotNull($downloader); + } + + public function testConstructWithCustomLocalOption(): void + { + $downloader = new ArtifactDownloader( + ['custom-local' => ['my-lib:/tmp/my-lib-source']], + false + ); + + $this->assertNotNull($downloader); + } + + public function testConstructWithNoAltOption(): void + { + $downloader = new ArtifactDownloader(['no-alt' => true], false); + + $this->assertNotNull($downloader); + } + + // ==================== getRetry ==================== + + public function testGetRetryDefaultsToZero(): void + { + $downloader = new ArtifactDownloader([], false); + + $this->assertSame(0, $downloader->getRetry()); + } + + public function testGetRetryReturnsConfiguredValue(): void + { + $downloader = new ArtifactDownloader(['retry' => 5], false); + + $this->assertSame(5, $downloader->getRetry()); + } + + // ==================== getOption ==================== + + public function testGetOptionReturnsConfiguredValue(): void + { + $downloader = new ArtifactDownloader(['retry' => 2, 'parallel' => 3], false); + + $this->assertSame(2, $downloader->getOption('retry')); + $this->assertSame(3, $downloader->getOption('parallel')); + } + + public function testGetOptionReturnsDefaultWhenNotSet(): void + { + $downloader = new ArtifactDownloader([], false); + + $this->assertNull($downloader->getOption('non-existent')); + $this->assertSame('default-val', $downloader->getOption('non-existent', 'default-val')); + } + + // ==================== getArtifacts ==================== + + public function testGetArtifactsReturnsEmptyInitially(): void + { + $downloader = new ArtifactDownloader([], false); + + $this->assertSame([], $downloader->getArtifacts()); + } + + // ==================== add ==================== + + public function testAddWithArtifactObjectAddsToList(): void + { + $downloader = new ArtifactDownloader([], false); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $downloader->add($artifact); + + $artifacts = $downloader->getArtifacts(); + $this->assertArrayHasKey('my-pkg', $artifacts); + $this->assertSame($artifact, $artifacts['my-pkg']); + } + + public function testAddReturnsSelf(): void + { + $downloader = new ArtifactDownloader([], false); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $result = $downloader->add($artifact); + + $this->assertSame($downloader, $result); + } + + public function testAddDoesNotAddDuplicateArtifact(): void + { + $downloader = new ArtifactDownloader([], false); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $downloader->add($artifact); + $downloader->add($artifact); + + $this->assertCount(1, $downloader->getArtifacts()); + } + + public function testAddWithStringNameLooksUpFromArtifactLoader(): void + { + $this->injectArtifactConfig('my-lib', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $downloader = new ArtifactDownloader([], false); + $downloader->add('my-lib'); + + $artifacts = $downloader->getArtifacts(); + $this->assertArrayHasKey('my-lib', $artifacts); + } + + public function testAddWithStringNameThrowsForNonExistentArtifact(): void + { + $downloader = new ArtifactDownloader([], false); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage("Artifact 'non-existent' not found"); + + $downloader->add('non-existent'); + } + + // ==================== addArtifacts ==================== + + public function testAddArtifactsAddsMultipleAtOnce(): void + { + $downloader = new ArtifactDownloader([], false); + $a1 = new Artifact('pkg-a', ['source' => ['type' => 'url', 'url' => 'https://example.com/a.tar.gz']]); + $a2 = new Artifact('pkg-b', ['source' => ['type' => 'url', 'url' => 'https://example.com/b.tar.gz']]); + + $downloader->addArtifacts([$a1, $a2]); + + $this->assertCount(2, $downloader->getArtifacts()); + $this->assertArrayHasKey('pkg-a', $downloader->getArtifacts()); + $this->assertArrayHasKey('pkg-b', $downloader->getArtifacts()); + } + + public function testAddArtifactsReturnsSelf(): void + { + $downloader = new ArtifactDownloader([], false); + + $result = $downloader->addArtifacts([]); + + $this->assertSame($downloader, $result); + } + + // ==================== setParallel ==================== + + public function testSetParallelReturnsSelf(): void + { + $downloader = new ArtifactDownloader([], false); + + $result = $downloader->setParallel(3); + + $this->assertSame($downloader, $result); + } + + public function testSetParallelEnforcesMinimumOfOne(): void + { + $downloader = new ArtifactDownloader([], false); + + $downloader->setParallel(0); + // No direct getter for parallel, but verifying it doesn't throw + $this->assertSame($downloader, $downloader->setParallel(0)); + } + + public function testSetParallelAcceptsNormalValue(): void + { + $downloader = new ArtifactDownloader([], false); + + $result = $downloader->setParallel(5); + $this->assertSame($downloader, $result); + } + + // ==================== Helpers ==================== + + private function injectArtifactConfig(string $name, array $config): void + { + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $configs = $property->getValue(null) ?? []; + $configs[$name] = $config; + $property->setValue(null, $configs); + } +} diff --git a/tests/StaticPHP/Artifact/ArtifactExtractorTest.php b/tests/StaticPHP/Artifact/ArtifactExtractorTest.php new file mode 100644 index 000000000..d95275a64 --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactExtractorTest.php @@ -0,0 +1,229 @@ +tempDir = sys_get_temp_dir() . '/artifact_extractor_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + $this->cacheFile = $this->tempDir . '/.cache.json'; + file_put_contents($this->cacheFile, json_encode([])); + + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue(null, []); + + $loaderReflection = new \ReflectionClass(ArtifactLoader::class); + $loaderProperty = $loaderReflection->getProperty('artifacts'); + $loaderProperty->setAccessible(true); + $loaderProperty->setValue(null, null); + + ApplicationContext::reset(); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue(null, []); + + $loaderReflection = new \ReflectionClass(ArtifactLoader::class); + $loaderProperty = $loaderReflection->getProperty('artifacts'); + $loaderProperty->setAccessible(true); + $loaderProperty->setValue(null, null); + + ApplicationContext::reset(); + } + + // ==================== Constructor ==================== + + public function testConstructorStoresProvidedCache(): void + { + $cache = new ArtifactCache($this->cacheFile); + $extractor = new ArtifactExtractor($cache, false); + + // Verify the extractor was created without error; it holds the cache internally + $this->assertInstanceOf(ArtifactExtractor::class, $extractor); + } + + public function testConstructorDefaultsInteractiveToTrue(): void + { + $cache = new ArtifactCache($this->cacheFile); + $extractor = new ArtifactExtractor($cache); + + $this->assertInstanceOf(ArtifactExtractor::class, $extractor); + } + + // ==================== extractForPackages ==================== + + public function testExtractForPackagesWithEmptyArrayDoesNothing(): void + { + $cache = new ArtifactCache($this->cacheFile); + $extractor = new ArtifactExtractor($cache, false); + + // Should complete without exception + $extractor->extractForPackages([]); + $this->assertTrue(true); + } + + public function testExtractForPackagesDeduplicatesArtifacts(): void + { + ApplicationContext::initialize(); + $artifactConfig = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]; + $this->injectArtifactConfig('shared-lib', $artifactConfig); + + $artifact = new Artifact('shared-lib', $artifactConfig); + + // Create two mock packages that share the same artifact + $pkg1 = $this->createMockPackage($artifact); + $pkg2 = $this->createMockPackage($artifact); + + $cache = new ArtifactCache($this->cacheFile); + + // Partial mock to verify extract is called exactly once despite two packages + $extractor = $this->getMockBuilder(ArtifactExtractor::class) + ->setConstructorArgs([$cache, false]) + ->onlyMethods(['extract']) + ->getMock(); + + $extractor->expects($this->once()) + ->method('extract') + ->with($artifact, false) + ->willReturn(SPC_STATUS_ALREADY_EXTRACTED); + + $extractor->extractForPackages([$pkg1, $pkg2]); + } + + public function testExtractForPackagesSkipsPackagesWithNoArtifact(): void + { + $pkgWithoutArtifact = $this->createMockPackage(null); + + $cache = new ArtifactCache($this->cacheFile); + + $extractor = $this->getMockBuilder(ArtifactExtractor::class) + ->setConstructorArgs([$cache, false]) + ->onlyMethods(['extract']) + ->getMock(); + + // extract should NOT be called when no artifact + $extractor->expects($this->never())->method('extract'); + + $extractor->extractForPackages([$pkgWithoutArtifact]); + } + + // ==================== extract ==================== + + public function testExtractReturnsAlreadyExtractedForSecondCall(): void + { + ApplicationContext::initialize(); + $artifactConfig = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]; + $artifact = new Artifact('my-pkg', $artifactConfig); + + $cache = $this->createMock(ArtifactCache::class); + $cache->method('getSourceInfo')->willReturn(null); + $cache->method('getBinaryInfo')->willReturn(null); + $cache->method('isBinaryDownloaded')->willReturn(false); + ApplicationContext::set(ArtifactCache::class, $cache); + + $extractor = new ArtifactExtractor($cache, false); + + // Pre-populate the extracted map for 'my-pkg' via reflection + $reflection = new \ReflectionClass(ArtifactExtractor::class); + $extractedProperty = $reflection->getProperty('extracted'); + $extractedProperty->setAccessible(true); + $extractedProperty->setValue($extractor, ['my-pkg' => true]); + + $result = $extractor->extract($artifact, false); + $this->assertSame(SPC_STATUS_ALREADY_EXTRACTED, $result); + } + + public function testExtractWithStringNameLooksUpFromArtifactLoader(): void + { + ApplicationContext::initialize(); + $artifactConfig = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]; + $this->injectArtifactConfig('my-pkg', $artifactConfig); + + $cache = $this->createMock(ArtifactCache::class); + $cache->method('getSourceInfo')->willReturn(null); + $cache->method('getBinaryInfo')->willReturn(null); + $cache->method('isBinaryDownloaded')->willReturn(false); + ApplicationContext::set(ArtifactCache::class, $cache); + + $extractor = new ArtifactExtractor($cache, false); + + // Pre-populate the extracted map so we don't need actual downloads + $reflection = new \ReflectionClass(ArtifactExtractor::class); + $extractedProperty = $reflection->getProperty('extracted'); + $extractedProperty->setAccessible(true); + $extractedProperty->setValue($extractor, ['my-pkg' => true]); + + $result = $extractor->extract('my-pkg', false); + $this->assertSame(SPC_STATUS_ALREADY_EXTRACTED, $result); + } + + // ==================== Helpers ==================== + + /** + * Create a mock Package object that returns the given artifact from getArtifact(). + */ + private function createMockPackage(?Artifact $artifact): \StaticPHP\Package\Package + { + $mock = $this->createMock(\StaticPHP\Package\Package::class); + $mock->method('getArtifact')->willReturn($artifact); + return $mock; + } + + private function injectArtifactConfig(string $name, array $config): void + { + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $configs = $property->getValue(null) ?? []; + $configs[$name] = $config; + $property->setValue(null, $configs); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/tests/StaticPHP/Artifact/ArtifactTest.php b/tests/StaticPHP/Artifact/ArtifactTest.php new file mode 100644 index 000000000..e1cb8ccd2 --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactTest.php @@ -0,0 +1,750 @@ +tempDir = sys_get_temp_dir() . '/artifact_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset ArtifactConfig static state + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue(null, []); + + // Reset DI container + ApplicationContext::reset(); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $property->setValue(null, []); + + ApplicationContext::reset(); + } + + // ==================== Constants ==================== + + public function testConstantValues(): void + { + $this->assertSame(0, Artifact::FETCH_PREFER_SOURCE); + $this->assertSame(1, Artifact::FETCH_PREFER_BINARY); + $this->assertSame(2, Artifact::FETCH_ONLY_SOURCE); + $this->assertSame(3, Artifact::FETCH_ONLY_BINARY); + } + + // ==================== Constructor ==================== + + public function testConstructWithInlineConfig(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertSame('my-pkg', $artifact->getName()); + } + + public function testConstructFallsBackToArtifactConfig(): void + { + $this->injectArtifactConfig('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $artifact = new Artifact('my-pkg'); + $this->assertSame('my-pkg', $artifact->getName()); + } + + public function testConstructThrowsForNonExistentArtifact(): void + { + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage("Artifact 'non-existent' not found."); + + new Artifact('non-existent'); + } + + // ==================== getName ==================== + + public function testGetName(): void + { + $artifact = new Artifact('openssl', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $this->assertSame('openssl', $artifact->getName()); + } + + // ==================== getDownloadConfig ==================== + + public function testGetDownloadConfigReturnsSection(): void + { + $config = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], 'binary' => []]; + $artifact = new Artifact('my-pkg', $config); + + $this->assertSame(['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], $artifact->getDownloadConfig('source')); + } + + public function testGetDownloadConfigReturnsNullForMissingSection(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertNull($artifact->getDownloadConfig('non-existent')); + } + + // ==================== hasSource ==================== + + public function testHasSourceReturnsTrueWhenConfigHasSource(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertTrue($artifact->hasSource()); + } + + public function testHasSourceReturnsFalseWhenNoSource(): void + { + $artifact = new Artifact('my-pkg', ['binary' => []]); + + $this->assertFalse($artifact->hasSource()); + } + + public function testHasSourceReturnsTrueWithCustomCallback(): void + { + $artifact = new Artifact('my-pkg', ['binary' => []]); + $artifact->setCustomSourceCallback(function () {}); + + $this->assertTrue($artifact->hasSource()); + } + + // ==================== hasPlatformBinary ==================== + + public function testHasPlatformBinaryReturnsTrueWhenConfigHasBinaryForCurrentPlatform(): void + { + $platform = $this->getCurrentPlatform(); + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']], + ]); + + $this->assertTrue($artifact->hasPlatformBinary()); + } + + public function testHasPlatformBinaryReturnsFalseWhenNoBinaryConfig(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertFalse($artifact->hasPlatformBinary()); + } + + public function testHasPlatformBinaryReturnsTrueWithCustomCallback(): void + { + $platform = $this->getCurrentPlatform(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $artifact->setCustomBinaryCallback($platform, function () {}); + + $this->assertTrue($artifact->hasPlatformBinary()); + } + + // ==================== getBinaryPlatforms ==================== + + public function testGetBinaryPlatformsReturnsConfiguredPlatforms(): void + { + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [ + 'linux-x86_64' => ['type' => 'url', 'url' => 'https://example.com/linux.tar.gz'], + 'macos-aarch64' => ['type' => 'url', 'url' => 'https://example.com/mac.tar.gz'], + ], + ]); + + $platforms = $artifact->getBinaryPlatforms(); + $this->assertContains('linux-x86_64', $platforms); + $this->assertContains('macos-aarch64', $platforms); + } + + public function testGetBinaryPlatformsExcludesCustomTypeWithoutCallback(): void + { + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [ + 'linux-x86_64' => ['type' => 'custom'], + ], + ]); + + // No custom callback registered, so custom-type platform should NOT be included + $platforms = $artifact->getBinaryPlatforms(); + $this->assertNotContains('linux-x86_64', $platforms); + } + + public function testGetBinaryPlatformsIncludesCustomTypeWhenCallbackRegistered(): void + { + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [ + 'linux-x86_64' => ['type' => 'custom'], + ], + ]); + $artifact->setCustomBinaryCallback('linux-x86_64', function () {}); + + $platforms = $artifact->getBinaryPlatforms(); + $this->assertContains('linux-x86_64', $platforms); + } + + public function testGetBinaryPlatformsIncludesCustomCallbackPlatforms(): void + { + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + ]); + $artifact->setCustomBinaryCallback('linux-x86_64', function () {}); + + $platforms = $artifact->getBinaryPlatforms(); + $this->assertContains('linux-x86_64', $platforms); + } + + // ==================== getSourceDir ==================== + + public function testGetSourceDirDefaultsToSourcePathWithName(): void + { + $cache = $this->makeStubbedArtifactCache([]); + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SOURCE_PATH . '/my-pkg'); + $this->assertSame($expected, $artifact->getSourceDir()); + } + + public function testGetSourceDirWithRelativeExtractInConfig(): void + { + $cache = $this->makeStubbedArtifactCache([]); + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz', 'extract' => 'my-pkg-1.0'], + ]); + + $expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SOURCE_PATH . '/my-pkg-1.0'); + $this->assertSame($expected, $artifact->getSourceDir()); + } + + public function testGetSourceDirWithAbsoluteExtractInConfig(): void + { + $cache = $this->makeStubbedArtifactCache([]); + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz', 'extract' => '/tmp/my-pkg-extract'], + ]); + + $expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, '/tmp/my-pkg-extract'); + $this->assertSame($expected, $artifact->getSourceDir()); + } + + // ==================== getSourceRoot ==================== + + public function testGetSourceRootDefaultsToSourceDir(): void + { + $cache = $this->makeStubbedArtifactCache([]); + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertSame($artifact->getSourceDir(), $artifact->getSourceRoot()); + } + + public function testGetSourceRootUsesMetadataSourceRoot(): void + { + $cache = $this->makeStubbedArtifactCache([]); + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'metadata' => ['source-root' => 'src'], + ]); + + $expected = $artifact->getSourceDir() . DIRECTORY_SEPARATOR . 'src'; + $this->assertSame($expected, $artifact->getSourceRoot()); + } + + // ==================== getBinaryExtractConfig ==================== + + public function testGetBinaryExtractConfigDefaultsToStandard(): void + { + putenv('EMULATE_PLATFORM=linux-x86_64'); + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + // no binary for linux-x86_64 + ]); + + $config = $artifact->getBinaryExtractConfig(); + $this->assertSame('standard', $config['mode']); + $this->assertSame(PKG_ROOT_PATH, $config['path']); + putenv('EMULATE_PLATFORM'); + } + + public function testGetBinaryExtractConfigWithHostedExtractReturnsBuildRootPath(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $config = $artifact->getBinaryExtractConfig(['extract' => 'hosted']); + $this->assertSame('standard', $config['mode']); + $this->assertSame(BUILD_ROOT_PATH, $config['path']); + } + + public function testGetBinaryExtractConfigWithRelativeExtractInCacheInfo(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $config = $artifact->getBinaryExtractConfig(['extract' => 'subdir']); + $this->assertSame('standard', $config['mode']); + $this->assertStringContainsString('subdir', $config['path']); + } + + public function testGetBinaryExtractConfigWithAbsoluteExtractInCacheInfo(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $config = $artifact->getBinaryExtractConfig(['extract' => '/tmp/absolute-path']); + $this->assertSame('standard', $config['mode']); + $this->assertStringContainsString('absolute-path', $config['path']); + } + + public function testGetBinaryExtractConfigWithArrayReturnsSelective(): void + { + putenv('EMULATE_PLATFORM=linux-x86_64'); + $fileMap = ['lib/libfoo.a' => '/usr/local/lib/libfoo.a']; + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [ + 'linux-x86_64' => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz', 'extract' => $fileMap], + ], + ]); + + $config = $artifact->getBinaryExtractConfig(); + $this->assertSame('selective', $config['mode']); + $this->assertNull($config['path']); + $this->assertSame($fileMap, $config['files']); + putenv('EMULATE_PLATFORM'); + } + + // ==================== getBinaryDir ==================== + + public function testGetBinaryDirDelegatesToGetBinaryExtractConfig(): void + { + putenv('EMULATE_PLATFORM=linux-x86_64'); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertSame(PKG_ROOT_PATH, $artifact->getBinaryDir()); + putenv('EMULATE_PLATFORM'); + } + + // ==================== Custom source callbacks ==================== + + public function testSetAndGetCustomSourceCallback(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setCustomSourceCallback($cb); + + $this->assertSame($cb, $artifact->getCustomSourceCallback()); + } + + public function testGetCustomSourceCallbackReturnsNullWhenNotSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertNull($artifact->getCustomSourceCallback()); + } + + public function testSetAndGetCustomSourceCheckUpdateCallback(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setCustomSourceCheckUpdateCallback($cb); + + $this->assertSame($cb, $artifact->getCustomSourceCheckUpdateCallback()); + } + + public function testGetCustomSourceCheckUpdateCallbackReturnsNullWhenNotSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertNull($artifact->getCustomSourceCheckUpdateCallback()); + } + + // ==================== Custom binary callbacks ==================== + + public function testSetAndGetCustomBinaryCallback(): void + { + $platform = $this->getCurrentPlatform(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setCustomBinaryCallback($platform, $cb); + + $this->assertSame($cb, $artifact->getCustomBinaryCallback()); + } + + public function testGetCustomBinaryCallbackReturnsNullWhenNotSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertNull($artifact->getCustomBinaryCallback()); + } + + public function testSetCustomBinaryCallbackThrowsForInvalidPlatform(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->expectException(ValidationException::class); + $artifact->setCustomBinaryCallback('invalid-platform-string', function () {}); + } + + public function testSetAndGetCustomBinaryCheckUpdateCallback(): void + { + $platform = $this->getCurrentPlatform(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setCustomBinaryCheckUpdateCallback($platform, $cb); + + $this->assertSame($cb, $artifact->getCustomBinaryCheckUpdateCallback()); + } + + public function testSetCustomBinaryCheckUpdateCallbackThrowsForInvalidPlatform(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->expectException(ValidationException::class); + $artifact->setCustomBinaryCheckUpdateCallback('bad-platform', function () {}); + } + + // ==================== Source extract callbacks ==================== + + public function testSetAndGetSourceExtractCallback(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setSourceExtractCallback($cb); + + $this->assertSame($cb, $artifact->getSourceExtractCallback()); + } + + public function testHasSourceExtractCallbackReturnsFalseWhenNotSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertFalse($artifact->hasSourceExtractCallback()); + } + + public function testHasSourceExtractCallbackReturnsTrueWhenSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $artifact->setSourceExtractCallback(function () {}); + + $this->assertTrue($artifact->hasSourceExtractCallback()); + } + + // ==================== Binary extract callbacks ==================== + + public function testSetAndGetBinaryExtractCallbackForCurrentPlatform(): void + { + $platform = $this->getCurrentPlatform(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setBinaryExtractCallback($cb, [$platform]); + + $this->assertSame($cb, $artifact->getBinaryExtractCallback()); + } + + public function testGetBinaryExtractCallbackReturnsNullForNonMatchingPlatform(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + // Register callback for a platform that is definitely NOT the current one + $otherPlatforms = array_diff(['linux-x86_64', 'linux-aarch64', 'macos-x86_64', 'macos-aarch64', 'windows-x86_64'], [$this->getCurrentPlatform()]); + $artifact->setBinaryExtractCallback(function () {}, array_values($otherPlatforms)); + + // Only returns null if none of the listed platforms match current + // Since current platform is excluded, all remaining are "other" + $this->assertNull($artifact->getBinaryExtractCallback()); + } + + public function testSetBinaryExtractCallbackWithEmptyPlatformsMatchesAll(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $cb = function () {}; + $artifact->setBinaryExtractCallback($cb, []); + + $this->assertSame($cb, $artifact->getBinaryExtractCallback()); + } + + public function testHasBinaryExtractCallbackReturnsFalseWhenNotSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->assertFalse($artifact->hasBinaryExtractCallback()); + } + + public function testHasBinaryExtractCallbackReturnsTrueWhenSet(): void + { + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $artifact->setBinaryExtractCallback(function () {}); + + $this->assertTrue($artifact->hasBinaryExtractCallback()); + } + + // ==================== After-extract callbacks ==================== + + public function testEmitAfterSourceExtractCallsAllCallbacks(): void + { + ApplicationContext::initialize(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $called1 = false; + $called2 = false; + $artifact->addAfterSourceExtractCallback(function (string $target_path) use (&$called1) { + $called1 = true; + }); + $artifact->addAfterSourceExtractCallback(function (string $target_path) use (&$called2) { + $called2 = true; + }); + + $artifact->emitAfterSourceExtract('/tmp/test-path'); + + $this->assertTrue($called1); + $this->assertTrue($called2); + } + + public function testEmitAfterBinaryExtractCallsCallbackMatchingPlatform(): void + { + ApplicationContext::initialize(); + $platform = $this->getCurrentPlatform(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $called = false; + $artifact->addAfterBinaryExtractCallback(function (string $target_path, string $platform) use (&$called) { + $called = true; + }, [$platform]); + + $artifact->emitAfterBinaryExtract('/tmp/test-path', $platform); + + $this->assertTrue($called); + } + + public function testEmitAfterBinaryExtractSkipsCallbackForNonMatchingPlatform(): void + { + ApplicationContext::initialize(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $called = false; + $artifact->addAfterBinaryExtractCallback(function () use (&$called) { + $called = true; + }, ['windows-x86_64']); + + $artifact->emitAfterBinaryExtract('/tmp/test-path', 'linux-x86_64'); + + $this->assertFalse($called); + } + + public function testEmitAfterBinaryExtractWithEmptyPlatformsCallsForAnyPlatform(): void + { + ApplicationContext::initialize(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $called = false; + $artifact->addAfterBinaryExtractCallback(function () use (&$called) { + $called = true; + }, []); + + $artifact->emitAfterBinaryExtract('/tmp/test-path', 'linux-x86_64'); + + $this->assertTrue($called); + } + + // ==================== isSourceDownloaded / isBinaryDownloaded delegation ==================== + + public function testIsSourceDownloadedDelegatesToArtifactCache(): void + { + $cache = $this->createMock(ArtifactCache::class); + $cache->expects($this->once()) + ->method('isSourceDownloaded') + ->with('my-pkg', false) + ->willReturn(true); + + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $this->assertTrue($artifact->isSourceDownloaded()); + } + + public function testIsBinaryDownloadedDelegatesToArtifactCache(): void + { + $platform = $this->getCurrentPlatform(); + $cache = $this->createMock(ArtifactCache::class); + $cache->expects($this->once()) + ->method('isBinaryDownloaded') + ->with('my-pkg', $platform, false) + ->willReturn(true); + + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $this->assertTrue($artifact->isBinaryDownloaded($platform)); + } + + // ==================== shouldUseBinary ==================== + + public function testShouldUseBinaryReturnsFalseWhenNotDownloaded(): void + { + $platform = $this->getCurrentPlatform(); + $cache = $this->createMock(ArtifactCache::class); + $cache->method('isBinaryDownloaded')->willReturn(false); + + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']], + ]); + $this->assertFalse($artifact->shouldUseBinary()); + } + + public function testShouldUseBinaryReturnsFalseWhenNoBinaryConfig(): void + { + $cache = $this->createMock(ArtifactCache::class); + $cache->method('isBinaryDownloaded')->willReturn(true); + + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + $this->assertFalse($artifact->shouldUseBinary()); + } + + public function testShouldUseBinaryReturnsTrueWhenDownloadedAndHasBinaryConfig(): void + { + $platform = $this->getCurrentPlatform(); + $cache = $this->createMock(ArtifactCache::class); + $cache->method('isBinaryDownloaded')->willReturn(true); + + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + $artifact = new Artifact('my-pkg', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + 'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']], + ]); + $this->assertTrue($artifact->shouldUseBinary()); + } + + // ==================== isSourceExtracted ==================== + + public function testIsSourceExtractedReturnsFalseWhenDirNotExists(): void + { + $cache = $this->createMock(ArtifactCache::class); + $cache->method('getSourceInfo')->willReturn(null); + + ApplicationContext::initialize(); + ApplicationContext::set(ArtifactCache::class, $cache); + + // Use an artifact whose source dir doesn't exist on disk + $artifact = new Artifact('this-pkg-does-not-exist-on-disk-2xyz', [ + 'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], + ]); + $this->assertFalse($artifact->isSourceExtracted()); + } + + // ==================== emitCustomBinary ==================== + + public function testEmitCustomBinaryThrowsWhenNoBinaryCallbackDefined(): void + { + ApplicationContext::initialize(); + $artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]); + + $this->expectException(SPCInternalException::class); + $artifact->emitCustomBinary(); + } + + // ==================== Helpers ==================== + + private function getCurrentPlatform(): string + { + $emulated = getenv('EMULATE_PLATFORM'); + if ($emulated !== false) { + return $emulated; + } + $os = match (PHP_OS_FAMILY) { + 'Darwin' => 'macos', + 'Windows' => 'windows', + default => 'linux', + }; + $arch = php_uname('m'); + if ($arch === 'arm64') { + $arch = 'aarch64'; + } + return "{$os}-{$arch}"; + } + + private function injectArtifactConfig(string $name, array $config): void + { + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $configs = $property->getValue(null) ?? []; + $configs[$name] = $config; + $property->setValue(null, $configs); + } + + /** + * Create a stub ArtifactCache that always returns null for source/binary info + * and delegates isSourceDownloaded/isBinaryDownloaded to return false. + */ + private function makeStubbedArtifactCache(array $sourceInfoMap): ArtifactCache + { + $cacheFile = $this->tempDir . '/test-cache.json'; + file_put_contents($cacheFile, json_encode([])); + return new ArtifactCache($cacheFile); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} From f327dda6157fb1aa0f1cf118d4c2cd5dd06dc913 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:29:16 +0800 Subject: [PATCH 622/682] Update composer --- composer.json | 1 - composer.lock | 355 ++++++++++++++++++++++++++++++-------------------- 2 files changed, 217 insertions(+), 139 deletions(-) diff --git a/composer.json b/composer.json index d006fbbfd..60c12aaa2 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,6 @@ }, "autoload": { "psr-4": { - "SPC\\": "src/SPC", "StaticPHP\\": "src/StaticPHP", "Package\\": "src/Package" }, diff --git a/composer.lock b/composer.lock index 69cc2e278..5df074a64 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "laravel/prompts", - "version": "v0.3.15", + "version": "v0.3.16", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "4bb8107ec97651fd3f17f897d6489dbc4d8fb999" + "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/4bb8107ec97651fd3f17f897d6489dbc4d8fb999", - "reference": "4bb8107ec97651fd3f17f897d6489dbc4d8fb999", + "url": "https://api.github.com/repos/laravel/prompts/zipball/11e7d5f93803a2190b00e145142cb00a33d17ad2", + "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2", "shasum": "" }, "require": { @@ -61,22 +61,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.15" + "source": "https://github.com/laravel/prompts/tree/v0.3.16" }, - "time": "2026-03-17T13:45:17+00:00" + "time": "2026-03-23T14:35:33+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.10", + "version": "v2.0.11", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + "reference": "d1af40ac4a6ccc12bd062a7184f63c9995a63bdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", - "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/d1af40ac4a6ccc12bd062a7184f63c9995a63bdd", + "reference": "d1af40ac4a6ccc12bd062a7184f63c9995a63bdd", "shasum": "" }, "require": { @@ -124,7 +124,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-02-20T19:59:49+00:00" + "time": "2026-04-07T13:32:18+00:00" }, { "name": "php-di/invoker", @@ -359,16 +359,16 @@ }, { "name": "symfony/console", - "version": "v7.4.7", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", "shasum": "" }, "require": { @@ -433,7 +433,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.7" + "source": "https://github.com/symfony/console/tree/v7.4.8" }, "funding": [ { @@ -453,7 +453,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:20+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/deprecation-contracts", @@ -524,16 +524,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -583,7 +583,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0" }, "funding": [ { @@ -603,20 +603,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", "shasum": "" }, "require": { @@ -665,7 +665,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0" }, "funding": [ { @@ -685,11 +685,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -750,7 +750,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0" }, "funding": [ { @@ -774,16 +774,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -835,7 +835,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0" }, "funding": [ { @@ -855,20 +855,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/process", - "version": "v7.4.5", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", + "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", "shasum": "" }, "require": { @@ -900,7 +900,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" + "source": "https://github.com/symfony/process/tree/v7.4.8" }, "funding": [ { @@ -920,7 +920,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/service-contracts", @@ -1011,16 +1011,16 @@ }, { "name": "symfony/string", - "version": "v8.0.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", "shasum": "" }, "require": { @@ -1077,7 +1077,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/string/tree/v8.0.8" }, "funding": [ { @@ -1097,20 +1097,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "58751048de17bae71c5aa0d13cb19d79bca26391" + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/58751048de17bae71c5aa0d13cb19d79bca26391", - "reference": "58751048de17bae71c5aa0d13cb19d79bca26391", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", "shasum": "" }, "require": { @@ -1153,7 +1153,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.6" + "source": "https://github.com/symfony/yaml/tree/v7.4.8" }, "funding": [ { @@ -1173,7 +1173,7 @@ "type": "tidelift" } ], - "time": "2026-02-09T09:33:46+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "zhamao/logger", @@ -1839,24 +1839,27 @@ }, { "name": "amphp/serialization", - "version": "v1.0.0", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -1891,9 +1894,15 @@ ], "support": { "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" + "source": "https://github.com/amphp/serialization/tree/v1.1.0" }, - "time": "2020-03-25T21:39:07+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" }, { "name": "amphp/socket", @@ -2056,7 +2065,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.29.0", + "version": "5.29.2", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2110,7 +2119,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.29.0" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.29.2" }, "funding": [ { @@ -2545,6 +2554,75 @@ }, "time": "2026-02-07T07:09:04+00:00" }, + { + "name": "ergebnis/agent-detector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", + "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.50.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.16.0", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-04-10T13:45:13+00:00" + }, { "name": "evenement/evenement", "version": "v3.0.2", @@ -2807,22 +2885,23 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.94.2", + "version": "v3.95.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63" + "reference": "4ba5ab77108583d2a89ed48e1a5c01e62cc1d3f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7787ceff91365ba7d623ec410b8f429cdebb4f63", - "reference": "7787ceff91365ba7d623ec410b8f429cdebb4f63", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4ba5ab77108583d2a89ed48e1a5c01e62cc1d3f4", + "reference": "4ba5ab77108583d2a89ed48e1a5c01e62cc1d3f4", "shasum": "" }, "require": { "clue/ndjson-react": "^1.3", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.1.1", "ext-filter": "*", "ext-hash": "*", "ext-json": "*", @@ -2847,18 +2926,18 @@ "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.7.1", - "infection/infection": "^0.32.3", - "justinrainbow/json-schema": "^6.6.4", + "facile-it/paraunit": "^1.3.1 || ^2.8.0", + "infection/infection": "^0.32.6", + "justinrainbow/json-schema": "^6.8.0", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.9.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.7", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.7", - "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.51", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", "symfony/polyfill-php85": "^1.33", - "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.4", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.1" + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.8" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2899,7 +2978,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.94.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.0" }, "funding": [ { @@ -2907,7 +2986,7 @@ "type": "github" } ], - "time": "2026-02-20T16:13:53+00:00" + "time": "2026-04-11T17:30:38+00:00" }, { "name": "humbug/box", @@ -3156,16 +3235,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "v6.7.2", + "version": "6.8.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", "shasum": "" }, "require": { @@ -3225,9 +3304,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.0" }, - "time": "2026-02-15T15:06:22+00:00" + "time": "2026-04-02T12:43:11+00:00" }, { "name": "kelunik/certificate", @@ -4268,11 +4347,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.42", + "version": "2.1.46", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", - "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", "shasum": "" }, "require": { @@ -4317,7 +4396,7 @@ "type": "github" } ], - "time": "2026-03-17T14:58:32+00:00" + "time": "2026-04-01T09:25:14+00:00" }, { "name": "phpunit/php-code-coverage", @@ -6572,16 +6651,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.0.4", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", - "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6", + "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6", "shasum": "" }, "require": { @@ -6633,7 +6712,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8" }, "funding": [ { @@ -6653,7 +6732,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T11:45:55+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6733,16 +6812,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", + "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", "shasum": "" }, "require": { @@ -6779,7 +6858,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.8" }, "funding": [ { @@ -6799,20 +6878,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/finder", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + "reference": "e0be088d22278583a82da281886e8c3592fbf149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", "shasum": "" }, "require": { @@ -6847,7 +6926,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.6" + "source": "https://github.com/symfony/finder/tree/v7.4.8" }, "funding": [ { @@ -6867,20 +6946,20 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:40:50+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/options-resolver", - "version": "v8.0.0", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8", + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8", "shasum": "" }, "require": { @@ -6918,7 +6997,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.8" }, "funding": [ { @@ -6938,20 +7017,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:55:31+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa" + "reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/5f3b930437ae03ae5dff61269024d8ea1b3774aa", - "reference": "5f3b930437ae03ae5dff61269024d8ea1b3774aa", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/2c5729fd241b4b22f6e4b436bc3354a4f262df57", + "reference": "2c5729fd241b4b22f6e4b436bc3354a4f262df57", "shasum": "" }, "require": { @@ -7002,7 +7081,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.34.0" }, "funding": [ { @@ -7022,20 +7101,20 @@ "type": "tidelift" } ], - "time": "2024-09-17T14:58:18+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { @@ -7082,7 +7161,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.34.0" }, "funding": [ { @@ -7102,20 +7181,20 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { "name": "symfony/stopwatch", - "version": "v8.0.0", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" + "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", - "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/85954ed72d5440ea4dc9a10b7e49e01df766ffa3", + "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3", "shasum": "" }, "require": { @@ -7148,7 +7227,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.8" }, "funding": [ { @@ -7168,20 +7247,20 @@ "type": "tidelift" } ], - "time": "2025-08-04T07:36:47+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.6", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", "shasum": "" }, "require": { @@ -7235,7 +7314,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" }, "funding": [ { @@ -7255,7 +7334,7 @@ "type": "tidelift" } ], - "time": "2026-02-15T10:53:20+00:00" + "time": "2026-03-30T13:44:50+00:00" }, { "name": "thecodingmachine/safe", From 6ee4759444c339b776b725d483783ff113998045 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:32:43 +0800 Subject: [PATCH 623/682] PhPStAN fIX --- phpstan.neon | 1 - src/Package/Target/php/windows.php | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index ce7cdb2d2..f54fc35fc 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -20,4 +20,3 @@ parameters: analyseAndScan: - ./src/globals/ext-tests/ - ./src/globals/test-extensions.php - - ./src/SPC/ diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 47eb3c9f5..561628f0a 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -698,6 +698,7 @@ protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $p $output_label = match ($sapi) { 'php-cli' => 'Binary path for cli SAPI', 'php-cgi' => 'Binary path for cgi SAPI', + /* @phpstan-ignore-next-line */ 'php-micro' => 'Binary path for micro SAPI', default => null, }; From cc713069c21b17390d69f85f2e4793c44896f775 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:36:12 +0800 Subject: [PATCH 624/682] Remove unnecessary TODO mark --- src/StaticPHP/Util/GlobalPathTrait.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/StaticPHP/Util/GlobalPathTrait.php b/src/StaticPHP/Util/GlobalPathTrait.php index f94c501bc..a047bfc08 100644 --- a/src/StaticPHP/Util/GlobalPathTrait.php +++ b/src/StaticPHP/Util/GlobalPathTrait.php @@ -8,8 +8,6 @@ trait GlobalPathTrait { /** * Get the build root path for the package. - * - * TODO: Can be changed to support per-package build root path in the future. */ public function getBuildRootPath(): string { @@ -18,8 +16,6 @@ public function getBuildRootPath(): string /** * Get the include directory for the package. - * - * TODO: Can be changed to support per-package include directory in the future. */ public function getIncludeDir(): string { @@ -28,8 +24,6 @@ public function getIncludeDir(): string /** * Get the library directory for the package. - * - * TODO: Can be changed to support per-package library directory in the future. */ public function getLibDir(): string { From d2f007d4c41b98a22ec34b206b9e751bc09754c0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 14:17:53 +0800 Subject: [PATCH 625/682] Forward-port #1095 --- config/pkg/ext/ext-decimal.yml | 16 +++++++++++ config/pkg/lib/libmpdec.yml | 15 ++++++++++ src/Package/Extension/decimal.php | 29 +++++++++++++++++++ src/Package/Library/libmpdec.php | 46 +++++++++++++++++++++++++++++++ src/globals/ext-tests/decimal.php | 10 +++++++ 5 files changed, 116 insertions(+) create mode 100644 config/pkg/ext/ext-decimal.yml create mode 100644 config/pkg/lib/libmpdec.yml create mode 100644 src/Package/Extension/decimal.php create mode 100644 src/Package/Library/libmpdec.php create mode 100644 src/globals/ext-tests/decimal.php diff --git a/config/pkg/ext/ext-decimal.yml b/config/pkg/ext/ext-decimal.yml new file mode 100644 index 000000000..da5419115 --- /dev/null +++ b/config/pkg/ext/ext-decimal.yml @@ -0,0 +1,16 @@ +ext-decimal: + type: php-extension + artifact: + source: + type: ghtagtar + repo: php-decimal/ext-decimal + match: 'v2\.\d.*' + extract: php-src/ext/decimal + metadata: + license-files: [LICENSE] + license: MIT + depends: + - libmpdec + php-extension: + arg-type@unix: '--enable-decimal --with-libmpdec-path=@build_root_path@' + arg-type@windows: '--with-decimal' diff --git a/config/pkg/lib/libmpdec.yml b/config/pkg/lib/libmpdec.yml new file mode 100644 index 000000000..adb9e5318 --- /dev/null +++ b/config/pkg/lib/libmpdec.yml @@ -0,0 +1,15 @@ +libmpdec: + type: library + artifact: + source: + type: url + url: 'https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-4.0.1.tar.gz' + metadata: + license-files: [COPYRIGHT.txt] + license: BSD-2-Clause + headers: + - mpdecimal.h + static-libs@unix: + - libmpdec.a + static-libs@windows: + - libmpdec_a.lib diff --git a/src/Package/Extension/decimal.php b/src/Package/Extension/decimal.php new file mode 100644 index 000000000..147f7388e --- /dev/null +++ b/src/Package/Extension/decimal.php @@ -0,0 +1,29 @@ +getSourceDir() . '/php_decimal.c', + 'zend_module_entry decimal_module_entry', + 'zend_module_entry php_decimal_module_entry' + ); + } +} diff --git a/src/Package/Library/libmpdec.php b/src/Package/Library/libmpdec.php new file mode 100644 index 000000000..05b2df040 --- /dev/null +++ b/src/Package/Library/libmpdec.php @@ -0,0 +1,46 @@ +configure('--disable-cxx --disable-shared --enable-static') + ->make(); + } + + #[BuildFor('Windows')] + public function buildWin(LibraryPackage $lib): void + { + $makefileDir = $lib->getSourceDir() . DIRECTORY_SEPARATOR . 'libmpdec'; + + cmd()->cd($makefileDir) + ->exec('copy /y Makefile.vc Makefile') + ->exec('nmake /nologo clean') + ->exec('nmake /nologo MACHINE=x64'); + + // Copy static lib (rename from versioned name to libmpdec_a.lib) + $libs = glob($makefileDir . DIRECTORY_SEPARATOR . 'libmpdec-*.lib'); + foreach ($libs as $libFile) { + if (!str_contains($libFile, '.dll.')) { + FileSystem::copy($libFile, $lib->getLibDir() . DIRECTORY_SEPARATOR . 'libmpdec_a.lib'); + break; + } + } + + FileSystem::copy($makefileDir . DIRECTORY_SEPARATOR . 'mpdecimal.h', $lib->getIncludeDir() . DIRECTORY_SEPARATOR . 'mpdecimal.h'); + } +} diff --git a/src/globals/ext-tests/decimal.php b/src/globals/ext-tests/decimal.php new file mode 100644 index 000000000..54f660fbb --- /dev/null +++ b/src/globals/ext-tests/decimal.php @@ -0,0 +1,10 @@ + Date: Sun, 12 Apr 2026 16:05:06 +0800 Subject: [PATCH 626/682] Enhance debug flag handling in Makefile for release builds --- src/Package/Target/php/windows.php | 50 +++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 561628f0a..cffe04dc5 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -140,14 +140,19 @@ public function makeCliForWindows(TargetPackage $package, PackageBuilder $builde // 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 + // Read current CFLAGS and LDFLAGS from Makefile $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" '; + // Append debug flags to existing LDFLAGS to preserve /libpath entries set by configure + $ldflags = ''; + if (preg_match('/^LDFLAGS=(.+?)$/m', $makefile_content, $lm)) { + $ldflags = trim($lm[1]) . ' '; + } + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=' . $ldflags . '/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CLI=/DEBUG" '; } } @@ -209,7 +214,12 @@ public function makeCgiForWindows(TargetPackage $package, PackageBuilder $builde if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { $cflags = $matches[1]; $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; + // Append debug flags to existing LDFLAGS to preserve /libpath entries set by configure + $ldflags = ''; + if (preg_match('/^LDFLAGS=(.+?)$/m', $makefile_content, $lm)) { + $ldflags = trim($lm[1]) . ' '; + } + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=' . $ldflags . '/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; } } @@ -247,6 +257,23 @@ public function makeMicroForWindows(TargetPackage $package, PackageBuilder $buil // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) $makefile = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); + + // Add debug symbols for release build if --no-strip is specified. + // Extract CFLAGS/LDFLAGS here, before fiber content is appended, to ensure the regex works on clean content. + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + // Append debug flags to existing LDFLAGS to preserve /libpath entries set by configure + $ldflags = ''; + if (preg_match('/^LDFLAGS=(.+?)$/m', $makefile, $lm)) { + $ldflags = trim($lm[1]) . ' '; + } + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=' . $ldflags . '/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; + } + } + if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; } elseif ($this->getPHPVersionID() >= 80400 && str_contains($makefile, 'FIBER_ASM_ABI')) { @@ -267,16 +294,6 @@ public function makeMicroForWindows(TargetPackage $package, PackageBuilder $buil // extra lib $extra_libs = trim((getenv('SPC_EXTRA_LIBS') ?: '') . ' ' . implode(' ', $resolved_libs)); - // Add debug symbols for release build if --no-strip is specified - $debug_overrides = ''; - if ($package->getBuildOption('no-strip', false)) { - $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); - if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { - $cflags = $matches[1]; - $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; - } - } $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; @@ -387,7 +404,12 @@ public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $buil if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { $cflags = $matches[1]; $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); - $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" '; + // Append debug flags to existing LDFLAGS to preserve /libpath entries set by configure + $ldflags = ''; + if (preg_match('/^LDFLAGS=(.+?)$/m', $makefile_content, $lm)) { + $ldflags = trim($lm[1]) . ' '; + } + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=' . $ldflags . '/DEBUG /LTCG /INCREMENTAL:NO" '; } } From 75a7b21a6ff15ccbbe3dea5abf43af141f25dc59 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 16:05:20 +0800 Subject: [PATCH 627/682] Add patch to ensure ext/json MINIT runs before ext/decimal on Windows static builds --- src/Package/Extension/decimal.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Package/Extension/decimal.php b/src/Package/Extension/decimal.php index 147f7388e..c5059e2ef 100644 --- a/src/Package/Extension/decimal.php +++ b/src/Package/Extension/decimal.php @@ -26,4 +26,16 @@ public function patchBeforeBuildconf(): void 'zend_module_entry php_decimal_module_entry' ); } + + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-decimal')] + #[PatchDescription('Ensure ext/json MINIT runs before ext/decimal on Windows static builds')] + public function patchConfigW32(): void + { + FileSystem::replaceFileStr( + $this->getSourceDir() . '/config.w32', + 'ARG_WITH("decimal", "for decimal support", "no");', + 'ARG_WITH("decimal", "for decimal support", "no");' . "\n" . + 'ADD_EXTENSION_DEP("decimal", "json");' + ); + } } From 60d206cac09f454c4bd01cc8ac16f18e376ac6fa Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:11:53 +0800 Subject: [PATCH 628/682] Add go-win --- config/pkg/target/go-win.yml | 10 ++++ src/Package/Artifact/go_win.php | 85 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 config/pkg/target/go-win.yml create mode 100644 src/Package/Artifact/go_win.php diff --git a/config/pkg/target/go-win.yml b/config/pkg/target/go-win.yml new file mode 100644 index 000000000..46c13f5f1 --- /dev/null +++ b/config/pkg/target/go-win.yml @@ -0,0 +1,10 @@ +go-win: + type: target + artifact: + binary: custom + env: + GOROOT: '{pkg_root_path}/go-win' + GOBIN: '{pkg_root_path}/go-win/bin' + GOPATH: '{pkg_root_path}/go-win/go' + path@windows: + - '{pkg_root_path}/go-win/bin' diff --git a/src/Package/Artifact/go_win.php b/src/Package/Artifact/go_win.php new file mode 100644 index 000000000..44cf61b96 --- /dev/null +++ b/src/Package/Artifact/go_win.php @@ -0,0 +1,85 @@ +executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + + // find SHA256 hash from download page + $page = default_shell()->executeCurl('https://go.dev/dl/'); + if ($page === '' || $page === false) { + throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); + } + + $version_regex = str_replace('.', '\.', $version); + $pattern = "/class=\"download\" href=\"\\/dl\\/{$version_regex}\\.windows-amd64\\.zip\">.*?([a-f0-9]{64})<\\/tt>/s"; + if (preg_match($pattern, $page, $matches)) { + $hash = $matches[1]; + } else { + throw new DownloaderException("Failed to find download hash for Go {$version} windows-amd64"); + } + + $url = "https://go.dev/dl/{$version}.windows-amd64.zip"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.windows-amd64.zip"; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + + // verify hash + $file_hash = hash_file('sha256', $path); + if ($file_hash !== $hash) { + throw new DownloaderException("Hash mismatch for downloaded go-win binary. Expected {$hash}, got {$file_hash}"); + } + + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-win", verified: true, version: $version); + } + + #[CustomBinaryCheckUpdate('go-win', ['windows-x86_64'])] + public function checkUpdateBinary(?string $old_version): CheckUpdateResult + { + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + + #[AfterBinaryExtract('go-win', ['windows-x86_64'])] + public function afterExtract(string $target_path): void + { + if (!file_exists("{$target_path}\\bin\\go.exe")) { + throw new DownloaderException("Go installation appears incomplete: go.exe not found at {$target_path}\\bin\\go.exe"); + } + + GlobalEnvManager::putenv("GOROOT={$target_path}"); + GlobalEnvManager::putenv("GOPATH={$target_path}\\gopath"); + GlobalEnvManager::putenv("GOCACHE={$target_path}\\gocache"); + GlobalEnvManager::putenv("GOMODCACHE={$target_path}\\gopath\\pkg\\mod"); + GlobalEnvManager::addPathIfNotExists("{$target_path}\\bin"); + } +} From be034b756baed6ee3f69f2a6478226f7ec0ec09e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:12:11 +0800 Subject: [PATCH 629/682] Allow detect static-bins for windows --- src/StaticPHP/Package/LibraryPackage.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 769612b9f..fccb84ce8 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -53,11 +53,12 @@ public function isInstalled(): bool return false; } } - foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { - $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; - if (!file_exists($path)) { - return false; - } + } + + foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { + $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; + if (!file_exists($path)) { + return false; } } return true; From 0dbbf7abb248a8056e73f1c757a47c041736c4a7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:12:26 +0800 Subject: [PATCH 630/682] Allow all packages has output --- src/StaticPHP/Package/PackageInstaller.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 1f778a0d4..fa049bbd7 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -133,7 +133,8 @@ public function getTracker(): ?BuildRootTracker public function printBuildPackageOutputs(): void { - foreach ($this->build_packages as $package) { + /** @var Package $package */ + foreach ($this->getResolvedPackages() as $package) { if (($outputs = $package->getOutputs()) !== []) { InteractiveTerm::notice('Package ' . ConsoleColor::green($package->getName()) . ' outputs'); $this->printArrayInfo(info: $outputs); @@ -685,6 +686,7 @@ private function handlePhpTargetPackage(TargetPackage $package): void if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { $frankenphp = PackageLoader::getPackage('frankenphp'); $this->install_packages[$frankenphp->getName()] = $frankenphp; + $this->build_packages[$package->getName()] = $package; $added = true; } $this->build_packages[$package->getName()] = $package; From ffc677e4b396f0d25a7af45f92cf2a60a133b23f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:13:07 +0800 Subject: [PATCH 631/682] chore --- src/Package/Target/php.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 29b6848a9..36427a27c 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -161,6 +161,7 @@ public function init(TargetPackage $package): void // embed build options if ($package->getName() === 'php' || $package->getName() === 'php-embed') { $package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); + $package->addBuildOption('maintainer-skip-build', null, null, '(maintainer only) skip embed build if exists'); } // legacy php target build options @@ -265,10 +266,6 @@ public function validate(Package $package): void if (!$package->getBuildOption('enable-zts')) { throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); } - // frankenphp doesn't support windows, BSD is currently not supported by StaticPHP - if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { - throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); - } } // linux does not support loading shared libraries when target is pure static $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; From f4918ba92c3b475f79f3285d3dfca4144b0bfb27 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:13:30 +0800 Subject: [PATCH 632/682] Add pthreadVC3.lib for frankenphp --- config/pkg/lib/pthreads4w.yml | 1 + src/Package/Library/pthreads4w.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/config/pkg/lib/pthreads4w.yml b/config/pkg/lib/pthreads4w.yml index 06a6245f8..5c8eabace 100644 --- a/config/pkg/lib/pthreads4w.yml +++ b/config/pkg/lib/pthreads4w.yml @@ -10,3 +10,4 @@ pthreads4w: license: Apache-2.0 static-libs@windows: - libpthreadVC3.lib + - pthreadVC3.lib diff --git a/src/Package/Library/pthreads4w.php b/src/Package/Library/pthreads4w.php index 69f181f0b..6a263d4e2 100644 --- a/src/Package/Library/pthreads4w.php +++ b/src/Package/Library/pthreads4w.php @@ -26,6 +26,8 @@ public function buildWin(LibraryPackage $lib): void FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); FileSystem::copy("{$lib->getSourceDir()}\\libpthreadVC3.lib", "{$lib->getLibDir()}\\libpthreadVC3.lib"); + // FrankenPHP's cgo.go uses -lpthreadVC3, which lld-link maps to pthreadVC3.lib (no lib prefix) + FileSystem::copy("{$lib->getSourceDir()}\\libpthreadVC3.lib", "{$lib->getLibDir()}\\pthreadVC3.lib"); FileSystem::copy("{$lib->getSourceDir()}\\_ptw32.h", "{$lib->getIncludeDir()}\\_ptw32.h"); FileSystem::copy("{$lib->getSourceDir()}\\pthread.h", "{$lib->getIncludeDir()}\\pthread.h"); FileSystem::copy("{$lib->getSourceDir()}\\sched.h", "{$lib->getIncludeDir()}\\sched.h"); From f8ed1aa86e12cda137eabf277b84678263421fa4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:17:16 +0800 Subject: [PATCH 633/682] Fix unpassed env for windows cmd --- src/StaticPHP/Runtime/Shell/WindowsCmd.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index ad07f93bd..4c7af181d 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -27,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, cwd: $this->cd); + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd, env: $this->env); return $this; } From 4ddc137eae85b031fee16e4189f8d3d86e2bb871 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:17:33 +0800 Subject: [PATCH 634/682] Add clang finder for Windows --- src/StaticPHP/Util/System/WindowsUtil.php | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index 150730c0a..406372625 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -85,6 +85,44 @@ public static function getCpuCount(): int return intval($result); } + /** + * Find Clang compiler from the Visual Studio LLVM toolchain. + * + * Checks the CC environment variable first (user override), then searches + * the VS2022/VS2019 installation via vswhere. + * + * @return array{clang: string, clangpp: string}|false False if not found + */ + public static function findClang(): array|false + { + // Allow user to override via CC environment variable + if ($cc = getenv('CC')) { + if (file_exists($cc)) { + $clangpp = dirname($cc) . DIRECTORY_SEPARATOR . 'clang++.exe'; + return [ + 'clang' => $cc, + 'clangpp' => file_exists($clangpp) ? $clangpp : $cc, + ]; + } + } + + $vs = self::findVisualStudio(); + if ($vs === false) { + return false; + } + + $clang = $vs['dir'] . '\VC\Tools\Llvm\x64\bin\clang.exe'; + $clangpp = $vs['dir'] . '\VC\Tools\Llvm\x64\bin\clang++.exe'; + if (!file_exists($clang)) { + return false; + } + + return [ + 'clang' => $clang, + 'clangpp' => file_exists($clangpp) ? $clangpp : $clang, + ]; + } + /** * Create CMake toolchain file. * From 8cc5c8259525b249f50f222e1f25e356e28110b3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:17:50 +0800 Subject: [PATCH 635/682] Add frankenphp build --- config/pkg/target/frankenphp.yml | 8 ++ src/Package/Target/php.php | 6 +- src/Package/Target/php/frankenphp.php | 196 ++++++++++++++++++++++++++ src/Package/Target/php/windows.php | 52 ++++++- 4 files changed, 256 insertions(+), 6 deletions(-) diff --git a/config/pkg/target/frankenphp.yml b/config/pkg/target/frankenphp.yml index 96f9c082a..21d47448c 100644 --- a/config/pkg/target/frankenphp.yml +++ b/config/pkg/target/frankenphp.yml @@ -11,8 +11,16 @@ frankenphp: depends: - php-embed - go-xcaddy + depends@windows: + - php-embed + - go-win + - pthreads4w suggests@unix: - brotli - watcher + suggests@windows: + - brotli static-bins@unix: - frankenphp + static-bins@windows: + - frankenphp.exe diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 36427a27c..e2513cb45 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -342,7 +342,11 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void public function postInstall(TargetPackage $package, PackageInstaller $installer): void { if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + if (SystemTarget::getTargetOS() === 'Windows') { + $package->runStage([$this, 'smokeTestFrankenphpForWindows']); + } else { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + } return; } if ($package->getName() !== 'php') { diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index f513242b2..05a32f33f 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -6,9 +6,12 @@ use Package\Target\php; use StaticPHP\Attribute\Package\Stage; +use StaticPHP\Config\PackageConfig; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\TargetPackage; @@ -18,6 +21,7 @@ use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\SPCConfigUtil; use StaticPHP\Util\System\LinuxUtil; +use StaticPHP\Util\System\WindowsUtil; use ZM\Logger\ConsoleColor; trait frankenphp @@ -171,6 +175,198 @@ public function processFrankenphpApp(TargetPackage $package): void } } + #[Stage] + public function buildFrankenphpForWindows(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + if (getenv('GOROOT') === false) { + throw new EnvironmentException('go-win is not initialized properly. GOROOT is not set.'); + } + + $clang_info = WindowsUtil::findClang(); + if ($clang_info === false) { + throw new EnvironmentException( + 'Clang not found. FrankenPHP Windows build requires the LLVM toolchain component of Visual Studio. ' . + 'Install it in Visual Studio Installer under "C++ Clang tools for Windows", or set the CC environment variable.' + ); + } + + $frankenphp_version = $this->getFrankenPHPVersion($package); + $libphp_version = php::getPHPVersion(); + $major = intdiv(PHP_VERSION_ID, 10000); + $source_dir = $package->getSourceDir(); + + // collect PHP include paths in clang -I format (not MSVC /I). + // Use forward slashes and NO quotes around paths: when Go passes CGO_CFLAGS tokens + // directly to clang via exec(), any embedded quotes become literal characters in + // the argument string and break include-path resolution. + $include = str_replace('\\', '/', BUILD_INCLUDE_PATH); + // The PHP source root is needed so that Windows-only headers installed only in + // the source tree (e.g. win32/ioutil.h, win32/winutil.h) can be found via their + // relative #include paths like `#include "win32/ioutil.h"`. + $php_src = str_replace('\\', '/', SOURCE_PATH . '/php-src'); + $cgo_cflags = implode(' ', [ + "-I{$include}", + "-I{$include}/php", + "-I{$include}/php/main", + "-I{$include}/php/Zend", + "-I{$include}/php/TSRM", + "-I{$include}/php/ext", + "-I{$php_src}", + "-I{$php_src}/main", + "-I{$php_src}/ext", + "-I{$php_src}/Zend", + "-I{$php_src}/TSRM", + "-DFRANKENPHP_VERSION={$frankenphp_version}", + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + ]); + + $dep_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $lib_name = preg_replace('/\.lib$/i', '', $lib_file); + $dep_libs[] = "-l{$lib_name}"; + } + } + } + + $dep_libs = array_unique($dep_libs); + $lib_dir = str_replace('\\', '/', BUILD_LIB_PATH); + $php_embed_lib = "-lphp{$major}embed"; + $win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt'; + $cgo_ldflags = clean_spaces(implode(' ', array_filter([ + "-L{$lib_dir}", + $php_embed_lib, + implode(' ', $dep_libs), + $win_sys_libs, + '-llibcmt', + '-Wl,/NODEFAULTLIB:msvcrt', + '-Wl,/NODEFAULTLIB:msvcrtd', + '-Wl,/FORCE:MULTIPLE', + ]))); + + // build tags: skip watcher (no inotify/kqueue on Windows) + $go_build_tags = 'nobadger,nomysql,nopgx,nowatcher'; + if (!$installer->isPackageResolved('brotli')) { + $go_build_tags .= ',nobrotli'; + } + + $go_ldflags = + '-extldflags=-fuse-ld=lld ' . + "-X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy' " . + "-X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' " . + "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP v{$frankenphp_version} PHP {$libphp_version} Caddy'"; + + // CGO on Windows tokenizes CC/CXX like a shell command line, splitting on spaces. + // Paths like "C:\Program Files\..." break because only "C:\Program" is used. + // Fix: prepend clang's directory to PATH and use plain executable names instead, + // which matches FrankenPHP's official CI approach (CC=clang, CXX=clang++). + $clang_dir = dirname($clang_info['clang']); + $env = [ + 'CGO_ENABLED' => '1', + 'CC' => 'clang.exe', + 'CXX' => 'clang++.exe', + 'PATH' => $clang_dir . ';' . getenv('PATH'), + 'CGO_CFLAGS' => clean_spaces($cgo_cflags), + 'CGO_LDFLAGS' => $cgo_ldflags, + ]; + + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('embedding Windows metadata')); + $package->runStage([$this, 'embedFrankenphpWindowsMetadata']); + + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with go build')); + + cmd()->cd("{$source_dir}\\caddy\\frankenphp") + ->setEnv($env) + ->exec("go build -v -tags \"{$go_build_tags}\" -ldflags \"{$go_ldflags}\" -o frankenphp.exe ."); + + $builder->deployBinary("{$source_dir}\\caddy\\frankenphp\\frankenphp.exe", BUILD_BIN_PATH . '\frankenphp.exe'); + $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '\frankenphp.exe'); + } + + /** + * Embed Windows PE metadata (version info + icon) into resource.syso so that + * go build picks it up automatically. Mirrors the official FrankenPHP Windows CI. + */ + #[Stage] + public function embedFrankenphpWindowsMetadata(TargetPackage $package): void + { + $frankenphp_version = $this->getFrankenPHPVersion($package); + $source_dir = $package->getSourceDir(); + $build_dir = "{$source_dir}\\caddy\\frankenphp"; + + // Parse version components for the FixedFileInfo block. + $parts = explode('.', $frankenphp_version); + $major = (int) ($parts[0] ?? 0); + $minor = (int) ($parts[1] ?? 0); + $patch = (int) ($parts[2] ?? 0); + + $version_info = [ + 'FixedFileInfo' => [ + 'FileVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0], + 'ProductVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0], + ], + 'StringFileInfo' => [ + 'CompanyName' => 'FrankenPHP', + 'FileDescription' => 'The modern PHP app server', + 'FileVersion' => $frankenphp_version, + 'InternalName' => 'frankenphp', + 'OriginalFilename' => 'frankenphp.exe', + 'LegalCopyright' => '(c) 2022 Kévin Dunglas, MIT License', + 'ProductName' => 'FrankenPHP', + 'ProductVersion' => $frankenphp_version, + 'Comments' => 'https://frankenphp.dev/', + ], + 'VarFileInfo' => [ + 'Translation' => ['LangID' => 9, 'CharsetID' => 1200], + ], + ]; + + file_put_contents("{$build_dir}\\versioninfo.json", json_encode($version_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + // Install goversioninfo if not already installed. + // GOPATH is set by the go-win artifact initializer via GlobalEnvManager::putenv(). + $goversioninfo = getenv('GOROOT') . '\bin\goversioninfo.exe'; + if (!file_exists($goversioninfo)) { + cmd()->exec('go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest'); + } + + // -64: embed as 64-bit resource; -icon: relative path from the build dir to the repo root icon. + cmd()->cd($build_dir) + ->exec("\"{$goversioninfo}\" -64 -icon {$package->getSourceDir()}\\frankenphp.ico versioninfo.json -o resource.syso"); + } + + #[Stage] + public function smokeTestFrankenphpForWindows(PackageBuilder $builder): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test', false); + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,... + }; + if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) { + return; + } + + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); + $frankenphp = BUILD_BIN_PATH . '\frankenphp.exe'; + if (!file_exists($frankenphp)) { + throw new ValidationException( + "FrankenPHP binary not found: {$frankenphp}", + validation_module: 'FrankenPHP smoke test' + ); + } + [$ret, $output] = cmd()->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new ValidationException( + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', + validation_module: 'FrankenPHP smoke test' + ); + } + } + protected function getFrankenPHPVersion(TargetPackage $package): string { if ($version = getenv('FRANKENPHP_VERSION')) { diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index cffe04dc5..8ab7b0191 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -447,12 +447,26 @@ public function makeEmbedForWindows(TargetPackage $package, PackageBuilder $buil } #[BuildFor('Windows')] - public function buildWin(TargetPackage $package): void + public function buildWin(TargetPackage $package, PackageInstaller $installer): void { + if ($package->getName() === 'frankenphp') { + /* @var php $this */ + $package->runStage([$this, 'buildFrankenphpForWindows']); + return; + } if ($package->getName() !== 'php') { return; } + // maintainer can skip build though ... + if ( + $installer->isPackageResolved('php-embed') + && $installer->getTargetPackage('php-embed')->getBuildOption('maintainer-skip-build') + && file_exists(BUILD_LIB_PATH . '\php8embed.lib') + ) { + return; + } + $package->runStage([$this, 'buildconfForWindows']); $package->runStage([$this, 'configureForWindows']); $package->runStage([$this, 'makeForWindows']); @@ -467,6 +481,32 @@ public function patchBeforeBuildconfForWindows(TargetPackage $package): void // php-src patches from micro SourcePatcher::patchPhpSrc(); + /* wsyslog.h is generated by mc.exe from win32/build/wsyslog.mc but is absent in some + PHP tarballs (e.g. 8.4.x). wsyslog.c still #includes it for the PHP_SYSLOG_*_TYPE + event-ID constants. Recreate the missing header with the correct mc.exe-encoded values: + MessageId=N + Severity bits (Success=0x00, Info=0x40, Warning=0x80, Error=0xC0) + combined into a 32-bit DWORD (Facility=0, Customer=0). + */ + $wsyslog_h = "{$package->getSourceDir()}\\win32\\wsyslog.h"; + if (!file_exists($wsyslog_h)) { + $shim = <<<'HEADER' +/* Auto-generated compatibility shim: wsyslog.h (from win32/build/wsyslog.mc) */ +#ifndef WSYSLOG_H +#define WSYSLOG_H + +#include "syslog.h" + +/* Event IDs generated by mc.exe from wsyslog.mc (Facility=0, Customer=0) */ +#define PHP_SYSLOG_SUCCESS_TYPE ((DWORD)0x00000001L) +#define PHP_SYSLOG_INFO_TYPE ((DWORD)0x40000002L) +#define PHP_SYSLOG_WARNING_TYPE ((DWORD)0x80000003L) +#define PHP_SYSLOG_ERROR_TYPE ((DWORD)0xC0000004L) + +#endif /* WSYSLOG_H */ +HEADER; + FileSystem::writeFile($wsyslog_h, $shim); + } + // php 8.1 bug if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) { logger()->info('Patching PHP 8.1 windows Fiber bug'); @@ -656,22 +696,24 @@ public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPack $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; - // Build include flags pointing to source dirs (like PHP Windows build does) // Note: embed.c uses #include , so we need $source_dir itself + $zts_define = $ts ? ' /D ZTS=1' : ''; $include_flags = sprintf( '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . - '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', + '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES%s', $build_dir, $source_dir, $source_dir, $source_dir, - $source_dir + $source_dir, + $zts_define ); // MSVC cl.exe format: compiler flags must come before /link, linker flags after // ldflags contains /LIBPATH which must be after /link + // /FORCE:MULTIPLE: in ZTS mode both zend.obj and php_embed.obj (both packed into the fat php8embed.lib) define _tsrm_ls_cache as a __declspec(thread) variable. $compile_cmd = sprintf( - 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', + 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /LIBPATH:"%s\lib" %s %s', $include_flags, BUILD_ROOT_PATH, $config['libs'], From 5d76e0b6cb6a5100349871151ab54ddc02b23601 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 23:20:38 +0800 Subject: [PATCH 636/682] phpstan --- src/Package/Target/php/frankenphp.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 05a32f33f..4414af665 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -295,11 +295,10 @@ public function embedFrankenphpWindowsMetadata(TargetPackage $package): void $source_dir = $package->getSourceDir(); $build_dir = "{$source_dir}\\caddy\\frankenphp"; - // Parse version components for the FixedFileInfo block. - $parts = explode('.', $frankenphp_version); - $major = (int) ($parts[0] ?? 0); - $minor = (int) ($parts[1] ?? 0); - $patch = (int) ($parts[2] ?? 0); + [$p1, $p2, $p3] = explode('.', $frankenphp_version); + $major = (int) $p1; + $minor = (int) $p2; + $patch = (int) $p3; $version_info = [ 'FixedFileInfo' => [ From 9fc2b64b6ba1694111b6a9831a024d7b699b125f Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Mon, 13 Apr 2026 08:21:04 +0800 Subject: [PATCH 637/682] Update src/Package/Library/gettext.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Library/gettext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/gettext.php b/src/Package/Library/gettext.php index 7af69905e..e30bd160f 100644 --- a/src/Package/Library/gettext.php +++ b/src/Package/Library/gettext.php @@ -36,7 +36,7 @@ public function build(LibraryPackage $pkg, PackageBuilder $builder): void $autoconf->addConfigureArgs('--enable-threads=isoc+posix') ->appendEnv([ 'CFLAGS' => '-lpthread -D_REENTRANT', - 'LDFLGAS' => '-lpthread', + 'LDFLAGS' => '-lpthread', ]); } else { $autoconf->addConfigureArgs('--disable-threads'); From d37e23218b4aab0dce7baadda6f1eb979af6aee3 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Mon, 13 Apr 2026 08:23:33 +0800 Subject: [PATCH 638/682] Update src/Package/Artifact/attr.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Artifact/attr.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Artifact/attr.php b/src/Package/Artifact/attr.php index 9974a4a23..23f6282e9 100644 --- a/src/Package/Artifact/attr.php +++ b/src/Package/Artifact/attr.php @@ -17,7 +17,7 @@ class attr #[PatchDescription('Patch attr for Alpine Linux (musl) and macOS - gethostname declaration')] public function patchAttrForAlpine(Artifact $artifact): void { - spc_skip_unless(SystemTarget::getTargetOS() === 'Darwin' || SystemTarget::getTargetOS() === 'Linux' && !LinuxUtil::isMuslDist(), 'Only for Alpine Linux (musl) and macOS'); + spc_skip_unless(SystemTarget::getTargetOS() === 'Darwin' || SystemTarget::getTargetOS() === 'Linux' && LinuxUtil::isMuslDist(), 'Only for Alpine Linux (musl) and macOS'); SourcePatcher::patchFile('attr_alpine_gethostname.patch', $artifact->getSourceDir()); } } From 79d0cd4d19c24db9693ec3c5b4b511373d6adb4b Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Mon, 13 Apr 2026 08:24:04 +0800 Subject: [PATCH 639/682] Update src/Package/Extension/swow.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Extension/swow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Extension/swow.php b/src/Package/Extension/swow.php index 884f0217a..3ebe3d582 100644 --- a/src/Package/Extension/swow.php +++ b/src/Package/Extension/swow.php @@ -40,6 +40,6 @@ public function patchBeforeBuildconf(PackageInstaller $installer): bool } // replace AC_DEFUN([SWOW_PKG_CHECK_MODULES] to AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC] FileSystem::replaceFileStr($this->getSourceDir() . '/ext/config.m4', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES]', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC]'); - return false; + return true; } } From 3816b94a9b66e954461e1efe80c6dbbf1039f8b2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 10:21:10 +0800 Subject: [PATCH 640/682] Update README.md --- README-zh.md | 322 ++++++++++++++++++++++++--------------------------- README.md | 85 +++++--------- 2 files changed, 181 insertions(+), 226 deletions(-) diff --git a/README-zh.md b/README-zh.md index 8dc8d0a3e..d75d07938 100755 --- a/README-zh.md +++ b/README-zh.md @@ -1,172 +1,150 @@ -# StaticPHP - -[![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) -[![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) -[![Releases](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/releases) -[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) -[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) - -**StaticPHP** 是一个用于构建静态编译可执行文件(包括 PHP、扩展等)的强大工具。 - -## 特性 - -- :elephant: **支持多 PHP 版本** - 支持 PHP 8.1, 8.2, 8.3, 8.4, 8.5 -- :handbag: **单文件 PHP 可执行文件** - 构建零依赖的独立 PHP -- :hamburger: **phpmicro 集成** - 构建 **[phpmicro](https://github.com/dixyes/phpmicro)** 自解压可执行文件(将 PHP 二进制文件和源代码合并为一个文件) -- :pill: **智能环境检查器** - 自动构建环境检查器,具备自动修复功能 -- :zap: **跨平台支持** - 支持 Linux、macOS、FreeBSD 和 Windows -- :wrench: **可配置补丁** - 可自定义的源代码补丁系统 -- :books: **智能依赖管理** - 自动处理构建依赖 -- 📦 **自包含工具** - 提供使用 [box](https://github.com/box-project/box) 构建的 `spc` 可执行文件 -- :fire: **广泛的扩展支持** - 支持 75+ 流行 [扩展](https://static-php.dev/zh/guide/extensions.html) -- :floppy_disk: **UPX 压缩** - 减小二进制文件大小 30-50%(仅 Linux/Windows) - -**单文件独立 php-cli:** - -out1 - -**使用 phpmicro 将 PHP 代码与 PHP 解释器结合:** - -out2 - -## 快速开始 - -### 1. 下载 spc 二进制文件 - -```bash -# Linux x86_64 -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-x86_64 -# Linux aarch64 -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-aarch64 -# macOS x86_64 (Intel) -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-x86_64 -# macOS aarch64 (Apple) -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-aarch64 -# Windows (x86_64, win10 build 17063 或更高版本,请先安装 VS2022) -curl.exe -fsSL -o spc.exe https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-windows-x64.exe -``` - -对于 macOS 和 Linux,请先添加执行权限: - -```bash -chmod +x ./spc -``` - -### 2. 构建静态 PHP - -首先,创建一个 `craft.yml` 文件,并从 [扩展列表](https://static-php.dev/zh/guide/extensions.html) 或 [命令生成器](https://static-php.dev/zh/guide/cli-generator.html) 中指定要包含的扩展: - -```yml -# PHP 版本支持:8.1, 8.2, 8.3, 8.4, 8.5 -php-version: 8.4 -# 在此处放置您的扩展列表 -extensions: "apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" -sapi: - - cli - - micro - - fpm -download-options: - prefer-pre-built: true -``` - -运行命令: - -```bash -./spc craft - -# 输出完整控制台日志 -./spc craft --debug -``` - -### 3. 静态 PHP 使用 - -现在您可以将 StaticPHP 构建的二进制文件复制到另一台机器上,无需依赖即可运行: - -``` -# php-cli -buildroot/bin/php -v - -# phpmicro -echo ' a.php -./spc micro:combine a.php -O my-app -./my-app - -# php-fpm -buildroot/bin/php-fpm -v -``` - -## 文档 - -当前 README 包含基本用法。有关 StaticPHP 的所有功能, -请访问 。 - -## 直接下载 - -如果您不想构建或想先测试,可以从 [Actions](https://github.com/static-php/static-php-cli-hosted/actions/workflows/build-php-bulk.yml) 下载示例预编译工件,或从自托管服务器下载。 - -以下是几个具有不同扩展组合的预编译静态 PHP 二进制文件, -您可以根据需要直接下载。 - -| 组合名称 | 扩展数量 | 系统 | 备注 | -|----------------------------------------------------------------------|----------------------------------------------------------------------------|--------------|--------------------| -| [common](https://dl.static-php.dev/static-php-cli/common/) | [30+](https://dl.static-php.dev/static-php-cli/common/README.txt) | Linux, macOS | 二进制文件大小约为 7.5MB | -| [bulk](https://dl.static-php.dev/static-php-cli/bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | 二进制文件大小约为 25MB | -| [gnu-bulk](https://dl.static-php.dev/static-php-cli/gnu-bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | 使用 glibc 的 bulk 组合 | -| [minimal](https://dl.static-php.dev/static-php-cli/minimal/) | [5](https://dl.static-php.dev/static-php-cli/minimal/README.txt) | Linux, macOS | 二进制文件大小约为 3MB | -| [spc-min](https://dl.static-php.dev/static-php-cli/windows/spc-min/) | [5](https://dl.static-php.dev/static-php-cli/windows/spc-min/README.txt) | Windows | 二进制文件大小约为 3MB | -| [spc-max](https://dl.static-php.dev/static-php-cli/windows/spc-max/) | [40+](https://dl.static-php.dev/static-php-cli/windows/spc-max/README.txt) | Windows | 二进制文件大小约为 8.5MB | - -> Linux 和 Windows 支持对二进制文件进行 UPX 压缩,可以将二进制文件大小减少 30% 到 50%。 -> macOS 不支持 UPX 压缩,因此 mac 的预构建二进制文件大小较大。 - -### 在线构建(使用 GitHub Actions) - -上方直接下载的二进制不能满足需求时,可使用 GitHub Action 可以轻松构建静态编译的 PHP, -同时自行定义要编译的扩展。 - -1. Fork 本项目。 -2. 进入项目的 Actions 并选择 `CI`。 -3. 选择 `Run workflow`,填入您要编译的 PHP 版本、目标类型和扩展列表。(扩展用逗号分隔,例如 `bcmath,curl,mbstring`) -4. 等待一段时间后,进入相应的任务并获取 `Artifacts`。 - -如果您启用 `debug`,构建时将输出所有日志,包括编译日志,以便故障排除。 - -## 贡献 - -如果您需要的扩展缺失,可以创建 issue。 -如果您熟悉本项目,也欢迎发起 pull request。 - -如果您想贡献文档,请直接编辑 `docs/` 目录。 - -现在有一个 [static-php](https://github.com/static-php) 组织,用于存储与项目相关的仓库。 - -## 赞助本项目 - -您可以从 [GitHub Sponsor](https://github.com/crazywhalecc) 赞助我或我的项目。您捐赠的一部分将用于维护 **static-php.dev** 服务器。 - -**特别感谢以下赞助商**: - -Beyond Code Logo - -NativePHP Logo - -## 开源许可证 - -本项目本身基于 MIT 许可证, -一些新添加的扩展和依赖可能来自其他项目, -这些代码文件的头部也会给出额外的许可证和作者说明。 - -这些是类似的项目: - -- [dixyes/lwmbs](https://github.com/dixyes/lwmbs) -- [swoole/swoole-cli](https://github.com/swoole/swoole-cli) - -本项目使用了 [dixyes/lwmbs](https://github.com/dixyes/lwmbs) 的一些代码,例如 Windows 静态构建目标和 libiconv 支持。 -lwmbs 基于 [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2) 许可证。 - -由于本项目的特殊性, -项目编译过程中会使用许多其他开源项目,如 curl 和 protobuf, -它们都有自己的开源许可证。 - -请在编译后使用 `bin/spc dump-license` 命令导出项目中使用的开源许可证, -并遵守相应项目的 LICENSE。 +# StaticPHP + +[![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) +[![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) +[![Releases](https://img.shields.io/packagist/v/crazywhalecc/static-php-cli?include_prereleases&label=Release&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/releases) +[![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) +[![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) + +**StaticPHP** 是一个强大的工具,用于构建可移植的可执行文件,包括 PHP、扩展等。 + +## 特性 + +- :elephant: 支持多个 PHP 版本 - PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- :handbag: 构建零依赖的单文件 PHP 可执行程序 +- :hamburger: 构建 **[phpmicro](https://github.com/static-php/phpmicro)** 自解压可执行文件(将 PHP 二进制和源码合并为单个文件) +- :pill: 自动构建环境检查器,支持自动修复 +- :zap: 支持 `Linux`、`macOS`、`Windows` +- :wrench: 通过 vendor 模式和自定义注册表实现便捷扩展 +- :books: 智能依赖管理 +- 📦 自包含 `spc` 可执行文件,便于自安装 +- :fire: 支持 100+ 热门 [PHP 扩展](https://static-php.dev/en/guide/extensions.html) +- :floppy_disk: 支持 UPX 压缩(二进制体积可缩小 30-50%) + +**单文件独立 php-cli:** + +out1 + +**使用 phpmicro 将 PHP 代码与 PHP 解释器结合:** + +out2 + +## 快速开始 + +### 1. 下载 spc 二进制 + +```bash +# For Linux x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-x86_64 +# For Linux aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-aarch64 +# macOS x86_64 (Intel) +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-x86_64 +# macOS aarch64 (Apple) +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-aarch64 +# Windows (x86_64, win10 build 17063 or later, please install VS2022 first) +curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-release/latest/spc-windows-x64.exe +``` + +对于 macOS 和 Linux,请先添加可执行权限: + +```bash +chmod +x ./spc +``` + +### 2. 构建静态 PHP + +首先,创建 `craft.yml` 文件,并从 [扩展列表](https://static-php.dev/en/guide/extensions.html) 或 [命令生成器](https://static-php.dev/en/guide/cli-generator.html) 指定要包含的扩展: + +```yml +# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5 +php-version: 8.5 +# Put your extension list here +extensions: "apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" +sapi: + - cli + - micro +download-options: + parallel: 10 +``` + +运行命令: + +```bash +./spc craft + +# 输出完整控制台日志 +./spc craft -vvv +``` + +### 3. 静态 PHP 使用 + +现在你可以将 StaticPHP 构建的二进制复制到另一台机器并在无依赖环境下运行: + +``` +# php-cli +buildroot/bin/php -v + +# phpmicro +echo ' a.php +./spc micro:combine a.php -O my-app +./my-app +``` + +## 文档 + +当前 README 包含基础用法。有关 StaticPHP 的完整功能集, +请访问 。 + +## 直接下载 + +如果你暂时不想构建,或只想先测试,可以从 [Actions](https://github.com/static-php/static-php-cli-hosted/actions/workflows/build-php-bulk.yml) 下载示例预编译产物,或从自托管服务器下载。 + +我们为每个 PHP 版本提供 2 种扩展集合: + +- **gigantic**:尽可能包含更多扩展,二进制大小约 100-150MB。 +- **base**:仅包含 StaticPHP 自身使用的少量扩展,二进制大小约 10MB。 + +> WIP + +### 在线构建(使用 GitHub Actions) + +当上方直接下载的二进制无法满足你的需求时, +你可以使用 GitHub Actions 轻松构建静态编译的 PHP, +并同时自定义要编译的扩展列表。 + +1. Fork 此仓库。 +2. 进入项目的 Actions 并选择 `CI`。 +3. 选择 `Run workflow`,填写你要编译的 PHP 版本、目标类型和扩展列表。(扩展用逗号分隔,例如 `bcmath,curl,mbstring`) +4. 等待工作流执行完成后,进入对应运行记录并下载 `Artifacts`。 + +如果你启用 `debug`,构建时将输出所有日志,包括编译日志,便于排查问题。 + +> 我们也计划在未来提供可复用的 GitHub Actions 工作流, +> 这样你无需 fork 本项目,也能在自己的仓库中轻松构建 static PHP。 + +## 贡献 + +如果你需要的扩展缺失,可以创建 issue。 +如果你熟悉本项目,也欢迎发起 pull request。 + +如果你想贡献文档,请直接编辑 `docs/`。 + +## 赞助本项目 + +你可以通过 [GitHub Sponsor](https://github.com/crazywhalecc) 赞助我或我的项目。你捐赠的一部分将用于维护 **static-php.dev** 服务器。 + +**特别感谢以下赞助商:** + +Beyond Code Logo + +NativePHP Logo + +## 开源许可证 + +本项目本身采用 MIT 许可证。 +一些新添加的扩展和依赖可能来自其他项目。 +这些源码文件头部也可能包含额外的 LICENSE 和 AUTHOR 信息。 + +请在编译后使用 `bin/spc dump-license` 命令导出项目中使用的开源许可证, +并遵守对应项目的 LICENSE。 diff --git a/README.md b/README.md index 1d355c469..7c3af6b03 100755 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - :elephant: Support multiple PHP versions - PHP 8.1, 8.2, 8.3, 8.4, 8.5 - :handbag: Build single-file PHP executable with zero dependencies -- :hamburger:Build **[phpmicro](https://github.com/dixyes/phpmicro)** self-extracting executables (combines PHP binary and source code into one file) +- :hamburger: Build **[phpmicro](https://github.com/static-php/phpmicro)** self-extracting executables (combines PHP binary and source code into one file) - :pill: Automatic build environment checker with auto-fix capabilities -- :zap: `Linux`, `macOS`, `FreeBSD`, `Windows` support -- :wrench: Configurable source code patching +- :zap: `Linux`, `macOS`, `Windows` support +- :wrench: Easy to extend with vendor mode and custom registries - :books: Intelligent dependency management -- 📦 Self-contained `spc` executable (built with [box](https://github.com/box-project/box)) -- :fire: Support 100+ popular [extensions](https://static-php.dev/en/guide/extensions.html) +- 📦 Self-contained `spc` executable for easy self-installation +- :fire: Support 100+ popular [PHP extensions](https://static-php.dev/en/guide/extensions.html) - :floppy_disk: UPX compression support (reduces binary size by 30-50%) **Single-file standalone php-cli:** @@ -35,15 +35,15 @@ ```bash # For Linux x86_64 -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-x86_64 # For Linux aarch64 -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-linux-aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-aarch64 # macOS x86_64 (Intel) -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-x86_64 # macOS aarch64 (Apple) -curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-macos-aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-aarch64 # Windows (x86_64, win10 build 17063 or later, please install VS2022 first) -curl.exe -fsSL -o spc.exe https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-windows-x64.exe +curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-release/latest/spc-windows-x64.exe ``` For macOS and Linux, add execute permission first: @@ -58,15 +58,14 @@ First, create a `craft.yml` file and specify which extensions you want to includ ```yml # PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5 -php-version: 8.4 +php-version: 8.5 # Put your extension list here extensions: "apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" sapi: - cli - micro - - fpm download-options: - prefer-pre-built: true + parallel: 10 ``` Run command: @@ -75,7 +74,7 @@ Run command: ./spc craft # Output full console log -./spc craft --debug +./spc craft -vvv ``` ### 3. Static PHP usage @@ -90,48 +89,40 @@ buildroot/bin/php -v echo ' a.php ./spc micro:combine a.php -O my-app ./my-app - -# php-fpm -buildroot/bin/php-fpm -v ``` ## Documentation -The current README contains basic usage. For all the features of StaticPHP, -see . +The current README contains basic usage. For the complete feature set of StaticPHP, +see . ## Direct Download -If you don't want to build or want to test first, you can download example pre-compiled artifact from [Actions](https://github.com/static-php/static-php-cli-hosted/actions/workflows/build-php-bulk.yml), or from self-hosted server. +If you do not want to build yet or just want to test first, you can download example pre-compiled artifacts from [Actions](https://github.com/static-php/static-php-cli-hosted/actions/workflows/build-php-bulk.yml) or from a self-hosted server. -Below are several precompiled static-php binaries with different extension combinations, -which can be downloaded directly according to your needs. +We offer 2 types of extension sets for each PHP version: -| Combination | Extension Count | OS | Comment | -|----------------------------------------------------------------------|----------------------------------------------------------------------------|--------------|--------------------------------| -| [common](https://dl.static-php.dev/static-php-cli/common/) | [30+](https://dl.static-php.dev/static-php-cli/common/README.txt) | Linux, macOS | The binary size is about 7.5MB | -| [bulk](https://dl.static-php.dev/static-php-cli/bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | The binary size is about 25MB | -| [gnu-bulk](https://dl.static-php.dev/static-php-cli/gnu-bulk/) | [50+](https://dl.static-php.dev/static-php-cli/bulk/README.txt) | Linux, macOS | Using shared glibc | -| [minimal](https://dl.static-php.dev/static-php-cli/minimal/) | [5](https://dl.static-php.dev/static-php-cli/minimal/README.txt) | Linux, macOS | The binary size is about 3MB | -| [spc-min](https://dl.static-php.dev/static-php-cli/windows/spc-min/) | [5](https://dl.static-php.dev/static-php-cli/windows/spc-min/README.txt) | Windows | The binary size is about 3MB | -| [spc-max](https://dl.static-php.dev/static-php-cli/windows/spc-max/) | [40+](https://dl.static-php.dev/static-php-cli/windows/spc-max/README.txt) | Windows | The binary size is about 8.5MB | +- **gigantic**: Includes as many extensions as possible, the binary size is about 100-150MB. +- **base**: Only includes a few extensions used by StaticPHP itself, the binary size is about 10MB. -> Linux and Windows supports UPX compression for binaries, which can reduce the size of the binary by 30% to 50%. -> macOS does not support UPX compression, so the size of the pre-built binaries for mac is larger. +> WIP ### Build Online (using GitHub Actions) -When the above direct download binaries cannot meet your needs, -you can use GitHub Action to easily build a statically compiled PHP, -and at the same time define the extensions to be compiled by yourself. +When the direct-download binaries above cannot meet your needs, +you can use GitHub Actions to easily build a statically compiled PHP +while defining your own extension list. -1. Fork me. +1. Fork this repository. 2. Go to the Actions of the project and select `CI`. 3. Select `Run workflow`, fill in the PHP version you want to compile, the target type, and the list of extensions. (extensions comma separated, e.g. `bcmath,curl,mbstring`) -4. After waiting for about a period of time, enter the corresponding task and get `Artifacts`. +4. After waiting for the workflow to finish, open the corresponding run and download `Artifacts`. If you enable `debug`, all logs will be output at build time, including compiled logs, for troubleshooting. +> We are also planning to provide a reusable GitHub Actions workflow in the future, +> so that you can easily build static PHP in your own repository, without forking this project. + ## Contribution If the extension you need is missing, you can create an issue. @@ -139,8 +130,6 @@ If you are familiar with this project, you are also welcome to initiate a pull r If you want to contribute documentation, please just edit in `docs/`. -Now there is a [static-php](https://github.com/static-php) organization, which is used to store the repo related to the project. - ## Sponsor this project You can sponsor me or my project from [GitHub Sponsor](https://github.com/crazywhalecc). A portion of your donation will be used to maintain the **static-php.dev** server. @@ -153,21 +142,9 @@ You can sponsor me or my project from [GitHub Sponsor](https://github.com/crazyw ## Open-Source License -This project itself is based on MIT License, -some newly added extensions and dependencies may originate from the the other projects, -and the headers of these code files will also be given additional instructions LICENSE and AUTHOR. - -These are similar projects: - -- [dixyes/lwmbs](https://github.com/dixyes/lwmbs) -- [swoole/swoole-cli](https://github.com/swoole/swoole-cli) - -The project uses some code from [dixyes/lwmbs](https://github.com/dixyes/lwmbs), such as windows static build target and libiconv support. -lwmbs is licensed under the [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2). - -Due to the special nature of this project, -many other open source projects such as curl and protobuf will be used during the project compilation process, -and they all have their own open source licenses. +This project itself is licensed under MIT. +Some newly added extensions and dependencies may originate from other projects. +The headers of those source files may also include additional LICENSE and AUTHOR information. Please use the `bin/spc dump-license` command to export the open source licenses used in the project after compilation, and comply with the corresponding project's LICENSE. From 7b66a88af10204208778de4f1783dfa6ab3b8550 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 10:47:34 +0800 Subject: [PATCH 641/682] Add dump-extensions command --- .../Command/DumpExtensionsCommand.php | 160 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 2 files changed, 162 insertions(+) create mode 100644 src/StaticPHP/Command/DumpExtensionsCommand.php diff --git a/src/StaticPHP/Command/DumpExtensionsCommand.php b/src/StaticPHP/Command/DumpExtensionsCommand.php new file mode 100644 index 000000000..4e1fb932b --- /dev/null +++ b/src/StaticPHP/Command/DumpExtensionsCommand.php @@ -0,0 +1,160 @@ +addArgument('path', InputArgument::OPTIONAL, 'Path to project root', '.'); + $this->addOption('format', 'F', InputOption::VALUE_REQUIRED, 'Parsed output format', 'default'); + // output zero extension replacement rather than exit as failure + $this->addOption('no-ext-output', 'N', InputOption::VALUE_REQUIRED, 'When no extensions found, output default combination (comma separated)'); + // no dev + $this->addOption('no-dev', null, null, 'Do not include dev dependencies'); + // no spc filter + $this->addOption('no-spc-filter', 'S', null, 'Do not use SPC filter to determine the required extensions'); + } + + public function handle(): int + { + $path = FileSystem::convertPath($this->getArgument('path')); + + $path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json'); + $path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock'); + + $ext_installed = $this->extractFromInstalledJson($path_installed, !$this->getOption('no-dev')); + if ($ext_installed === null) { + if ($this->getOption('format') === 'default') { + $this->output->writeln('vendor/composer/installed.json load failed, skipped'); + } + $ext_installed = []; + } + + $ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev')); + if ($ext_lock === null) { + $this->output->writeln('composer.lock load failed'); + return static::FAILURE; + } + + $extensions = array_unique(array_merge($ext_installed, $ext_lock)); + sort($extensions); + + if (empty($extensions)) { + if ($this->getOption('no-ext-output')) { + $this->outputExtensions(explode(',', $this->getOption('no-ext-output'))); + return static::SUCCESS; + } + $this->output->writeln('No extensions found'); + return static::FAILURE; + } + + $this->outputExtensions($extensions); + return static::SUCCESS; + } + + private function filterExtensions(array $requirements): array + { + return array_map( + fn ($key) => substr($key, 4), + array_keys( + array_filter($requirements, function ($key) { + return str_starts_with($key, 'ext-'); + }, ARRAY_FILTER_USE_KEY) + ) + ); + } + + private function loadJson(string $file): array|bool + { + if (!file_exists($file)) { + return false; + } + + $data = json_decode(file_get_contents($file), true); + if (!$data) { + return false; + } + return $data; + } + + private function extractFromInstalledJson(string $file, bool $include_dev = true): ?array + { + if (!($data = $this->loadJson($file))) { + return null; + } + + $packages = $data['packages'] ?? []; + + if (!$include_dev) { + $packages = array_filter($packages, fn ($package) => !in_array($package['name'], $data['dev-package-names'] ?? [])); + } + + return array_merge( + ...array_map(fn ($x) => isset($x['require']) ? $this->filterExtensions($x['require']) : [], $packages) + ); + } + + private function extractFromComposerLock(string $file, bool $include_dev = true): ?array + { + if (!($data = $this->loadJson($file))) { + return null; + } + + // get packages ext + $packages = $data['packages'] ?? []; + $exts = array_merge( + ...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages) + ); + + // get dev packages ext + if ($include_dev) { + $packages = $data['packages-dev'] ?? []; + $exts = array_merge( + $exts, + ...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages) + ); + } + + // get require ext + $platform = $data['platform'] ?? []; + $exts = array_merge($exts, $this->filterExtensions($platform)); + + // get require-dev ext + if ($include_dev) { + $platform = $data['platform-dev'] ?? []; + $exts = array_merge($exts, $this->filterExtensions($platform)); + } + + return $exts; + } + + private function outputExtensions(array $extensions): void + { + if (!$this->getOption('no-spc-filter')) { + $extensions = parse_extension_list($extensions); + } + switch ($this->getOption('format')) { + case 'json': + $this->output->writeln(json_encode($extensions, JSON_PRETTY_PRINT)); + break; + case 'text': + $this->output->writeln(implode(',', $extensions)); + break; + default: + $this->output->writeln('Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':'); + $this->output->writeln(implode(',', $extensions)); + } + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 21e3ee8b6..1fced54c5 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -18,6 +18,7 @@ use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; +use StaticPHP\Command\DumpExtensionsCommand; use StaticPHP\Command\DumpLicenseCommand; use StaticPHP\Command\ExtractCommand; use StaticPHP\Command\InstallPackageCommand; @@ -65,6 +66,7 @@ public function __construct() new ExtractCommand(), new SPCConfigCommand(), new DumpLicenseCommand(), + new DumpExtensionsCommand(), new ResetCommand(), new CheckUpdateCommand(), new MicroCombineCommand(), From 165372d17bf7cea6b22ec4638419696adc3d636d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 11:04:43 +0800 Subject: [PATCH 642/682] Add OS support for Linux and Darwin in memcache and memcached configurations --- config/pkg/ext/ext-memcache.yml | 3 +++ config/pkg/ext/ext-memcached.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/config/pkg/ext/ext-memcache.yml b/config/pkg/ext/ext-memcache.yml index 9db51c05b..6e1f4385c 100644 --- a/config/pkg/ext/ext-memcache.yml +++ b/config/pkg/ext/ext-memcache.yml @@ -11,4 +11,7 @@ ext-memcache: - ext-zlib - ext-session php-extension: + os: + - Linux + - Darwin arg-type: '--enable-memcache@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/config/pkg/ext/ext-memcached.yml b/config/pkg/ext/ext-memcached.yml index 329227f2b..b320c6699 100644 --- a/config/pkg/ext/ext-memcached.yml +++ b/config/pkg/ext/ext-memcached.yml @@ -20,4 +20,7 @@ ext-memcached: - ext-msgpack - ext-session php-extension: + os: + - Linux + - Darwin arg-type: '--enable-memcached@shared_suffix@ --with-zlib-dir=@build_root_path@' From f6f7b629e3644eae115cf8b43b9dfd55ca2a542e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 11:17:55 +0800 Subject: [PATCH 643/682] Disable v2 workflows temporarily --- .github/workflows/release-build.yml | 350 ++++++++++++------------- .github/workflows/tests.yml | 2 + .github/workflows/vitepress-deploy.yml | 142 +++++----- 3 files changed, 248 insertions(+), 246 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index aaeee1ffc..bb08fa059 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -1,175 +1,175 @@ -name: Build SPC Binary - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - paths: - - '.github/workflows/release-build.yml' - release: - types: - - published - workflow_dispatch: - -env: - PHP_VERSION: 8.4 - MICRO_VERSION: 8.4.11 - -jobs: - build-release-artifacts: - name: "Build SPC Binary for ${{ matrix.operating-system.name }}" - runs-on: ${{ matrix.operating-system.os }} - strategy: - matrix: - operating-system: - - name: "linux-x86_64" - os: "ubuntu-latest" - filename: "spc-linux-x86_64.tar.gz" - - name: "macos-x86_64" - os: "macos-15-intel" - filename: "spc-macos-x86_64.tar.gz" - - name: "linux-aarch64" - os: "ubuntu-latest" - filename: "spc-linux-aarch64.tar.gz" - - name: "macos-aarch64" - os: "macos-14" - filename: "spc-macos-aarch64.tar.gz" - - name: "windows-x64" - os: "ubuntu-latest" - filename: "spc-windows-x64.exe" - steps: - - name: "Checkout" - uses: "actions/checkout@v4" - - - if: inputs.debug == true - run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV - - - name: "Install PHP for official runners" - uses: shivammathur/setup-php@v2 - with: - coverage: none - tools: composer:v2 - php-version: "${{ env.PHP_VERSION }}" - ini-values: memory_limit=-1 - extensions: curl, openssl, mbstring - - - name: "Get Composer Cache Directory" - id: composer-cache - run: | - echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: "Cache Composer dependencies" - uses: "actions/cache@v4" - with: - path: "${{ steps.composer-cache.outputs.dir }}" - key: "php-${{ env.PHP_VERSION }}-locked-composer-${{ hashFiles('**/composer.lock') }}" - restore-keys: | - php-${{ env.PHP_VERSION }}-locked-composer - - - name: "Install Locked Dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "Build PHAR File" - run: "composer build:phar" - - - name: "Download Minimal Combination" - run: | - if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then - curl -fsSL https://dl.static-php.dev/static-php-cli/windows/spc-min/php-${{ env.MICRO_VERSION }}-micro-win.zip -o tmp.zip - unzip tmp.zip - else - curl -fsSL https://dl.static-php.dev/static-php-cli/minimal/php-${{ env.MICRO_VERSION }}-micro-${{ matrix.operating-system.name }}.tar.gz -o tmp.tgz - tar -zxvf tmp.tgz - fi - - - name: "Generate Executable" - run: | - bin/spc micro:combine spc.phar -M micro.sfx -O spc -I "memory_limit=2G" - if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then - mv spc spc.exe - else - chmod +x spc - fi - if [ "${{ matrix.operating-system.name }}" = "macos-aarch64" ] || [ "${{ matrix.operating-system.name }}" = "macos-x86_64" ]; then - sudo xattr -cr ./spc - fi - - - name: "Archive Executable and Validate Binary" - run: | - if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then - tar -czf ${{ matrix.operating-system.filename }} spc - # validate spc binary - if [ "${{ matrix.operating-system.name }}" == "linux-x86_64" ]; then - ./spc dev:extensions - fi - fi - - - name: "Copy file" - run: | - if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then - mkdir dist/ && cp ${{ matrix.operating-system.filename }} dist/ && cp spc dist/spc-${{ matrix.operating-system.name }} - else - mkdir dist/ && cp spc.exe dist/${{ matrix.operating-system.filename }} - echo "SUFFIX=.exe" >> $GITHUB_ENV - fi - - - name: "Upload Binaries to Release" - uses: softprops/action-gh-release@v1 - if: ${{startsWith(github.ref, 'refs/tags/') }} - with: - files: dist/${{ matrix.operating-system.filename }} - - - name: "Deploy to self-hosted OSS" - # only run this step if the repository is static-php-cli and the branch is main - if: github.repository == 'crazywhalecc/static-php-cli' && github.ref == 'refs/heads/main' - uses: static-php/upload-s3-action@v1.0.0 - with: - aws_key_id: ${{ secrets.AWS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_bucket: ${{ secrets.AWS_BUCKET }} - source_dir: "dist/" - destination_dir: static-php-cli/spc-bin/nightly/ - endpoint: ${{ secrets.AWS_ENDPOINT }} - - - name: "Upload Artifact" - uses: actions/upload-artifact@v4 - with: - path: spc${{ env.SUFFIX }} - name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} - test-spc: - name: "Test SPC Binary for ${{ matrix.operating-system.name }}" - runs-on: ${{ matrix.operating-system.os }} - needs: [build-release-artifacts] - strategy: - matrix: - operating-system: - - name: "linux-x86_64" - os: "ubuntu-latest" - - name: "macos-x86_64" - os: "macos-15-intel" - - name: "linux-aarch64" - os: "ubuntu-24.04-arm" - - name: "macos-aarch64" - os: "macos-15" - - name: "windows-x64" - os: "windows-latest" - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - - name: "Download Artifact" - uses: actions/download-artifact@v4 - env: - SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} - with: - name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} - - - name: "Chmod" - if: matrix.operating-system.name != 'windows-x64' - run: chmod +x spc - - - name: "Run SPC Tests" - env: - SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} - run: ./spc${{ env.SUFFIX }} dev:extensions +name: Build SPC Binary + +on: + push: + branches: [ "main", "v3" ] + pull_request: + branches: [ "main", "v3" ] + paths: + - '.github/workflows/release-build.yml' + release: + types: + - published + workflow_dispatch: + +env: + PHP_VERSION: 8.4 + MICRO_VERSION: 8.4.11 + +jobs: + build-release-artifacts: + name: "Build SPC Binary for ${{ matrix.operating-system.name }}" + runs-on: ${{ matrix.operating-system.os }} + strategy: + matrix: + operating-system: + - name: "linux-x86_64" + os: "ubuntu-latest" + filename: "spc-linux-x86_64.tar.gz" + - name: "macos-x86_64" + os: "macos-15-intel" + filename: "spc-macos-x86_64.tar.gz" + - name: "linux-aarch64" + os: "ubuntu-latest" + filename: "spc-linux-aarch64.tar.gz" + - name: "macos-aarch64" + os: "macos-14" + filename: "spc-macos-aarch64.tar.gz" + - name: "windows-x64" + os: "ubuntu-latest" + filename: "spc-windows-x64.exe" + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + + - if: inputs.debug == true + run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV + + - name: "Install PHP for official runners" + uses: shivammathur/setup-php@v2 + with: + coverage: none + tools: composer:v2 + php-version: "${{ env.PHP_VERSION }}" + ini-values: memory_limit=-1 + extensions: curl, openssl, mbstring + + - name: "Get Composer Cache Directory" + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: "Cache Composer dependencies" + uses: "actions/cache@v4" + with: + path: "${{ steps.composer-cache.outputs.dir }}" + key: "php-${{ env.PHP_VERSION }}-locked-composer-${{ hashFiles('**/composer.lock') }}" + restore-keys: | + php-${{ env.PHP_VERSION }}-locked-composer + + - name: "Install Locked Dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Build PHAR File" + run: "composer build:phar" + + - name: "Download Minimal Combination" + run: | + if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then + curl -fsSL https://dl.static-php.dev/static-php-cli/windows/spc-min/php-${{ env.MICRO_VERSION }}-micro-win.zip -o tmp.zip + unzip tmp.zip + else + curl -fsSL https://dl.static-php.dev/static-php-cli/minimal/php-${{ env.MICRO_VERSION }}-micro-${{ matrix.operating-system.name }}.tar.gz -o tmp.tgz + tar -zxvf tmp.tgz + fi + + - name: "Generate Executable" + run: | + bin/spc micro:combine spc.phar -M micro.sfx -O spc -I "memory_limit=2G" + if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then + mv spc spc.exe + else + chmod +x spc + fi + if [ "${{ matrix.operating-system.name }}" = "macos-aarch64" ] || [ "${{ matrix.operating-system.name }}" = "macos-x86_64" ]; then + sudo xattr -cr ./spc + fi + + - name: "Archive Executable and Validate Binary" + run: | + if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then + tar -czf ${{ matrix.operating-system.filename }} spc + # validate spc binary + if [ "${{ matrix.operating-system.name }}" == "linux-x86_64" ]; then + ./spc dev:extensions + fi + fi + + - name: "Copy file" + run: | + if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then + mkdir dist/ && cp ${{ matrix.operating-system.filename }} dist/ && cp spc dist/spc-${{ matrix.operating-system.name }} + else + mkdir dist/ && cp spc.exe dist/${{ matrix.operating-system.filename }} + echo "SUFFIX=.exe" >> $GITHUB_ENV + fi + + - name: "Upload Binaries to Release" + uses: softprops/action-gh-release@v1 + if: ${{startsWith(github.ref, 'refs/tags/') }} + with: + files: dist/${{ matrix.operating-system.filename }} + + - name: "Deploy to self-hosted OSS" + # only run this step if the repository is static-php-cli and the branch is main + if: github.repository == 'crazywhalecc/static-php-cli' && github.ref == 'refs/heads/main' + uses: static-php/upload-s3-action@v1.0.0 + with: + aws_key_id: ${{ secrets.AWS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_bucket: ${{ secrets.AWS_BUCKET }} + source_dir: "dist/" + destination_dir: static-php-cli/spc-bin/nightly/ + endpoint: ${{ secrets.AWS_ENDPOINT }} + + - name: "Upload Artifact" + uses: actions/upload-artifact@v4 + with: + path: spc${{ env.SUFFIX }} + name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} + test-spc: + name: "Test SPC Binary for ${{ matrix.operating-system.name }}" + runs-on: ${{ matrix.operating-system.os }} + needs: [build-release-artifacts] + strategy: + matrix: + operating-system: + - name: "linux-x86_64" + os: "ubuntu-latest" + - name: "macos-x86_64" + os: "macos-15-intel" + - name: "linux-aarch64" + os: "ubuntu-24.04-arm" + - name: "macos-aarch64" + os: "macos-15" + - name: "windows-x64" + os: "windows-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Download Artifact" + uses: actions/download-artifact@v4 + env: + SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} + with: + name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} + + - name: "Chmod" + if: matrix.operating-system.name != 'windows-x64' + run: chmod +x spc + + - name: "Run SPC Tests" + env: + SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} + run: ./spc${{ env.SUFFIX }} dev:extensions diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c2730fdb..1f3c73180 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -106,6 +106,7 @@ jobs: run: SPC_LIBC=glibc vendor/bin/phpunit tests/ --no-coverage define-matrix: + if: false # TODO: enable when refactoring workflows name: "Define Matrix" runs-on: ubuntu-latest outputs: @@ -131,6 +132,7 @@ jobs: build: + if: false name: "Build PHP Test (PHP ${{ matrix.php }} ${{ matrix.os }})" runs-on: ${{ matrix.os }} needs: [define-matrix, php-cs-fixer, phpstan, phpunit] diff --git a/.github/workflows/vitepress-deploy.yml b/.github/workflows/vitepress-deploy.yml index 9101f11c7..6eb37b4a8 100644 --- a/.github/workflows/vitepress-deploy.yml +++ b/.github/workflows/vitepress-deploy.yml @@ -1,71 +1,71 @@ -name: Docs Auto Deploy -on: - push: - branches: - - main - paths: - - 'config/**.json' - - 'docs/**' - - 'package.json' - - 'yarn.lock' - - '.github/workflows/vitepress-deploy.yml' - -jobs: - build: - name: Deploy docs - runs-on: ubuntu-latest - if: github.repository == 'crazywhalecc/static-php-cli' - steps: - - name: Checkout master - uses: actions/checkout@v4 - - - uses: actions/setup-node@v3 - with: - cache: yarn - - - run: yarn install --frozen-lockfile - - - name: "Copy Config Files" - run: | - mkdir -p docs/.vitepress/config - cp -r config/* docs/.vitepress/config/ - - - name: "Install PHP for official runners" - uses: shivammathur/setup-php@v2 - with: - coverage: none - tools: composer:v2 - php-version: 8.4 - ini-values: memory_limit=-1 - extensions: curl, openssl, mbstring - - - name: "Get Composer Cache Directory" - id: composer-cache - run: | - echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: "Cache Composer dependencies" - uses: "actions/cache@v4" - with: - path: "${{ steps.composer-cache.outputs.dir }}" - key: "php-8.2-locked-composer-${{ hashFiles('**/composer.lock') }}" - restore-keys: | - php-8.2-locked-composer - - - name: "Install Locked Dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "Generate Extension Support List" - run: | - bin/spc dev:gen-ext-docs > docs/extensions.md - bin/spc dev:gen-ext-dep-docs > docs/deps-map-ext.md - bin/spc dev:gen-lib-dep-docs > docs/deps-map-lib.md - - - name: Build - run: yarn docs:build - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/.vitepress/dist +name: Docs Auto Deploy +on: + push: + branches: + - v3 + paths: + - 'config/**.yml' + - 'docs/**' + - 'package.json' + - 'yarn.lock' + - '.github/workflows/vitepress-deploy.yml' + +jobs: + build: + name: Deploy docs + runs-on: ubuntu-latest + if: github.repository == 'crazywhalecc/static-php-cli' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: actions/setup-node@v3 + with: + cache: yarn + + - run: yarn install --frozen-lockfile + + - name: "Copy Config Files" + run: | + mkdir -p docs/.vitepress/config + cp -r config/* docs/.vitepress/config/ + + - name: "Install PHP for official runners" + uses: shivammathur/setup-php@v2 + with: + coverage: none + tools: composer:v2 + php-version: 8.4 + ini-values: memory_limit=-1 + extensions: curl, openssl, mbstring + + - name: "Get Composer Cache Directory" + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: "Cache Composer dependencies" + uses: "actions/cache@v4" + with: + path: "${{ steps.composer-cache.outputs.dir }}" + key: "php-8.2-locked-composer-${{ hashFiles('**/composer.lock') }}" + restore-keys: | + php-8.2-locked-composer + + - name: "Install Locked Dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Generate Extension Support List" + run: | + bin/spc dev:gen-ext-docs > docs/extensions.md + bin/spc dev:gen-ext-dep-docs > docs/deps-map-ext.md + bin/spc dev:gen-lib-dep-docs > docs/deps-map-lib.md + + - name: Build + run: yarn docs:build + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/.vitepress/dist From 764894e9306636f0fae7b5ef8fa08ecbd9160972 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 11:19:54 +0800 Subject: [PATCH 644/682] Use dev:info instead of dev:extensions --- .github/workflows/release-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index bb08fa059..ac38b782c 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -101,7 +101,7 @@ jobs: tar -czf ${{ matrix.operating-system.filename }} spc # validate spc binary if [ "${{ matrix.operating-system.name }}" == "linux-x86_64" ]; then - ./spc dev:extensions + ./spc dev:info php fi fi @@ -172,4 +172,4 @@ jobs: - name: "Run SPC Tests" env: SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} - run: ./spc${{ env.SUFFIX }} dev:extensions + run: ./spc${{ env.SUFFIX }} dev:info php From 8e970f37dc38e893979032caa009195da1d1b593 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 11:26:11 +0800 Subject: [PATCH 645/682] Use versioned spc-bin and latest spc-bin --- .github/workflows/release-build.yml | 22 +++++++++++++++++----- README-zh.md | 10 +++++----- README.md | 10 +++++----- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index ac38b782c..97a4c22cf 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -60,7 +60,7 @@ jobs: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: "Cache Composer dependencies" - uses: "actions/cache@v4" + uses: "actions/cache@v5" with: path: "${{ steps.composer-cache.outputs.dir }}" key: "php-${{ env.PHP_VERSION }}-locked-composer-${{ hashFiles('**/composer.lock') }}" @@ -120,16 +120,28 @@ jobs: with: files: dist/${{ matrix.operating-system.filename }} - - name: "Deploy to self-hosted OSS" - # only run this step if the repository is static-php-cli and the branch is main - if: github.repository == 'crazywhalecc/static-php-cli' && github.ref == 'refs/heads/main' + - name: "Deploy to self-hosted OSS (latest)" + # only run this step if the repository is static-php-cli and is release tag + if: ${{ github.repository == 'crazywhalecc/static-php-cli' && startsWith(github.ref, 'refs/tags/') }} uses: static-php/upload-s3-action@v1.0.0 with: aws_key_id: ${{ secrets.AWS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws_bucket: ${{ secrets.AWS_BUCKET }} source_dir: "dist/" - destination_dir: static-php-cli/spc-bin/nightly/ + destination_dir: v3/spc-bin/latest/ + endpoint: ${{ secrets.AWS_ENDPOINT }} + + - name: "Deploy to self-hosted OSS (versioned)" + # only run this step if the repository is static-php-cli and is release tag + if: ${{ github.repository == 'crazywhalecc/static-php-cli' && startsWith(github.ref, 'refs/tags/') }} + uses: static-php/upload-s3-action@v1.0.0 + with: + aws_key_id: ${{ secrets.AWS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_bucket: ${{ secrets.AWS_BUCKET }} + source_dir: "dist/" + destination_dir: v3/spc-bin/${{ github.ref_name }}/ endpoint: ${{ secrets.AWS_ENDPOINT }} - name: "Upload Artifact" diff --git a/README-zh.md b/README-zh.md index d75d07938..fe3dc2800 100755 --- a/README-zh.md +++ b/README-zh.md @@ -35,15 +35,15 @@ ```bash # For Linux x86_64 -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-x86_64 # For Linux aarch64 -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-aarch64 # macOS x86_64 (Intel) -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-x86_64 # macOS aarch64 (Apple) -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-aarch64 # Windows (x86_64, win10 build 17063 or later, please install VS2022 first) -curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-release/latest/spc-windows-x64.exe +curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-bin/latest/spc-windows-x64.exe ``` 对于 macOS 和 Linux,请先添加可执行权限: diff --git a/README.md b/README.md index 7c3af6b03..2ac3e349f 100755 --- a/README.md +++ b/README.md @@ -35,15 +35,15 @@ ```bash # For Linux x86_64 -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-x86_64 # For Linux aarch64 -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-linux-aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-aarch64 # macOS x86_64 (Intel) -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-x86_64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-x86_64 # macOS aarch64 (Apple) -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-release/latest/spc-macos-aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-aarch64 # Windows (x86_64, win10 build 17063 or later, please install VS2022 first) -curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-release/latest/spc-windows-x64.exe +curl.exe -fsSL -o spc.exe https://dl.static-php.dev/v3/spc-bin/latest/spc-windows-x64.exe ``` For macOS and Linux, add execute permission first: From 4413529d8328869d6d5ec94b8bdfb47462f5238f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 11:30:03 +0800 Subject: [PATCH 646/682] Update to v5 --- .github/workflows/release-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 97a4c22cf..4127c2dcf 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -40,7 +40,7 @@ jobs: filename: "spc-windows-x64.exe" steps: - name: "Checkout" - uses: "actions/checkout@v4" + uses: "actions/checkout@v5" - if: inputs.debug == true run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV @@ -145,7 +145,7 @@ jobs: endpoint: ${{ secrets.AWS_ENDPOINT }} - name: "Upload Artifact" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: path: spc${{ env.SUFFIX }} name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} @@ -168,10 +168,10 @@ jobs: os: "windows-latest" steps: - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: "Download Artifact" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 env: SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} with: From e22e615ba49bd54a5100084f9aa251392ca4d9a9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 12:27:52 +0800 Subject: [PATCH 647/682] Upgrade actions/upload-artifact to v8 in build and test workflows --- .github/workflows/build-unix.yml | 16 +- .github/workflows/build-windows-x86_64.yml | 8 +- .github/workflows/release-build.yml | 374 ++++++++++----------- .github/workflows/tests.yml | 2 +- 4 files changed, 200 insertions(+), 200 deletions(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 083b464c9..3b088273d 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -276,7 +276,7 @@ jobs: # Upload debug logs - if: ${{ inputs.debug && failure() }} name: "Upload build logs on failure" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: spc-logs-${{ inputs.php-version }}-${{ inputs.os }} path: log/*.log @@ -284,7 +284,7 @@ jobs: # Upload cli executable - if: ${{ inputs.build-cli == true }} name: "Upload PHP cli SAPI" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: php-cli-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/php @@ -292,7 +292,7 @@ jobs: # Upload micro self-extracted executable - if: ${{ inputs.build-micro == true }} name: "Upload PHP micro SAPI" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: php-micro-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/micro.sfx @@ -300,7 +300,7 @@ jobs: # Upload fpm executable - if: ${{ inputs.build-fpm == true }} name: "Upload PHP fpm SAPI" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: php-fpm-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/php-fpm @@ -308,7 +308,7 @@ jobs: # Upload frankenphp executable - if: ${{ inputs['build-frankenphp'] == true }} name: "Upload FrankenPHP SAPI" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: php-frankenphp-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/frankenphp @@ -316,17 +316,17 @@ jobs: # Upload extensions metadata - if: ${{ inputs['shared-extensions'] != '' }} name: "Upload shared extensions" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: php-shared-ext-${{ inputs.php-version }}-${{ inputs.os }} path: | buildroot/modules/*.so - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v8 name: "Upload License Files" with: name: license-files-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/license/ - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v8 name: "Upload Build Metadata" with: name: build-meta-${{ inputs.php-version }}-${{ inputs.os }} diff --git a/.github/workflows/build-windows-x86_64.yml b/.github/workflows/build-windows-x86_64.yml index a53d9e437..6fa9f25de 100644 --- a/.github/workflows/build-windows-x86_64.yml +++ b/.github/workflows/build-windows-x86_64.yml @@ -94,24 +94,24 @@ jobs: # Upload cli executable - if: ${{ inputs.build-cli == true }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: php-${{ inputs.version }} path: buildroot/bin/php.exe # Upload micro self-extracted executable - if: ${{ inputs.build-micro == true }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: micro-${{ inputs.version }} path: buildroot/bin/micro.sfx # Upload extensions metadata - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v8 with: name: license-files path: buildroot/license/ - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v8 with: name: build-meta path: | diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 4127c2dcf..6745fffb2 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -1,187 +1,187 @@ -name: Build SPC Binary - -on: - push: - branches: [ "main", "v3" ] - pull_request: - branches: [ "main", "v3" ] - paths: - - '.github/workflows/release-build.yml' - release: - types: - - published - workflow_dispatch: - -env: - PHP_VERSION: 8.4 - MICRO_VERSION: 8.4.11 - -jobs: - build-release-artifacts: - name: "Build SPC Binary for ${{ matrix.operating-system.name }}" - runs-on: ${{ matrix.operating-system.os }} - strategy: - matrix: - operating-system: - - name: "linux-x86_64" - os: "ubuntu-latest" - filename: "spc-linux-x86_64.tar.gz" - - name: "macos-x86_64" - os: "macos-15-intel" - filename: "spc-macos-x86_64.tar.gz" - - name: "linux-aarch64" - os: "ubuntu-latest" - filename: "spc-linux-aarch64.tar.gz" - - name: "macos-aarch64" - os: "macos-14" - filename: "spc-macos-aarch64.tar.gz" - - name: "windows-x64" - os: "ubuntu-latest" - filename: "spc-windows-x64.exe" - steps: - - name: "Checkout" - uses: "actions/checkout@v5" - - - if: inputs.debug == true - run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV - - - name: "Install PHP for official runners" - uses: shivammathur/setup-php@v2 - with: - coverage: none - tools: composer:v2 - php-version: "${{ env.PHP_VERSION }}" - ini-values: memory_limit=-1 - extensions: curl, openssl, mbstring - - - name: "Get Composer Cache Directory" - id: composer-cache - run: | - echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: "Cache Composer dependencies" - uses: "actions/cache@v5" - with: - path: "${{ steps.composer-cache.outputs.dir }}" - key: "php-${{ env.PHP_VERSION }}-locked-composer-${{ hashFiles('**/composer.lock') }}" - restore-keys: | - php-${{ env.PHP_VERSION }}-locked-composer - - - name: "Install Locked Dependencies" - run: "composer install --no-interaction --no-progress" - - - name: "Build PHAR File" - run: "composer build:phar" - - - name: "Download Minimal Combination" - run: | - if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then - curl -fsSL https://dl.static-php.dev/static-php-cli/windows/spc-min/php-${{ env.MICRO_VERSION }}-micro-win.zip -o tmp.zip - unzip tmp.zip - else - curl -fsSL https://dl.static-php.dev/static-php-cli/minimal/php-${{ env.MICRO_VERSION }}-micro-${{ matrix.operating-system.name }}.tar.gz -o tmp.tgz - tar -zxvf tmp.tgz - fi - - - name: "Generate Executable" - run: | - bin/spc micro:combine spc.phar -M micro.sfx -O spc -I "memory_limit=2G" - if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then - mv spc spc.exe - else - chmod +x spc - fi - if [ "${{ matrix.operating-system.name }}" = "macos-aarch64" ] || [ "${{ matrix.operating-system.name }}" = "macos-x86_64" ]; then - sudo xattr -cr ./spc - fi - - - name: "Archive Executable and Validate Binary" - run: | - if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then - tar -czf ${{ matrix.operating-system.filename }} spc - # validate spc binary - if [ "${{ matrix.operating-system.name }}" == "linux-x86_64" ]; then - ./spc dev:info php - fi - fi - - - name: "Copy file" - run: | - if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then - mkdir dist/ && cp ${{ matrix.operating-system.filename }} dist/ && cp spc dist/spc-${{ matrix.operating-system.name }} - else - mkdir dist/ && cp spc.exe dist/${{ matrix.operating-system.filename }} - echo "SUFFIX=.exe" >> $GITHUB_ENV - fi - - - name: "Upload Binaries to Release" - uses: softprops/action-gh-release@v1 - if: ${{startsWith(github.ref, 'refs/tags/') }} - with: - files: dist/${{ matrix.operating-system.filename }} - - - name: "Deploy to self-hosted OSS (latest)" - # only run this step if the repository is static-php-cli and is release tag - if: ${{ github.repository == 'crazywhalecc/static-php-cli' && startsWith(github.ref, 'refs/tags/') }} - uses: static-php/upload-s3-action@v1.0.0 - with: - aws_key_id: ${{ secrets.AWS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_bucket: ${{ secrets.AWS_BUCKET }} - source_dir: "dist/" - destination_dir: v3/spc-bin/latest/ - endpoint: ${{ secrets.AWS_ENDPOINT }} - - - name: "Deploy to self-hosted OSS (versioned)" - # only run this step if the repository is static-php-cli and is release tag - if: ${{ github.repository == 'crazywhalecc/static-php-cli' && startsWith(github.ref, 'refs/tags/') }} - uses: static-php/upload-s3-action@v1.0.0 - with: - aws_key_id: ${{ secrets.AWS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_bucket: ${{ secrets.AWS_BUCKET }} - source_dir: "dist/" - destination_dir: v3/spc-bin/${{ github.ref_name }}/ - endpoint: ${{ secrets.AWS_ENDPOINT }} - - - name: "Upload Artifact" - uses: actions/upload-artifact@v5 - with: - path: spc${{ env.SUFFIX }} - name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} - test-spc: - name: "Test SPC Binary for ${{ matrix.operating-system.name }}" - runs-on: ${{ matrix.operating-system.os }} - needs: [build-release-artifacts] - strategy: - matrix: - operating-system: - - name: "linux-x86_64" - os: "ubuntu-latest" - - name: "macos-x86_64" - os: "macos-15-intel" - - name: "linux-aarch64" - os: "ubuntu-24.04-arm" - - name: "macos-aarch64" - os: "macos-15" - - name: "windows-x64" - os: "windows-latest" - steps: - - name: "Checkout" - uses: actions/checkout@v5 - - - name: "Download Artifact" - uses: actions/download-artifact@v5 - env: - SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} - with: - name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} - - - name: "Chmod" - if: matrix.operating-system.name != 'windows-x64' - run: chmod +x spc - - - name: "Run SPC Tests" - env: - SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} - run: ./spc${{ env.SUFFIX }} dev:info php +name: Build SPC Binary + +on: + push: + branches: [ "main", "v3" ] + pull_request: + branches: [ "main", "v3" ] + paths: + - '.github/workflows/release-build.yml' + release: + types: + - published + workflow_dispatch: + +env: + PHP_VERSION: 8.4 + MICRO_VERSION: 8.4.11 + +jobs: + build-release-artifacts: + name: "Build SPC Binary for ${{ matrix.operating-system.name }}" + runs-on: ${{ matrix.operating-system.os }} + strategy: + matrix: + operating-system: + - name: "linux-x86_64" + os: "ubuntu-latest" + filename: "spc-linux-x86_64.tar.gz" + - name: "macos-x86_64" + os: "macos-15-intel" + filename: "spc-macos-x86_64.tar.gz" + - name: "linux-aarch64" + os: "ubuntu-latest" + filename: "spc-linux-aarch64.tar.gz" + - name: "macos-aarch64" + os: "macos-14" + filename: "spc-macos-aarch64.tar.gz" + - name: "windows-x64" + os: "ubuntu-latest" + filename: "spc-windows-x64.exe" + steps: + - name: "Checkout" + uses: "actions/checkout@v5" + + - if: inputs.debug == true + run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV + + - name: "Install PHP for official runners" + uses: shivammathur/setup-php@v2 + with: + coverage: none + tools: composer:v2 + php-version: "${{ env.PHP_VERSION }}" + ini-values: memory_limit=-1 + extensions: curl, openssl, mbstring + + - name: "Get Composer Cache Directory" + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: "Cache Composer dependencies" + uses: "actions/cache@v5" + with: + path: "${{ steps.composer-cache.outputs.dir }}" + key: "php-${{ env.PHP_VERSION }}-locked-composer-${{ hashFiles('**/composer.lock') }}" + restore-keys: | + php-${{ env.PHP_VERSION }}-locked-composer + + - name: "Install Locked Dependencies" + run: "composer install --no-interaction --no-progress" + + - name: "Build PHAR File" + run: "composer build:phar" + + - name: "Download Minimal Combination" + run: | + if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then + curl -fsSL https://dl.static-php.dev/static-php-cli/windows/spc-min/php-${{ env.MICRO_VERSION }}-micro-win.zip -o tmp.zip + unzip tmp.zip + else + curl -fsSL https://dl.static-php.dev/static-php-cli/minimal/php-${{ env.MICRO_VERSION }}-micro-${{ matrix.operating-system.name }}.tar.gz -o tmp.tgz + tar -zxvf tmp.tgz + fi + + - name: "Generate Executable" + run: | + bin/spc micro:combine spc.phar -M micro.sfx -O spc -I "memory_limit=2G" + if [ "${{ matrix.operating-system.name }}" = "windows-x64" ]; then + mv spc spc.exe + else + chmod +x spc + fi + if [ "${{ matrix.operating-system.name }}" = "macos-aarch64" ] || [ "${{ matrix.operating-system.name }}" = "macos-x86_64" ]; then + sudo xattr -cr ./spc + fi + + - name: "Archive Executable and Validate Binary" + run: | + if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then + tar -czf ${{ matrix.operating-system.filename }} spc + # validate spc binary + if [ "${{ matrix.operating-system.name }}" == "linux-x86_64" ]; then + ./spc dev:info php + fi + fi + + - name: "Copy file" + run: | + if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then + mkdir dist/ && cp ${{ matrix.operating-system.filename }} dist/ && cp spc dist/spc-${{ matrix.operating-system.name }} + else + mkdir dist/ && cp spc.exe dist/${{ matrix.operating-system.filename }} + echo "SUFFIX=.exe" >> $GITHUB_ENV + fi + + - name: "Upload Binaries to Release" + uses: softprops/action-gh-release@v1 + if: ${{startsWith(github.ref, 'refs/tags/') }} + with: + files: dist/${{ matrix.operating-system.filename }} + + - name: "Deploy to self-hosted OSS (latest)" + # only run this step if the repository is static-php-cli and is release tag + if: ${{ github.repository == 'crazywhalecc/static-php-cli' && startsWith(github.ref, 'refs/tags/') }} + uses: static-php/upload-s3-action@v1.0.0 + with: + aws_key_id: ${{ secrets.AWS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_bucket: ${{ secrets.AWS_BUCKET }} + source_dir: "dist/" + destination_dir: v3/spc-bin/latest/ + endpoint: ${{ secrets.AWS_ENDPOINT }} + + - name: "Deploy to self-hosted OSS (versioned)" + # only run this step if the repository is static-php-cli and is release tag + if: ${{ github.repository == 'crazywhalecc/static-php-cli' && startsWith(github.ref, 'refs/tags/') }} + uses: static-php/upload-s3-action@v1.0.0 + with: + aws_key_id: ${{ secrets.AWS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_bucket: ${{ secrets.AWS_BUCKET }} + source_dir: "dist/" + destination_dir: v3/spc-bin/${{ github.ref_name }}/ + endpoint: ${{ secrets.AWS_ENDPOINT }} + + - name: "Upload Artifact" + uses: actions/upload-artifact@v8 + with: + path: spc${{ env.SUFFIX }} + name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} + test-spc: + name: "Test SPC Binary for ${{ matrix.operating-system.name }}" + runs-on: ${{ matrix.operating-system.os }} + needs: [build-release-artifacts] + strategy: + matrix: + operating-system: + - name: "linux-x86_64" + os: "ubuntu-latest" + - name: "macos-x86_64" + os: "macos-15-intel" + - name: "linux-aarch64" + os: "ubuntu-24.04-arm" + - name: "macos-aarch64" + os: "macos-15" + - name: "windows-x64" + os: "windows-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v5 + + - name: "Download Artifact" + uses: actions/download-artifact@v5 + env: + SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} + with: + name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} + + - name: "Chmod" + if: matrix.operating-system.name != 'windows-x64' + run: chmod +x spc + + - name: "Run SPC Tests" + env: + SUFFIX: ${{ matrix.operating-system.name == 'windows-x64' && '.exe' || '' }} + run: ./spc${{ env.SUFFIX }} dev:info php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f3c73180..2a4cdeff1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -206,7 +206,7 @@ jobs: - name: "Upload logs" if: ${{ always() && hashFiles('log/**') != '' }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v8 with: name: build-logs-${{ matrix.os }}-${{ matrix.php }} path: log From 182f4ee0d001fce9603ae0470a17e94f5a7d0412 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 13 Apr 2026 12:56:02 +0800 Subject: [PATCH 648/682] Too new, too new --- .github/workflows/build-unix.yml | 16 ++++++++-------- .github/workflows/build-windows-x86_64.yml | 8 ++++---- .github/workflows/release-build.yml | 2 +- .github/workflows/tests.yml | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 3b088273d..ea9fb6eec 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -276,7 +276,7 @@ jobs: # Upload debug logs - if: ${{ inputs.debug && failure() }} name: "Upload build logs on failure" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: spc-logs-${{ inputs.php-version }}-${{ inputs.os }} path: log/*.log @@ -284,7 +284,7 @@ jobs: # Upload cli executable - if: ${{ inputs.build-cli == true }} name: "Upload PHP cli SAPI" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: php-cli-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/php @@ -292,7 +292,7 @@ jobs: # Upload micro self-extracted executable - if: ${{ inputs.build-micro == true }} name: "Upload PHP micro SAPI" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: php-micro-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/micro.sfx @@ -300,7 +300,7 @@ jobs: # Upload fpm executable - if: ${{ inputs.build-fpm == true }} name: "Upload PHP fpm SAPI" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: php-fpm-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/php-fpm @@ -308,7 +308,7 @@ jobs: # Upload frankenphp executable - if: ${{ inputs['build-frankenphp'] == true }} name: "Upload FrankenPHP SAPI" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: php-frankenphp-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/bin/frankenphp @@ -316,17 +316,17 @@ jobs: # Upload extensions metadata - if: ${{ inputs['shared-extensions'] != '' }} name: "Upload shared extensions" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: php-shared-ext-${{ inputs.php-version }}-${{ inputs.os }} path: | buildroot/modules/*.so - - uses: actions/upload-artifact@v8 + - uses: actions/upload-artifact@v7 name: "Upload License Files" with: name: license-files-${{ inputs.php-version }}-${{ inputs.os }} path: buildroot/license/ - - uses: actions/upload-artifact@v8 + - uses: actions/upload-artifact@v7 name: "Upload Build Metadata" with: name: build-meta-${{ inputs.php-version }}-${{ inputs.os }} diff --git a/.github/workflows/build-windows-x86_64.yml b/.github/workflows/build-windows-x86_64.yml index 6fa9f25de..d9cece6a2 100644 --- a/.github/workflows/build-windows-x86_64.yml +++ b/.github/workflows/build-windows-x86_64.yml @@ -94,24 +94,24 @@ jobs: # Upload cli executable - if: ${{ inputs.build-cli == true }} - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: php-${{ inputs.version }} path: buildroot/bin/php.exe # Upload micro self-extracted executable - if: ${{ inputs.build-micro == true }} - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: micro-${{ inputs.version }} path: buildroot/bin/micro.sfx # Upload extensions metadata - - uses: actions/upload-artifact@v8 + - uses: actions/upload-artifact@v7 with: name: license-files path: buildroot/license/ - - uses: actions/upload-artifact@v8 + - uses: actions/upload-artifact@v7 with: name: build-meta path: | diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 6745fffb2..d6f987e1a 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -145,7 +145,7 @@ jobs: endpoint: ${{ secrets.AWS_ENDPOINT }} - name: "Upload Artifact" - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: path: spc${{ env.SUFFIX }} name: spc-${{ matrix.operating-system.name }}${{ env.SUFFIX }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a4cdeff1..0b2979f13 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -206,7 +206,7 @@ jobs: - name: "Upload logs" if: ${{ always() && hashFiles('log/**') != '' }} - uses: actions/upload-artifact@v8 + uses: actions/upload-artifact@v7 with: name: build-logs-${{ matrix.os }}-${{ matrix.php }} path: log From 3ff0742ff163d835f65c439a52931b384da689b1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 16 Apr 2026 14:08:06 +0800 Subject: [PATCH 649/682] Enhance error handling in artifact downloading process --- src/StaticPHP/Artifact/ArtifactDownloader.php | 8 +++--- .../Exception/DownloaderException.php | 13 +++++++++- src/StaticPHP/Exception/ExceptionHandler.php | 25 ++++++++++++++++++- src/StaticPHP/Runtime/Shell/Shell.php | 5 ++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 6cc57439e..aefe471b3 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -545,6 +545,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, } $try = false; + $last_exception = null; foreach ($queue as $item) { try { $instance = null; @@ -605,6 +606,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } + $last_exception = $e; $try = true; continue; } catch (ValidationException $e) { @@ -612,11 +614,12 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Validation failed: {$e->getMessage()}"); } + $last_exception = $e; break; } } $vvv = !ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-v`, `-vv` or `-vvv` to enable verbose mode, or disable parallel downloading for more details." : ''; - throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}"); + throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}", previous: $last_exception, artifact_name: $artifact->getName()); } private function downloadWithConcurrency(): void @@ -672,8 +675,7 @@ private function downloadWithConcurrency(): void $artifact_name = $artifact->getName(); } $failed_downloads[] = ['artifact' => $artifact_name, 'error' => $e]; - InteractiveTerm::setMessage("[{$downloaded}/{$total}] Download failed: {$artifact_name}"); - InteractiveTerm::advance(); + throw $e; } // remove from pool unset($fiber_pool[$index]); diff --git a/src/StaticPHP/Exception/DownloaderException.php b/src/StaticPHP/Exception/DownloaderException.php index 50e828445..0a3da3327 100644 --- a/src/StaticPHP/Exception/DownloaderException.php +++ b/src/StaticPHP/Exception/DownloaderException.php @@ -10,4 +10,15 @@ * This exception is used to indicate that a download operation has failed, * typically due to network issues, invalid URLs, or other related problems. */ -class DownloaderException extends SPCException {} +class DownloaderException extends SPCException +{ + public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, private readonly ?string $artifact_name = null) + { + parent::__construct($message, $code, $previous); + } + + public function getArtifactName(): ?string + { + return $this->artifact_name; + } +} diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 053d82a3f..2d8c404d5 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -72,7 +72,7 @@ public static function handleSPCException(SPCException $e): int } } if (!ApplicationContext::isDebug()) { - self::logError('⚠ If you want to see more details in console, use `-vvv` option.'); + self::logError('⚠ If you want to see more details in console, use `-v`, `-vv` or `-vvv` option.'); } return self::getReturnCode($e); } @@ -232,6 +232,9 @@ private static function printModuleErrorInfo(SPCException $e): void if ($e instanceof ExecutionException) { self::logError(''); self::logError('Failed command: ' . ConsoleColor::gray($e->getExecutionCommand())); + if ($e->getCode() !== 0) { + self::logError(' - Exit code: ' . ConsoleColor::gray((string) $e->getCode())); + } if ($cd = $e->getCd()) { self::logError(' - Command executed in: ' . ConsoleColor::gray($cd)); } @@ -243,6 +246,26 @@ private static function printModuleErrorInfo(SPCException $e): void } } + // get downloader info + if ($e instanceof DownloaderException) { + if ($artifact_name = $e->getArtifactName()) { + self::logError('Failed artifact: ' . ConsoleColor::gray($artifact_name)); + } + $cause = $e->getPrevious(); + if ($cause instanceof ExecutionException) { + self::logError(''); + self::logError('Last failed command: ' . ConsoleColor::gray($cause->getExecutionCommand())); + if ($cause->getCode() !== 0) { + self::logError(' - Exit code: ' . ConsoleColor::gray((string) $cause->getCode())); + } + if ($cd = $cause->getCd()) { + self::logError(' - Command executed in: ' . ConsoleColor::gray($cd)); + } + } elseif ($cause instanceof DownloaderException || $cause instanceof ValidationException) { + self::logError('Cause: ' . ConsoleColor::gray($cause->getMessage())); + } + } + // validation error if ($e instanceof ValidationException) { self::logError('Failed validation module: ' . ConsoleColor::gray($e->getValidationModuleString())); diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index f9f4f1759..37601997c 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -174,6 +174,7 @@ protected function passthru( $process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env, options: PHP_OS_FAMILY === 'Windows' ? ['create_process_group' => true] : null); $output_value = ''; + $process_completed = false; try { if (!is_resource($process)) { throw new ExecutionException( @@ -251,11 +252,15 @@ protected function passthru( } } + $process_completed = true; return [ 'code' => $status['exitcode'], 'output' => $output_value, ]; } finally { + if (!$process_completed && is_resource($process)) { + proc_terminate($process); + } fclose($pipes[1]); fclose($pipes[2]); if ($file_res !== null) { From e3b07d701ea2ab168a1b2b87d59a01fe401c7da9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 16 Apr 2026 14:32:29 +0800 Subject: [PATCH 650/682] Remove unused fail download elements --- src/StaticPHP/Artifact/ArtifactDownloader.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index aefe471b3..f52036c64 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -650,7 +650,6 @@ private function downloadWithConcurrency(): void }); InteractiveTerm::indicateProgress("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ..."); - $failed_downloads = []; while (true) { // fill pool while (count($fiber_pool) < $pool_count && ($artifact = array_shift($this->artifacts)) !== null) { @@ -674,7 +673,7 @@ private function downloadWithConcurrency(): void if (isset($artifact)) { $artifact_name = $artifact->getName(); } - $failed_downloads[] = ['artifact' => $artifact_name, 'error' => $e]; + logger()->debug("Download failed for artifact '{$artifact_name}': {$e->getMessage()}"); throw $e; } // remove from pool @@ -688,13 +687,6 @@ private function downloadWithConcurrency(): void } // all done if (count($this->artifacts) === 0 && count($fiber_pool) === 0) { - if (!empty($failed_downloads)) { - InteractiveTerm::finish('Download completed with ' . count($failed_downloads) . ' failure(s).', false); - foreach ($failed_downloads as $failure) { - InteractiveTerm::error("Failed to download '{$failure['artifact']}': {$failure['error']->getMessage()}"); - } - throw new DownloaderException('Failed to download ' . count($failed_downloads) . ' artifact(s). Please check your internet connection and try again.'); - } $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; InteractiveTerm::finish("Downloaded all {$total} artifacts.{$skip_msg}"); break; From a175c5862dbda0876fb51e5ff9ce82e32a94c222 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 16 Apr 2026 14:40:45 +0800 Subject: [PATCH 651/682] Update index --- docs/en/index.md | 19 +++++++++++-------- docs/index.md | 14 +++++++------- docs/zh/index.md | 17 ++++++++++------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/en/index.md b/docs/en/index.md index 7d80bf2f7..fb4937ce0 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -3,23 +3,26 @@ layout: home hero: - name: "Static PHP" - tagline: "Build standalone PHP binary on Linux, macOS, FreeBSD, Windows, with PHP project together, with popular extensions included." + name: "StaticPHP" + tagline: "StaticPHP is a powerful tool designed for building portable executables including PHP, extensions, and more." image: src: /images/static-php_nobg.png - alt: Static PHP CLI Logo + alt: StaticPHP Logo actions: - theme: brand text: Get Started - link: ./guide/ + link: /en/guide/ + - theme: alt + text: 中文文档 + link: /zh/ features: - - title: Static CLI Binary - details: You can easily compile a standalone php binary for general use. Including CLI, FPM sapi. + - title: Static PHP Binary + details: You can easily compile a standalone php binary for general use. Including cli, fpm, cgi, frankenphp SAPI. - title: Micro Self-Extracted Executable - details: You can compile a self-extracted executable and build with your php source code. + details: You can compile a self-extracted executable and build with your php source code using micro SAPI. - title: Dependency Management - details: static-php-cli comes with dependency management and supports installation of different types of PHP extensions. + details: StaticPHP comes with dependency management and supports installation of different types of PHP extensions, packages and libraries. --- + + + + diff --git a/docs-v2/.vitepress/components/Contributors.vue b/docs-v2/.vitepress/components/Contributors.vue new file mode 100644 index 000000000..c1f474b0c --- /dev/null +++ b/docs-v2/.vitepress/components/Contributors.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/docs-v2/.vitepress/components/DependencyUtil.js b/docs-v2/.vitepress/components/DependencyUtil.js new file mode 100644 index 000000000..fd1f75bd1 --- /dev/null +++ b/docs-v2/.vitepress/components/DependencyUtil.js @@ -0,0 +1,193 @@ +export function getMetaList(meta, type, name, list_name) { + if (meta.os === 'linux') { + return meta[type][name][list_name + '-linux'] ?? meta[type][name][list_name + '-unix'] ?? meta[type][name][list_name] ?? []; + } + if (meta.os === 'macos') { + return meta[type][name][list_name + '-macos'] ?? meta[type][name][list_name + '-unix'] ?? meta[type][name][list_name] ?? []; + } + if (meta.os === 'windows') { + return meta[type][name][list_name + '-windows'] ?? meta[type][name][list_name] ?? []; + } + return []; +} + +export function getExtDepends(meta, ext_name) { + return getMetaList(meta, 'ext', ext_name, 'ext-depends'); +} + +export function getExtSuggests(meta, ext_name) { + return getMetaList(meta, 'ext', ext_name, 'ext-suggests'); +} + +export function getExtLibDepends(meta, ext_name) { + const ls = getMetaList(meta, 'ext', ext_name, 'lib-depends'); + return ls; +} + +export function getExtLibSuggests(meta, ext_name) { + return getMetaList(meta, 'ext', ext_name, 'lib-suggests'); +} + +export function getLibDepends(meta, lib_name) { + return getMetaList(meta, 'lib', lib_name, 'lib-depends'); +} + +export function getLibSuggests(meta, lib_name) { + return getMetaList(meta, 'lib', lib_name, 'lib-suggests'); +} + +/** + * Obtain the dependent lib list according to the required ext list, and sort according to the dependency + * @param meta + * @param exts + */ +export function getExtLibsByDeps(meta, exts) { + const sorted = []; + const visited = new Set(); + const notIncludedExts = []; + exts.forEach((ext) => { + if (!visited.has(ext)) { + visitExtDeps(meta, ext, visited, sorted); + } + }); + + const sortedSuggests = []; + const visitedSuggests = new Set(); + const final = []; + exts.forEach((ext) => { + if (!visited.has(ext)) { + visitExtAllDeps(meta, ext, visitedSuggests, sortedSuggests); + } + }); + sortedSuggests.forEach((suggest) => { + if (sorted.indexOf(suggest) !== -1) { + final.push(suggest); + } + }); + const libs = []; + final.forEach((ext) => { + if (exts.indexOf(ext) === -1) { + notIncludedExts.push(ext); + } + getExtLibDepends(meta, ext).forEach((lib) => { + if (libs.indexOf(lib) === -1) { + libs.push(lib); + } + }); + }); + + return { exts: final, libs: getLibsByDeps(meta, libs), notIncludedExts: notIncludedExts }; +} + +export function getAllExtLibsByDeps(meta, exts) { + const sorted = []; + const visited = new Set(); + const notIncludedExts = []; + exts.forEach((ext) => { + if (!visited.has(ext)) { + visitExtAllDeps(meta, ext, visited, sorted); + } + }); + const libs = []; + sorted.forEach((ext) => { + if (exts.indexOf(ext) === -1) { + notIncludedExts.push(ext); + } + const allLibs = [...getExtLibDepends(meta, ext), ...getExtLibSuggests(meta, ext)]; + allLibs.forEach((dep) => { + if (libs.indexOf(dep) === -1) { + libs.push(dep); + } + }); + }); + return { exts: sorted, libs: getAllLibsByDeps(meta, libs), notIncludedExts: notIncludedExts }; +} + +export function getAllLibsByDeps(meta, libs) { + const sorted = []; + const visited = new Set(); + + libs.forEach((lib) => { + if (!visited.has(lib)) { + console.log('before visited'); + console.log(visited); + visitLibAllDeps(meta, lib, visited, sorted); + console.log('after visited'); + console.log(visited); + } + }); + return sorted; +} + +export function getLibsByDeps(meta, libs) { + const sorted = []; + const visited = new Set(); + + libs.forEach((lib) => { + if (!visited.has(lib)) { + visitLibDeps(meta, lib, visited, sorted); + } + }); + + const sortedSuggests = []; + const visitedSuggests = new Set(); + const final = []; + libs.forEach((lib) => { + if (!visitedSuggests.has(lib)) { + visitLibAllDeps(meta, lib, visitedSuggests, sortedSuggests); + } + }); + sortedSuggests.forEach((suggest) => { + if (sorted.indexOf(suggest) !== -1) { + final.push(suggest); + } + }); + return final; +} + +export function visitLibAllDeps(meta, lib_name, visited, sorted) { + if (visited.has(lib_name)) { + return; + } + visited.add(lib_name); + const allLibs = [...getLibDepends(meta, lib_name), ...getLibSuggests(meta, lib_name)]; + allLibs.forEach((dep) => { + visitLibDeps(meta, dep, visited, sorted); + }); + sorted.push(lib_name); +} + +export function visitLibDeps(meta, lib_name, visited, sorted) { + if (visited.has(lib_name)) { + return; + } + visited.add(lib_name); + getLibDepends(meta, lib_name).forEach((dep) => { + visitLibDeps(meta, dep, visited, sorted); + }); + sorted.push(lib_name); +} + +export function visitExtDeps(meta, ext_name, visited, sorted) { + if (visited.has(visited)) { + return; + } + visited.add(ext_name); + getExtDepends(meta, ext_name).forEach((dep) => { + visitExtDeps(meta, dep, visited, sorted); + }); + sorted.push(ext_name); +} + +export function visitExtAllDeps(meta, ext_name, visited, sorted) { + if (visited.has(ext_name)) { + return; + } + visited.add(ext_name); + + const allExts = [...getExtDepends(meta, ext_name), ...getExtSuggests(meta, ext_name)]; + allExts.forEach((dep) => { + visitExtDeps(meta, dep, visited, sorted); + }); + sorted.push(ext_name); +} \ No newline at end of file diff --git a/docs-v2/.vitepress/components/SearchTable.vue b/docs-v2/.vitepress/components/SearchTable.vue new file mode 100644 index 000000000..6cfdc680e --- /dev/null +++ b/docs-v2/.vitepress/components/SearchTable.vue @@ -0,0 +1,79 @@ + + + + + + + \ No newline at end of file diff --git a/docs-v2/.vitepress/config.ts b/docs-v2/.vitepress/config.ts new file mode 100644 index 000000000..8ddc9c1f2 --- /dev/null +++ b/docs-v2/.vitepress/config.ts @@ -0,0 +1,65 @@ +import sidebarEn from "./sidebar.en"; +import sidebarZh from "./sidebar.zh"; + + +// https://vitepress.dev/reference/site-config +export default { + title: "Static PHP", + description: "Build single static PHP binary, with PHP project together, with popular extensions included.", + locales: { + en: { + label: 'English', + lang: 'en', + themeConfig: { + nav: [ + {text: 'Guide', link: '/en/guide/',}, + {text: 'Advanced', link: '/en/develop/'}, + {text: 'Contributing', link: '/en/contributing/'}, + {text: 'FAQ', link: '/en/faq/'}, + ], + sidebar: sidebarEn, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2023-present crazywhalecc' + } + }, + }, + zh: { + label: '简体中文', + lang: 'zh', // optional, will be added as `lang` attribute on `html` tag + themeConfig: { + nav: [ + {text: '构建指南', link: '/zh/guide/'}, + {text: '进阶', link: '/zh/develop/'}, + {text: '贡献', link: '/zh/contributing/'}, + {text: 'FAQ', link: '/zh/faq/'}, + ], + sidebar: sidebarZh, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2023-present crazywhalecc' + } + }, + } + }, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + logo: '/images/static-php_nobg.png', + nav: [], + socialLinks: [ + {icon: 'github', link: 'https://github.com/crazywhalecc/static-php-cli'} + ], + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2023-present crazywhalecc' + }, + search: { + provider: 'algolia', + options: { + appId: 'IHJHUB1SF1', + apiKey: '8266d31cc2ffbd0e059f1c6e5bdaf8fc', + indexName: 'static-php docs', + }, + }, + } +} diff --git a/docs-v2/.vitepress/sidebar.en.ts b/docs-v2/.vitepress/sidebar.en.ts new file mode 100644 index 000000000..54bcb0e1a --- /dev/null +++ b/docs-v2/.vitepress/sidebar.en.ts @@ -0,0 +1,57 @@ +export default { + '/en/guide/': [ + { + text: 'Basic Build Guides', + items: [ + {text: 'Guide', link: '/en/guide/'}, + {text: 'Build (Local)', link: '/en/guide/manual-build'}, + {text: 'Build (CI)', link: '/en/guide/action-build'}, + {text: 'Supported Extensions', link: '/en/guide/extensions'}, + {text: 'Extension Notes', link: '/en/guide/extension-notes'}, + {text: 'Build Command Generator', link: '/en/guide/cli-generator'}, + {text: 'Environment Variables', link: '/en/guide/env-vars', collapsed: true,}, + {text: 'Dependency Table', link: '/en/guide/deps-map'}, + ] + }, + { + text: 'Extended Build Guides', + items: [ + {text: 'Troubleshooting', link: '/en/guide/troubleshooting'}, + {text: 'Build on Windows', link: '/en/guide/build-on-windows'}, + {text: 'Build with GNU libc', link: '/en/guide/build-with-glibc'}, + ], + } + ], + '/en/develop/': [ + { + text: 'Development', + items: [ + {text: 'Get Started', link: '/en/develop/'}, + {text: 'Project Structure', link: '/en/develop/structure'}, + {text: 'PHP Source Modification', link: '/en/develop/php-src-changes'}, + ], + }, + { + text: 'Module', + items: [ + {text: 'Doctor ', link: '/en/develop/doctor-module'}, + {text: 'Source', link: '/en/develop/source-module'}, + ] + }, + { + text: 'Extra', + items: [ + {text: 'Compilation Tools', link: '/en/develop/system-build-tools'}, + {text: 'craft.yml Configuration', link: '/zh/develop/craft-yml'}, + ] + } + ], + '/en/contributing/': [ + { + text: 'Contributing', + items: [ + {text: 'Contributing', link: '/en/contributing/'}, + ], + } + ], +}; diff --git a/docs-v2/.vitepress/sidebar.zh.ts b/docs-v2/.vitepress/sidebar.zh.ts new file mode 100644 index 000000000..63592b93b --- /dev/null +++ b/docs-v2/.vitepress/sidebar.zh.ts @@ -0,0 +1,57 @@ +export default { + '/zh/guide/': [ + { + text: '构建指南', + items: [ + {text: '指南', link: '/zh/guide/'}, + {text: '本地构建', link: '/zh/guide/manual-build'}, + {text: 'Actions 构建', link: '/zh/guide/action-build'}, + {text: '扩展列表', link: '/zh/guide/extensions'}, + {text: '扩展注意事项', link: '/zh/guide/extension-notes'}, + {text: '编译命令生成器', link: '/zh/guide/cli-generator'}, + {text: '环境变量列表', link: '/zh/guide/env-vars'}, + {text: '依赖关系图表', link: '/zh/guide/deps-map'}, + ] + }, + { + text: '扩展构建指南', + items: [ + {text: '故障排除', link: '/zh/guide/troubleshooting'}, + {text: '在 Windows 上构建', link: '/zh/guide/build-on-windows'}, + {text: '构建 GNU libc 兼容的二进制', link: '/zh/guide/build-with-glibc'}, + ], + } + ], + '/zh/develop/': [ + { + text: '开发指南', + items: [ + {text: '开发简介', link: '/zh/develop/'}, + {text: '项目结构简介', link: '/zh/develop/structure'}, + {text: '对 PHP 源码的修改', link: '/zh/develop/php-src-changes'}, + ], + }, + { + text: '模块', + items: [ + {text: 'Doctor 环境检查工具', link: '/zh/develop/doctor-module'}, + {text: '资源模块', link: '/zh/develop/source-module'}, + ] + }, + { + text: '其他', + items: [ + {text: '系统编译工具', link: '/zh/develop/system-build-tools'}, + {text: 'craft.yml 配置详解', link: '/zh/develop/craft-yml'}, + ] + } + ], + '/zh/contributing/': [ + { + text: '贡献指南', + items: [ + {text: '贡献指南', link: '/zh/contributing/'}, + ], + } + ], +}; diff --git a/docs-v2/.vitepress/theme/index.ts b/docs-v2/.vitepress/theme/index.ts new file mode 100644 index 000000000..06771bcf5 --- /dev/null +++ b/docs-v2/.vitepress/theme/index.ts @@ -0,0 +1,17 @@ +// docs/.vitepress/theme/index.ts +import DefaultTheme from 'vitepress/theme' +import {inBrowser, useData} from "vitepress"; +import {watchEffect} from "vue"; +import './style.css'; + +export default { + ...DefaultTheme, + setup() { + const { lang } = useData() + watchEffect(() => { + if (inBrowser) { + document.cookie = `nf_lang=${lang.value}; expires=Mon, 1 Jan 2024 00:00:00 UTC; path=/` + } + }) + } +} diff --git a/docs-v2/.vitepress/theme/style.css b/docs-v2/.vitepress/theme/style.css new file mode 100644 index 000000000..ccf440746 --- /dev/null +++ b/docs-v2/.vitepress/theme/style.css @@ -0,0 +1,24 @@ +/** override default styles */ +.vp-sponsor-grid-image { + max-height:36px !important; + max-width: 1000px !important; +} + + +.vp-doc .contributors-header h2 { + padding-top: 0; + border-top: none; +} + +.vp-doc .sponsors-header h2 { + padding-top: 0; + border-top: none; +} + +.dark .VPImage.logo { + filter: contrast(0.7); +} + +.dark .VPImage.image-src { + filter: contrast(0.7); +} diff --git a/docs/deps-craft-yml.md b/docs-v2/deps-craft-yml.md similarity index 100% rename from docs/deps-craft-yml.md rename to docs-v2/deps-craft-yml.md diff --git a/docs/deps-map-ext.md b/docs-v2/deps-map-ext.md similarity index 100% rename from docs/deps-map-ext.md rename to docs-v2/deps-map-ext.md diff --git a/docs/deps-map-lib.md b/docs-v2/deps-map-lib.md similarity index 100% rename from docs/deps-map-lib.md rename to docs-v2/deps-map-lib.md diff --git a/docs-v2/en/contributing/index.md b/docs-v2/en/contributing/index.md new file mode 100644 index 000000000..6dc5ecf55 --- /dev/null +++ b/docs-v2/en/contributing/index.md @@ -0,0 +1,63 @@ +# Contributing + +Thank you for being here, this project welcomes your contributions! + +## Contribution Guide + +If you have code or documentation to contribute, here's what you need to know first. + +1. What type of code are you contributing? (new extensions, bug fixes, security issues, project framework optimizations, documentation) +2. If you contribute new files or new snippets, is your code checked by `php-cs-fixer` and `phpstan`? +3. Have you fully read the [Developer Guide](../develop/) before contributing code? + +If you can answer the above questions and have made changes to the code, +you can initiate a Pull Request in the project GitHub repository in time. +After the code review is completed, the code can be modified according to the suggestion, or directly merged into the main branch. + +## Contribution Type + +The main purpose of this project is to compile statically linked PHP binaries, +and the command line processing function is written based on `symfony/console`. +Before development, if you are not familiar with it, +Check out the [symfony/console documentation](https://symfony.com/doc/current/components/console.html) first. + +### Security Update + +Because this project is basically a PHP project running locally, generally speaking, there will be no remote attacks. +But if you find such a problem, please **DO NOT submit a PR or Issue in the GitHub repository, +You need to contact the project maintainer (crazywhalecc) via [mail](mailto:admin@zhamao.me). + +### Fix Bugs + +Fixing bugs generally does not involve modification of the project structure and framework, +so if you can locate the wrong code and fix it directly, please submit a PR directly. + +### New Extensions + +For adding a new extension, +you need to understand some basic structure of the project and how to add a new extension according to the existing logic. +It will be covered in detail in the next section on this page. +In general, you will need: + +1. Evaluate whether the extension can be compiled inline into PHP. +2. Evaluate whether the extension's dependent libraries (if any) can be compiled statically. +3. Write library compile commands on different platforms. +4. Verify that the extension and its dependencies are compatible with existing extensions and dependencies. +5. Verify that the extension works normally in `cli`, `micro`, `fpm`, `embed` SAPIs. +6. Write documentation and add your extension. + +### Project Framework Optimization + +If you are already familiar with the working principle of `symfony/console`, +and at the same time want to make some modifications or optimizations to the framework of the project, +please understand the following things first: + +1. Adding extensions does not belong to project framework optimization, +but if you find that you have to optimize the framework when adding new extensions, +you need to modify the framework itself before adding extensions. +2. For some large-scale logical modifications (such as those involving LibraryBase, Extension objects, etc.), +it is recommended to submit an Issue or Draft PR for discussion first. +3. In the early stage of the project, it was a pure private development project, and there were some Chinese comments in the code. +After internationalizing your project you can submit a PR to translate these comments into English. +4. Please do not submit more useless code fragments in the code, +such as a large number of unused variables, methods, classes, and code that has been rewritten many times. diff --git a/docs-v2/en/develop/craft-yml.md b/docs-v2/en/develop/craft-yml.md new file mode 100644 index 000000000..e0aea228f --- /dev/null +++ b/docs-v2/en/develop/craft-yml.md @@ -0,0 +1,7 @@ +--- +aside: false +--- + +# craft.yml Configuration + + diff --git a/docs-v2/en/develop/doctor-module.md b/docs-v2/en/develop/doctor-module.md new file mode 100644 index 000000000..64aed4afc --- /dev/null +++ b/docs-v2/en/develop/doctor-module.md @@ -0,0 +1,70 @@ +# Doctor module + +The Doctor module is a relatively independent module used to check the system environment, which can be entered with the command `bin/spc doctor`, and the entry command class is in `DoctorCommand.php`. + +The Doctor module is a checklist with a series of check items and automatic repair items. +These items are stored in the `src/SPC/doctor/item/` directory, +And two Attributes are used as check item tags and auto-fix item tags: `#[AsCheckItem]` and `#[AsFixItem]`. + +Take the existing check item `if necessary tools are installed`, +which is used to check whether the packages necessary for compilation are installed in the macOS system. +The following is its source code: + +```php +use SPC\doctor\AsCheckItem; +use SPC\doctor\AsFixItem; +use SPC\doctor\CheckResult; + +#[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin', level: 997)] +public function checkCliTools(): ?CheckResult +{ + $missing = []; + foreach (self::REQUIRED_COMMANDS as $cmd) { + if ($this->findCommand($cmd) === null) { + $missing[] = $cmd; + } + } + if (!empty($missing)) { + return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]); + } + return CheckResult::ok(); +} +``` + +The first parameter of the attribute is the name of the check item, +and the following `limit_os` parameter restricts the check item to be triggered only under the specified system, +and `level` is the priority of executing the check item, the larger the number, the higher the priority higher. + +The `$this->findCommand()` method used in it is the method of `SPC\builder\traits\UnixSystemUtilTrait`, +the purpose is to find the location of the system command, and return NULL if it cannot be found. + +Each check item method should return a `SPC\doctor\CheckResult`: + +- When returning `CheckResult::fail()`, the first parameter is used to output the error prompt of the terminal, + and the second parameter is the name of the repair item when this check item can be automatically repaired. +- When `CheckResult::ok()` is returned, the check passed. You can also pass a parameter to return the check result, for example: `CheckResult::ok('OS supported')`. +- When returning `CheckResult::fail()`, if the third parameter is included, the array of the third parameter will be used as the parameter of `AsFixItem`. + +The following is the method for automatically repairing items corresponding to this check item: + +```php +#[AsFixItem('build-tools')] +public function fixBuildTools(array $missing): bool +{ + foreach ($missing as $cmd) { + try { + shell(true)->exec('brew install ' . escapeshellarg($cmd)); + } catch (RuntimeException) { + return false; + } + } + return true; +} +``` + +`#[AsFixItem()]` first parameter is the name of the fix item, and this method must return True or False. +When False is returned, the automatic repair failed and manual handling is required. + +In the code here, `shell()->exec()` is the method of executing commands of the project, +which is used to replace `exec()` and `system()`, and also provides debugging, obtaining execution status, +entering directories, etc. characteristic. diff --git a/docs-v2/en/develop/index.md b/docs-v2/en/develop/index.md new file mode 100644 index 000000000..33e4163f5 --- /dev/null +++ b/docs-v2/en/develop/index.md @@ -0,0 +1,35 @@ +# Start Developing + +Developing this project requires the installation and deployment of a PHP environment, +as well as some extensions and Composer commonly used in PHP projects. + +The development environment and running environment of the project are almost exactly the same. +You can refer to the **Manual Build** section to install system PHP or use the pre-built static PHP of this project as the environment. +I will not go into details here. + +Regardless of its purpose, this project itself is actually a `php-cli` program. You can edit and develop it as a normal PHP project. +At the same time, you need to understand the Shell languages of different systems. + +The current purpose of this project is to compile statically compiled independent PHP, +but the main part also includes compiling static versions of many dependent libraries, +so you can reuse this set of compilation logic to build independent binary versions of other programs, such as Nginx, etc. + +## Environment preparation + +A PHP environment is required to develop this project. You can use the PHP that comes with the system, +or you can use the static PHP built by this project. + +Regardless of which PHP you use, in your development environment you need to install these extensions: + +``` +curl,dom,filter,mbstring,openssl,pcntl,phar,posix,sodium,tokenizer,xml,xmlwriter +``` + +The static-php-cli project itself does not require so many extensions, but during the development process, +you will use tools such as Composer and PHPUnit, which require these extensions. + +> For micro self-executing binaries built by static-php-cli itself, only `pcntl,posix,mbstring,tokenizer,phar` is required. + +## Start development + +Continuing down to see the project structure documentation, you can learn how `static-php-cli` works. diff --git a/docs-v2/en/develop/php-src-changes.md b/docs-v2/en/develop/php-src-changes.md new file mode 100644 index 000000000..190702594 --- /dev/null +++ b/docs-v2/en/develop/php-src-changes.md @@ -0,0 +1,59 @@ +# Modifications to PHP source code + +During the static compilation process, static-php-cli made some modifications to the PHP source code +in order to achieve good compatibility, performance, and security. +The following is a description of the current modifications to the PHP source code. + +## Micro related patches + +Based on the patches provided by the phpmicro project, +static-php-cli has made some modifications to the PHP source code to meet the needs of static compilation. +The patches currently used by static-php-cli during compilation in the [patch list](https://github.com/easysoft/phpmicro/tree/master/patches) are: + +- static_opcache +- static_extensions_win32 +- cli_checks +- disable_huge_page +- vcruntime140 +- win32 +- zend_stream +- cli_static +- macos_iconv +- phar + +## PHP <= 8.1 libxml patch + +Because PHP only provides security updates for 8.1 and stops updating older versions, +static-php-cli applies the libxml compilation patch that has been applied in newer versions of PHP to PHP 8.1 and below. + +## gd extension Windows patch + +Compiling the gd extension under Windows requires major changes to the `config.w32` file. +static-php-cli has made some changes to the gd extension to make it easier to compile under Windows. + +## YAML extension Windows patch + +YAML extension needs to modify the `config.w32` file to compile under Windows. +static-php-cli has made some modifications to the YAML extension to make it easier to compile under Windows. + +## static-php-cli version information insertion + +When compiling, static-php-cli will insert the static-php-cli version information into the PHP version information for easy identification. + +## Add option to hardcode INI + +When using the `-I` parameter to hardcode INI into static PHP functionality, +static-php-cli will modify the PHP source code to insert the hardcoded content. + +## Linux system repair patch + +Some compilation environments may lack some system header files or libraries. +static-php-cli will automatically fix these problems during compilation, such as: + +- HAVE_STRLCAT missing problem +- HAVE_STRLCPY missing problem + +## Fiber issue fix patch for Windows + +When compiling PHP on Windows, there will be some issues with the Fiber extension. +static-php-cli will automatically fix these issues during compilation (modify `config.w32` in php-src). diff --git a/docs-v2/en/develop/source-module.md b/docs-v2/en/develop/source-module.md new file mode 100644 index 000000000..51c3ba3ca --- /dev/null +++ b/docs-v2/en/develop/source-module.md @@ -0,0 +1,372 @@ +# Source module + +The download source module of static-php-cli is a major module. +It includes dependent libraries, external extensions, PHP source code download methods and file decompression methods. +The download configuration file mainly involves the `source.json` and `pkg.json` file, which records the download method of all downloadable sources. + +The main commands involved in the download function are `bin/spc download` and `bin/spc extract`. +The `download` command is a downloader that downloads sources according to the configuration file, +and the `extract` command is an extractor that extract sources from downloaded files. + +Generally speaking, downloading sources may be slow because these sources come from various official websites, GitHub, +and other different locations. +At the same time, they also occupy a large space, so you can download the sources once and reuse them. + +The configuration file of the downloader is `source.json`, which contains the download methods of all sources. +You can add the source download methods you need, or modify the existing source download methods. + +The download configuration structure of each source is as follows. +The following is the source download configuration corresponding to the `libevent` extension: + +```json +{ + "libevent": { + "type": "ghrel", + "repo": "libevent/libevent", + "match": "libevent.+\\.tar\\.gz", + "provide-pre-built": true, + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +The most important field here is `type`. Currently, the types it supports are: + +- `url`: Directly use URL to download, for example: `https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`. +- `pie`: Download PHP extensions from Packagist using the PIE (PHP Installer for Extensions) standard. +- `ghrel`: Use the GitHub Release API to download, download the artifacts uploaded from the latest version released by maintainers. +- `ghtar`: Use the GitHub Release API to download. + Different from `ghrel`, `ghtar` is downloaded from the `source code (tar.gz)` in the latest Release of the project. +- `ghtagtar`: Use GitHub Release API to download. + Compared with `ghtar`, `ghtagtar` can find the latest one from the `tags` list and download the source code in `tar.gz` format + (because some projects only use `tag` release version). +- `bitbuckettag`: Download using BitBucket API, basically the same as `ghtagtar`, except this one applies to BitBucket. +- `git`: Clone the project directly from a Git address to download sources, applicable to any public Git repository. +- `filelist`: Use a crawler to crawl the Web download site that provides file index, + and get the latest version of the file name and download it. +- `custom`: If none of the above download methods are satisfactory, you can write `custom`, + create a new class under `src/SPC/store/source/`, extends `CustomSourceBase`, and write the download script yourself. + +## source.json Common parameters + +Each source file in source.json has the following params: + +- `license`: the open source license of the source code, see **Open Source License** section below +- `type`: must be one of the types mentioned above +- `path` (optional): release the source code to the specified directory instead of `source/{name}` +- `provide-pre-built` (optional): whether to provide precompiled binary files. + If `true`, it will automatically try to download precompiled binary files when running `bin/spc download` + +::: tip +The `path` parameter in `source.json` can specify a relative or absolute path. When specified as a relative path, the path is based on `source/`. +::: + +## Download type - url + +URL type sources refer to downloading files directly from the URL. + +The parameters included are: + +- `url`: The download address of the file, such as `https://example.com/file.tgz` +- `filename` (optional): The file name saved to the local area. If not specified, the file name of the url will be used. + +Example (download the imagick extension and extract it to the extension storage path of the php source code): + +```json +{ + "ext-imagick": { + "type": "url", + "url": "https://pecl.php.net/get/imagick", + "path": "php-src/ext/imagick", + "filename": "imagick.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## Download type - pie + +PIE (PHP Installer for Extensions) type sources refer to downloading PHP extensions from Packagist that follow the PIE standard. +This method automatically fetches extension information from the Packagist repository and downloads the appropriate distribution file. + +The parameters included are: + +- `repo`: The Packagist vendor/package name, such as `vendor/package-name` + +Example (download a PHP extension from Packagist using PIE): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +The PIE download type will automatically detect the extension information from Packagist metadata, +including the download URL, version, and distribution type. +The extension must be marked as `type: php-ext` or contain `php-ext` metadata in its Packagist package definition. +::: + +## Download type - ghrel + +ghrel will download files from Assets uploaded in GitHub Release. +First use the GitHub Release API to get the latest version, and then download the corresponding files according to the regular matching method. + +The parameters included are: + +- `repo`: GitHub repository name +- `match`: regular expression matching Assets files +- `prefer-stable`: Whether to download stable versions first (default is `false`) + +Example (download the libsodium library, matching the libsodium-x.y.tar.gz file in Release): + +```json +{ + "libsodium": { + "type": "ghrel", + "repo": "jedisct1/libsodium", + "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## Download type - ghtar + +ghtar will download the file from the GitHub Release Tag. +Unlike `ghrel`, `ghtar` will download the `source code (tar.gz)` from the latest Release of the project. + +The parameters included are: + +- `repo`: GitHub repository name +- `prefer-stable`: Whether to download stable versions first (default is `false`) + +Example (brotli library): + +```json +{ + "brotli": { + "type": "ghtar", + "repo": "google/brotli", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## Download type - ghtagtar + +Use the GitHub Release API to download. +Compared with `ghtar`, `ghtagtar` can find the latest one from the `tags` list and download the source code in `tar.gz` format +(because some projects only use the `tag` version). + +The parameters included are: + +- `repo`: GitHub repository name +- `prefer-stable`: Whether to download stable versions first (default is `false`) + +Example (gmp library): + +```json +{ + "gmp": { + "type": "ghtagtar", + "repo": "alisw/GMP", + "license": { + "type": "text", + "text": "EXAMPLE LICENSE" + } + } +} +``` + +## Download Type - bitbuckettag + +Download using BitBucket API, basically the same as `ghtagtar`, except this one works with BitBucket. + +The parameters included are: + +- `repo`: BitBucket repository name + +## Download type - git + +Clone the project directly from a Git address to download sources, applicable to any public Git repository. + +The parameters included are: + +- `url`: Git link (HTTPS only) +- `rev`: branch name + +```json +{ + "imap": { + "type": "git", + "url": "https://github.com/static-php/imap.git", + "rev": "master", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## Download type - filelist + +Use a crawler to crawl a web download site that provides a file index and get the latest version of the file name and download it. + +Note that this method is only applicable to static sites with page index functions such as mirror sites and GNU official websites. + +The parameters included are: + +- `url`: The URL of the page to crawl the latest version of the file +- `regex`: regular expression matching file names and download links + +Example (download the libiconv library from the GNU official website): + +```json +{ + "libiconv": { + "type": "filelist", + "url": "https://ftp.gnu.org/gnu/libiconv/", + "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/", + "license": { + "type": "file", + "path": "COPYING" + } + } +} +``` + +## Download type - custom + +If the above downloading methods are not satisfactory, you can write `custom`, +create a new class under `src/SPC/store/source/`, extends `CustomSourceBase`, and write the download script yourself. + +I won’t go into details here, you can look at `src/SPC/store/source/PhpSource.php` or `src/SPC/store/source/PostgreSQLSource.php` as examples. + +## pkg.json General parameters + +pkg.json stores non-source-code files, such as precompiled tools musl-toolchain and UPX. It includes: + +- `type`: The same type as `source.json` and different kinds of parameters. +- `extract` (optional): The path to decompress after downloading, the default is `pkgroot/{pkg_name}`. +- `extract-files` (optional): Extract only the specified files to the specified location after downloading. + +It should be noted that `pkg.json` does not involve compilation, modification and distribution of source code, +so there is no `license` open source license field. +And you cannot use the `extract` and `extract-files` parameters at the same time. + +Example (download nasm locally and extract only program files to PHP SDK): + +```json +{ + "nasm-x86_64-win": { + "type": "url", + "url": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip", + "extract-files": { + "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + } + } +} +``` + +The key name in `extract-files` is the file in the source folder, and the key value is the storage path. The storage path can use the following variables: + +- `{php_sdk_path}`: (Windows only) PHP SDK path +- `{pkg_root_path}`: `pkgroot/` +- `{working_dir}`: current working directory +- `{download_path}`: download directory +- `{source_path}`: source code decompression directory + +When `extract-files` does not use variables and is a relative path, the directory of the relative path is `{working_dir}`. + +## Open source license + +For `source.json`, each source file should contain an open source license. +The `license` field stores the open source license information. + +Each `license` contains the following parameters: + +- `type`: `file` or `text` +- `path`: the license file in the source code directory (required when `type` is `file`) +- `text`: License text (required when `type` is `text`) + +Example (yaml extension source code with LICENSE file): + +```json +{ + "yaml": { + "type": "git", + "path": "php-src/ext/yaml", + "rev": "php7", + "url": "https://github.com/php/pecl-file_formats-yaml", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +When an open source project has multiple licenses, multiple files can be specified: + +```json +{ + "libuv": { + "type": "ghtar", + "repo": "libuv/libuv", + "license": [ + { + "type": "file", + "path": "LICENSE" + }, + { + "type": "file", + "path": "LICENSE-extra" + } + ] + } +} +``` + +When the license of an open source project uses different files between versions, +`path` can be used as an array to list the possible license files: + +```json +{ + "redis": { + "type": "git", + "path": "php-src/ext/redis", + "rev": "release/6.0.2", + "url": "https://github.com/phpredis/phpredis", + "license": { + "type": "file", + "path": [ + "LICENSE", + "COPYING" + ] + } + } +} +``` diff --git a/docs-v2/en/develop/structure.md b/docs-v2/en/develop/structure.md new file mode 100644 index 000000000..e43b6d6d7 --- /dev/null +++ b/docs-v2/en/develop/structure.md @@ -0,0 +1,180 @@ +# Introduction to project structure + +static-php-cli mainly contains three logical components: sources, dependent libraries, and extensions. +These components contains 4 configuration files: `source.json`, `pkg.json`, `lib.json`, and `ext.json`. + +A complete process for building standalone static PHP is: + +1. Use the source download module `Downloader` to download specified or all source codes. + These sources include PHP source code, dependent library source code, and extension source code. +2. Use the source decompression module `SourceExtractor` to decompress the downloaded sources to the compilation directory. +3. Use the dependency tool to calculate the dependent extensions and dependent libraries of the currently added extension, + and then compile each library that needs to be compiled in the order of dependencies. +4. After building each dependent library using `Builder` under the corresponding operating system, install it to the `buildroot` directory. +5. If external extensions are included (the source code does not contain extensions within PHP), + copy the external extensions to the `source/php-src/ext/` directory. +6. Use `Builder` to build the PHP source code and build target to the `buildroot` directory. + +The project is mainly divided into several folders: + +- `bin/`: used to store program entry files, including `bin/spc`, `bin/spc-alpine-docker`, `bin/setup-runtime`. +- `config/`: Contains all the extensions and dependent libraries supported by the project, + as well as the download link and download methods of these sources. It is divided into files: `lib.json`, `ext.json`, `source.json`, `pkg.json`, `pre-built.json` . +- `src/`: The core code of the project, including the entire framework and commands for compiling various extensions and libraries. +- `vendor/`: The directory that Composer depends on, you do not need to make any modifications to it. + +The operating principle is to start a `ConsoleApplication` of `symfony/console`, and then parse the commands entered by the user in the terminal. + +## Basic command line structure + +`bin/spc` is an entry file, including the Unix common `#!/usr/bin/env php`, +which is used to allow the system to automatically execute with the PHP interpreter installed on the system. +After the project executes `new ConsoleApplication()`, the framework will automatically register them as commands. + +The project does not directly use the Command registration method and command execution method recommended by Symfony. Here are small changes: + +1. Each command uses the `#[AsCommand()]` Attribute to register the name and description. +2. Abstract `execute()` so that all commands are based on `BaseCommand` (which is based on `Symfony\Component\Console\Command\Command`), + and the execution code of each command itself is written in the `handle()` method . +3. Added variable `$no_motd` to `BaseCommand`, which is used to display the Figlet greeting when the command is executed. +4. `BaseCommand` saves `InputInterface` and `OutputInterface` as member variables. You can use `$this->input` and `$this->output` within the command class. + +## Basic source code structure + +The source code of the project is located in the `src/SPC` directory, +supports automatic loading of the PSR-4 standard, and contains the following subdirectories and classes: + +- `src/SPC/builder/`: The core compilation command code used to build libraries, + PHP and related extensions under different operating systems, and also includes some compilation system tool methods. +- `src/SPC/command/`: All commands of the project are here. +- `src/SPC/doctor/`: Doctor module, which is a relatively independent module used to check the system environment. + It can be entered using the command `bin/spc doctor`. +- `src/SPC/exception/`: exception class. +- `src/SPC/store/`: Classes related to storage, files and sources are all here. +- `src/SPC/util/`: Some reusable tool methods are here. +- `src/SPC/ConsoleApplication.php`: command line program entry file. + +If you have read the source code, you may find that there is also a `src/globals/` directory, +which is used to store some global variables, global methods, +and non-PSR-4 standard PHP source code that is relied upon during the build process, such as extension sanity check code etc. + +## Phar application directory issue + +Like other php-cli projects, spc itself has additional considerations for paths. +Because spc can run in multiple modes such as `php-cli directly`, `micro SAPI`, `php-cli with Phar`, `vendor with Phar`, etc., +there are ambiguities in various root directories. A complete explanation is given here. +This problem is generally common in the base class path selection problem of accessing files in PHP projects, especially when used with `micro.sfx`. + +Note that this may only be useful for you when developing Phar projects or PHP frameworks. + +> Next, we will treat `static-php-cli` (that is, spc) as a normal `php` command line program. You can understand spc as any of your own php-cli applications for reference. + +There are three basic constant theoretical values below. We recommend that you introduce these three constants when writing PHP projects: + +- `WORKING_DIR`: the working directory when executing PHP scripts + +- `SOURCE_ROOT_DIR` or `ROOT_DIR`: the root directory of the project folder, generally the directory where `composer.json` is located + +- `FRAMEWORK_ROOT_DIR`: the root directory of the framework used, which may be used by self-developed frameworks. Generally, the framework directory is read-only + +You can define these constants in your framework entry or cli applications to facilitate the use of paths in your project. + +The following are PHP built-in constant values, which have been defined inside the PHP interpreter: + +- `__DIR__`: the directory where the file of the currently executed script is located + +- `__FILE__`: the file path of the currently executed script + +### Git project mode (source) + +Git project mode refers to a framework or program itself stored in plain text in the current folder, and running through `php path/to/entry.php`. + +Assume that your project is stored in the `/home/example/static-php-cli/` directory, or your project is the framework itself, +which contains project files such as `composer.json`: + +``` +composer.json +src/App/MyCommand.app +vendor/* +bin/entry.php +``` + +We assume that the above constants are obtained from `src/App/MyCommand.php`: + +| Constant | Value | +|----------------------|------------------------------------------------------| +| `WORKING_DIR` | `/home/example/static-php-cli` | +| `SOURCE_ROOT_DIR` | `/home/example/static-php-cli` | +| `FRAMEWORK_ROOT_DIR` | `/home/example/static-php-cli` | +| `__DIR__` | `/home/example/static-php-cli/src/App` | +| `__FILE__` | `/home/example/static-php-cli/src/App/MyCommand.php` | + +In this case, the values of `WORKING_DIR`, `SOURCE_ROOT_DIR`, and `FRAMEWORK_ROOT_DIR` are exactly the same: `/home/example/static-php-cli`. + +The source code of the framework and the source code of the application are both in the current path. + +### Vendor library mode (vendor) + +The vendor library mode generally means that your project is a framework or is installed into the project as a composer dependency by other applications, +and the storage location is in the `vendor/author/XXX` directory. + +Suppose your project is `crazywhalecc/static-php-cli`, and you or others install this project in another project using `composer require`. + +We assume that static-php-cli contains all files except the `vendor` directory with the same `Git mode`, and get the constant value from `src/App/MyCommand`, +Directory constant should be: + +| Constant | Value | +|----------------------|--------------------------------------------------------------------------------------| +| `WORKING_DIR` | `/home/example/another-app` | +| `SOURCE_ROOT_DIR` | `/home/example/another-app` | +| `FRAMEWORK_ROOT_DIR` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli` | +| `__DIR__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App` | +| `__FILE__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | + +Here `SOURCE_ROOT_DIR` refers to the root directory of the project using `static-php-cli`. + +### Git project Phar mode (source-phar) + +Git project Phar mode refers to the mode of packaging the project directory of the Git project mode into a `phar` file. We assume that `/home/example/static-php-cli` will be packaged into a Phar file, and the directory has the following files: + +``` +composer.json +src/App/MyCommand.app +vendor/* +bin/entry.php +``` + +When packaged into `app.phar` and stored in the `/home/example/static-php-cli` directory, `app.phar` is executed at this time. Assuming that the `src/App/MyCommand` code is executed, the constant is obtained in the file: + +| Constant | Value | +|----------------------|----------------------------------------------------------------------| +| `WORKING_DIR` | `/home/example/static-php-cli` | +| `SOURCE_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | +| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | +| `__DIR__` | `phar:///home/example/static-php-cli/app.phar/src/App` | +| `__FILE__` | `phar:///home/example/static-php-cli/app.phar/src/App/MyCommand.php` | + +Because the `phar://` protocol is required to read files in the phar itself, the project root directory and the framework directory will be different from `WORKING_DIR`. + +### Vendor Library Phar Mode (vendor-phar) + +Vendor Library Phar Mode means that your project is installed as a framework in other projects and stored in the `vendor` directory. + +We assume that your project directory structure is as follows: + +``` +composer.json # Composer configuration file of the current project +box.json # Configuration file for packaging Phar +another-app.php # Entry file of another project +vendor/crazywhalecc/static-php-cli/* # Your project is used as a dependent library +``` + +When packaging these files under the directory `/home/example/another-app/` into `app.phar`, the value of the following constant for your project should be: + +| Constant | Value | +|----------------------|------------------------------------------------------------------------------------------------------| +| `WORKING_DIR` | `/home/example/another-app` | +| `SOURCE_ROOT_DIR` | `phar:///home/example/another-app/app.phar/` | +| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli` | +| `__DIR__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App` | +| `__FILE__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | diff --git a/docs-v2/en/develop/system-build-tools.md b/docs-v2/en/develop/system-build-tools.md new file mode 100644 index 000000000..48273f639 --- /dev/null +++ b/docs-v2/en/develop/system-build-tools.md @@ -0,0 +1,242 @@ +# Compilation Tools + +static-php-cli uses many system compilation tools when building static PHP. These tools mainly include: + +- `autoconf`: used to generate `configure` scripts. +- `make`: used to execute `Makefile`. +- `cmake`: used to execute `CMakeLists.txt`. +- `pkg-config`: Used to find the installation path of dependent libraries. +- `gcc`: used to compile C/C++ projects under Linux. +- `clang`: used to compile C/C++ projects under macOS. + +For Linux and macOS operating systems, +these tools can usually be installed through the package manager, which is written in the doctor module. +Theoretically we can also compile and download these tools manually, +but this will increase the complexity of compilation, so we do not recommend this. + +## Linux Compilation Tools + +For Linux systems, different distributions have different installation methods for compilation tools. +And for static compilation, the package management of some distributions cannot install libraries and tools for pure static compilation. +Therefore, for the Linux platform and its different distributions, +we currently provide a variety of compilation environment preparations. + +### Glibc Environment + +The glibc environment refers to the underlying `libc` library of the system +(that is, the C standard library that all programs written in C language are dynamically linked to) uses `glibc`, +which is the default environment for most distributions. +For example: Ubuntu, Debian, CentOS, RHEL, openSUSE, Arch Linux, etc. + +In the glibc environment, the package management and compiler we use point to glibc by default, +and glibc cannot be statically linked well. +One of the reasons it cannot be statically linked is that its network library `nss` cannot be compiled statically. + +For the glibc environment, in static-php-cli and spc in 2.0-RC8 and later, you can choose two ways to build static PHP: + +1. Use Docker to build, you can use `bin/spc-alpine-docker` to build, it will build an Alpine Linux docker image. +2. Use `bin/spc doctor --auto-fix` to install the `musl-wrapper` and `musl-cross-make` packages, and then build directly. +([Related source code](https://github.com/crazywhalecc/static-php-cli/blob/main/src/SPC/doctor/item/LinuxMuslCheck.php)) + +Generally speaking, the build results in these two environments are consistent, and you can choose according to actual needs. + +In the doctor module, static-php-cli will first detect the current Linux distribution. +If the current distribution is a glibc environment, you will be prompted to install the musl-wrapper and musl-cross-make packages. + +The process of installing `musl-wrapper` in the glibc environment is as follows: + +1. Download the specific version of [musl-wrapper source code](https://musl.libc.org/releases/) from the musl official website. +2. Use `gcc` installed from the package management to compile the musl-wrapper source code and generate `musl-libc` and other libraries: `./configure --disable-gcc-wrapper && make -j && sudo make install`. +3. The musl-wrapper related libraries will be installed in the `/usr/local/musl` directory. + +The process of installing `musl-cross-make` in the glibc environment is as follows: + +1. Download the precompiled [musl-cross-make](https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/) compressed package from dl.static-php.dev . +2. Unzip to the `/usr/local/musl` directory. + +::: tip +In the glibc environment, static compilation can be achieved by directly installing musl-wrapper, +but musl-wrapper only contains `musl-gcc` and not `musl-g++`, which means that C++ code cannot be compiled. +So we need musl-cross-make to provide `musl-g++`. + +The reason why the musl-cross-make package cannot be compiled directly locally is that +its compilation environment requirements are relatively high (requires more than 36GB of memory, compiled under Alpine Linux), +so we provide precompiled binary packages that can be used for all Linux distributions. + +At the same time, the package management of some distributions provides musl-wrapper, +but musl-cross-make needs to match the corresponding musl-wrapper version, +so we do not use package management to install musl-wrapper. + +Compiling musl-cross-make will be introduced in the **musl-cross-make Toolchain Compilation** section of this chapter. +::: + +### Musl Environment + +The musl environment refers to the system's underlying `libc` library that uses `musl`, +which is a lightweight C standard library that can be well statically linked. + +For the currently popular Linux distributions, Alpine Linux uses the musl environment, +so static-php-cli can directly build static PHP under Alpine Linux. +You only need to install basic compilation tools (such as `gcc`, `cmake`, etc.) directly from the package management. + +For other distributions, if your distribution uses the musl environment, +you can also use static-php-cli to build static PHP directly after installing the necessary compilation tools. + +::: tip +In the musl environment, static-php-cli will automatically skip the installation of musl-wrapper and musl-cross-make. +::: + +### Docker Environment + +The Docker environment refers to using Docker containers to build static PHP. You can use `bin/spc-alpine-docker` to build. +Before executing this command, you need to install Docker first, and then execute `bin/spc-alpine-docker` in the project root directory. + +After executing `bin/spc-alpine-docker`, static-php-cli will automatically download the Alpine Linux image and then build a `cwcc-spc-x86_64` or `cwcc-spc-aarch64` image. +Then all build process is performed within this image, which is equivalent to compiling in Alpine Linux. + +## musl-cross-make Toolchain Compilation + +In Linux, although you do not need to manually compile the musl-cross-make tool, +if you want to understand its compilation process, you can refer here. +Another important reason is that this may not be compiled using automated tools such as CI and Actions, +because the existing CI service compilation environment does not meet the compilation requirements of musl-cross-make, +and the configuration that meets the requirements is too expensive. + +The compilation process of musl-cross-make is as follows: + +Prepare an Alpine Linux environment (either directly installed or using Docker). +The compilation process requires more than **36GB** of memory, +so you need to compile on a machine with larger memory. +Without this much memory, compilation may fail. + +Then write the following content into the `config.mak` file: + +```makefile +STAT = -static --static +FLAG = -g0 -Os -Wno-error + +ifneq ($(NATIVE),) +COMMON_CONFIG += CC="$(HOST)-gcc ${STAT}" CXX="$(HOST)-g++ ${STAT}" +else +COMMON_CONFIG += CC="gcc ${STAT}" CXX="g++ ${STAT}" +endif + +COMMON_CONFIG += CFLAGS="${FLAG}" CXXFLAGS="${FLAG}" LDFLAGS="${STAT}" + +BINUTILS_CONFIG += --enable-gold=yes --enable-gprofng=no +GCC_CONFIG += --enable-static-pie --disable-cet --enable-default-pie +#--enable-default-pie + +CONFIG_SUB_REV = 888c8e3d5f7b +GCC_VER = 13.2.0 +BINUTILS_VER = 2.40 +MUSL_VER = 1.2.4 +GMP_VER = 6.2.1 +MPC_VER = 1.2.1 +MPFR_VER = 4.2.0 +LINUX_VER = 6.1.36 +``` + +And also you need to add `gcc-13.2.0.tar.xz.sha1` file, contents here: + +``` +5f95b6d042fb37d45c6cbebfc91decfbc4fb493c gcc-13.2.0.tar.xz +``` + +If you are using Docker to build, create a new `Dockerfile` file and write the following content: + +```dockerfile +FROM alpine:edge + +RUN apk add --no-cache \ +gcc g++ git make curl perl \ +rsync patch wget libtool \ +texinfo autoconf automake \ +bison tar xz bzip2 zlib \ +file binutils flex \ +linux-headers libintl \ +gettext gettext-dev icu-libs pkgconf \ +pkgconfig icu-dev bash \ +ccache libarchive-tools zip + +WORKDIR /opt + +RUN git clone https://git.zv.io/toolchains/musl-cross-make.git +WORKDIR /opt/musl-cross-make +COPY config.mak /opt/musl-cross-make +COPY gcc-13.2.0.tar.xz.sha1 /opt/musl-cross-make/hashes + +RUN make TARGET=x86_64-linux-musl -j || : +RUN sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h +RUN make TARGET=x86_64-linux-musl -j +RUN make TARGET=x86_64-linux-musl install -j +RUN tar cvzf x86_64-musl-toolchain.tgz output/* +``` + +If you are using Alpine Linux in a non-Docker environment, you can directly execute the commands in the Dockerfile, for example: + +```bash +apk add --no-cache \ +gcc g++ git make curl perl \ +rsync patch wget libtool \ +texinfo autoconf automake \ +bison tar xz bzip2 zlib \ +file binutils flex \ +linux-headers libintl \ +gettext gettext-dev icu-libs pkgconf \ +pkgconfig icu-dev bash \ +ccache libarchive-tools zip + +git clone https://git.zv.io/toolchains/musl-cross-make.git +# Copy config.mak to the working directory of musl-cross-make. +# You need to replace /path/to/config.mak with your config.mak file path. +cp /path/to/config.mak musl-cross-make/ +cp /path/to/gcc-13.2.0.tar.xz.sha1 musl-cross-make/hashes + +make TARGET=x86_64-linux-musl -j || : +sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h +make TARGET=x86_64-linux-musl -j +make TARGET=x86_64-linux-musl install -j +tar cvzf x86_64-musl-toolchain.tgz output/* +``` + +::: tip +All the above scripts are suitable for x86_64 architecture Linux. +If you need to build musl-cross-make for the ARM environment, just replace all `x86_64` above with `aarch64`. +::: + +This compilation process may fail due to insufficient memory, network problems, etc. +You can try a few more times, or use a machine with larger memory to compile. +If you encounter problems or you have better improvement solutions, go to [Discussion](https://github.com/crazywhalecc/static-php-cli-hosted/issues/1). + +## macOS Environment + +For macOS systems, the main compilation tool we use is `clang`, +which is the default compiler for macOS systems and is also the compiler of Xcode. + +Compiling under macOS mainly relies on Xcode or Xcode Command Line Tools. +You can download Xcode from the App Store, +or execute `xcode-select --install` in the terminal to install Xcode Command Line Tools. + +In addition, in the `doctor` environment check module, static-php-cli will check whether Homebrew, +compilation tools, etc. are installed on the macOS system. +If not, you will be prompted to install them. I will not go into details here. + +## FreeBSD Environment + +FreeBSD is also a Unix system, and its compilation tools are similar to macOS. +You can directly use the package management `pkg` to install `clang` and other compilation tools through the `doctor` command. + +## pkg-config Compilation (*nix only) + +If you observe the compilation log when using static-php-cli to build static PHP, you will find that no matter what is compiled, +`pkg-config` will be compiled first. This is because `pkg-config` is a library used to find dependencies. +In earlier versions of static-php-cli, we directly used the `pkg-config` tool installed by package management, +but this would cause some problems, such as: + +- Even if `PKG_CONFIG_PATH` is specified, `pkg-config` will try to find dependent packages from the system path. +- Since `pkg-config` will look for dependent packages from the system path, + if a dependent package with the same name exists in the system, compilation may fail. + +In order to avoid the above problems, we compile `pkg-config` into `buildroot/bin` in user mode and use it. +We use parameters such as `--without-sysroot` to avoid looking for dependent packages from the system path. diff --git a/docs-v2/en/faq/index.md b/docs-v2/en/faq/index.md new file mode 100644 index 000000000..b65dca463 --- /dev/null +++ b/docs-v2/en/faq/index.md @@ -0,0 +1,108 @@ +# FAQ + +Here will be some questions that you may encounter easily. There are currently many, but I need to take time to organize them. + +## What is the path of php.ini ? + +On Linux, macOS and FreeBSD, the path of `php.ini` is `/usr/local/etc/php/php.ini`. +On Windows, the path is `C:\windows\php.ini` or the current directory of `php.exe`. +The directory where to look for `php.ini` can be changed on *nix using the manual build option `--with-config-file-path`. + +In addition, on Linux, macOS and FreeBSD, `.ini` files present in the `/usr/local/etc/php/conf.d` directory will also be loaded. +On Windows, this path is empty by default. +The directory can be changed using the manual build option `--with-config-file-scan-dir`. + +`php.ini` will also be searched for in [the other standard locations](https://www.php.net/manual/configuration.file.php). + +## Can statically-compiled PHP install extensions? + +Because the principle of installing PHP extensions under the normal mode is to use `.so` type dynamic link library to install new extensions, +and we use the static link PHP compiled by this project. However, static linking has different definitions in different operating systems. + +First of all, for Linux systems, statically linked binaries will not link the system's dynamic link library. +Purely statically linked binaries (`build with -all-static`) cannot load dynamic libraries, so new extensions cannot be added. +At the same time, in pure static mode, you cannot use extensions such as `ffi` to load external `.so` modules. + +You can use the command `ldd buildroot/bin/php` to check whether the binary you built under Linux is purely statically linked. + +If you [build GNU libc based PHP](../guide/build-with-glibc), you can use the `ffi` extension to load external `.so` modules and load `.so` extensions with the same ABI. + +For example, you can use the following command to build a static PHP binary dynamically linked with glibc, +supporting FFI extensions and loading the `xdebug.so` extension of the same PHP version and the same TS type: + +```bash +bin/spc-gnu-docker download --for-extensions=ffi,xml --with-php=8.4 +bin/spc-gnu-docker build ffi,xml --build-cli --debug + +buildroot/bin/php -d "zend_extension=/path/to/php{PHP_VER}-{ts/nts}/xdebug.so" --ri xdebug +``` + +For macOS platform, almost all binaries under macOS cannot be truly purely statically linked, and almost all binaries will link macOS system libraries: `/usr/lib/libresolv.9.dylib` and `/usr/lib/libSystem.B.dylib`. +So on macOS, you can **directly** use SPC to build statically compiled PHP binaries with dynamically linked extensions: + +1. Build shared extension `xxx.so` using: `--build-shared=XXX` option. e.g. `bin/spc build bcmath,zlib --build-shared=xdebug --build-cli` +2. You will get `buildroot/modules/xdebug.so` and `buildroot/bin/php`. +3. The `xdebug.so` file could be used for php that version and thread-safe are the same. + +For the Windows platform, since officially built extensions (such as `php_yaml.dll`) force the use of the `php8.dll` dynamic library as a link, and statically built PHP does not include any dynamic libraries other than system libraries, +php.exe built by static-php cannot load officially built dynamic extensions. Since static-php-cli does not yet support building dynamic extensions, there is currently no way to load dynamic extensions with static-php. + +However, Windows can normally use the `FFI` extension to load other dll files and call them. + +## Can it support Oracle database extension? + +Some extensions that rely on closed source libraries, such as `oci8`, `sourceguardian`, etc., +they do not provide purely statically compiled dependent library files (`.a`), only dynamic dependent library files (`.so`). +These extensions cannot be compiled into static-php-cli using source code, so this project may never support these extensions. +However, in theory you can access and use such extensions under macOS and Linux according to the above questions. + +If you have a need for such extensions, or most people have needs for these closed-source extensions, +see the discussion on [standalone-php-cli](https://github.com/crazywhalecc/static-php-cli/discussions/58). Welcome to leave a message. + +## Does it support Windows? + +The project currently supports Windows, but the number of supported extensions is small. Windows support is not perfect. There are mainly the following problems: + +1. The compilation process of Windows is different from that of *nix, and the toolchain used is also different. The compilation tools used to compile the dependent libraries of each extension are almost completely different. +2. The demand for the Windows version will also be advanced based on the needs of all people who use this project. If many people need it, I will support related extensions as soon as possible. + +## Can I protect my source code with micro? + +You can't. micro.sfx is essentially combining php and php code into one file, +there is no process of compiling or encrypting the PHP code. + +First of all, php-src is the official interpreter of PHP code, and there is no PHP compiler compatible with mainstream branches on the market. +I saw on the Internet that there is a project called BPC (Binary PHP Compiler?) that can compile PHP into binary, +but there are many restrictions. + +The direction of encrypting and protecting the code is not the same as compiling. +After compiling, the code can also be obtained through reverse engineering and other methods. +The real protection is still carried out by means of packing and encrypting the code. + +Therefore, this project (static-php-cli) and related projects (lwmbs, swoole-cli) all provide a convenient compilation tool for php-src source code. +The phpmicro referenced by this project and related projects is only a package of PHP's sapi interface, not a compilation tool for PHP code. +The compiler for PHP code is a completely different project, so the extra cases are not taken into account. +If you are interested in encryption, you can consider using existing encryption technologies, +such as Swoole Compiler, Source Guardian, etc. + +## Unable to use ssl + +**Update: This issue has been fixed in the latest version of static-php-cli, which now reads the system's certificate file by default. If you still have problems, try the solution below.** + +When using curl, pgsql, etc. to request an HTTPS website or establish an SSL connection, there may be an `error:80000002:system library::No such file or directory` error. +This error is caused by statically compiled PHP without specifying `openssl.cafile` via `php.ini`. + +You can solve this problem by specifying `php.ini` before using PHP and adding `openssl.cafile=/path/to/your-cert.pem` in the INI. + +For Linux systems, you can download the [cacert.pem](https://curl.se/docs/caextract.html) file from the curl official website, or you can use the certificate file that comes with the system. +For the certificate locations of different distros, please refer to [Golang docs](https://go.dev/src/crypto/x509/root_linux.go). + +> INI configuration `openssl.cafile` cannot be set dynamically using the `ini_set()` function, because `openssl.cafile` is a `PHP_INI_SYSTEM` type configuration and can only be set in the `php.ini` file. + +## Why don't we support older versions of PHP? + +Because older versions of PHP have many problems, such as security issues, performance issues, and functional issues. +In addition, many older versions of PHP are not compatible with the latest dependency libraries, +which is one of the reasons why older versions of PHP are not supported. + +You can use older versions compiled earlier by static-php-cli, such as PHP 8.0, but earlier versions will not be explicitly supported. diff --git a/docs/en/guide/action-build.md b/docs-v2/en/guide/action-build.md similarity index 100% rename from docs/en/guide/action-build.md rename to docs-v2/en/guide/action-build.md diff --git a/docs/en/guide/build-on-windows.md b/docs-v2/en/guide/build-on-windows.md similarity index 100% rename from docs/en/guide/build-on-windows.md rename to docs-v2/en/guide/build-on-windows.md diff --git a/docs/en/guide/build-with-glibc.md b/docs-v2/en/guide/build-with-glibc.md similarity index 100% rename from docs/en/guide/build-with-glibc.md rename to docs-v2/en/guide/build-with-glibc.md diff --git a/docs-v2/en/guide/cli-generator.md b/docs-v2/en/guide/cli-generator.md new file mode 100644 index 000000000..87163d000 --- /dev/null +++ b/docs-v2/en/guide/cli-generator.md @@ -0,0 +1,16 @@ +--- +aside: false +--- + + + +# CLI Build Command Generator + +::: tip +The extensions selected below may contain extensions that are not supported by the selected operating system, +which may cause compilation to fail. Please check [Supported Extensions](./extensions) first. +::: + + diff --git a/docs-v2/en/guide/deps-map.md b/docs-v2/en/guide/deps-map.md new file mode 100644 index 000000000..79100041c --- /dev/null +++ b/docs-v2/en/guide/deps-map.md @@ -0,0 +1,26 @@ +--- +outline: 'deep' +--- + +# Dependency Table + +When compiling PHP, each extension and library has dependencies, which may be required or optional. +You can choose whether to include these optional dependencies. + +For example, when compiling the `gd` extension under Linux, +the `zlib,libpng` libraries and the `zlib` extension are forced to be compiled, +while the `libavif,libwebp,libjpeg,freetype` libraries are optional libraries and will not be compiled by default +unless specified by the `--with-libs=avif,webp,jpeg,freetype` option. + +- For optional extensions (optional features of extensions), you need to specify them manually at compile time, for example, to enable igbinary support for Redis: `bin/spc build redis,igbinary`. +- For optional libraries, you need to compile and specify them through the `--with-libs=XXX` option. +- If you want to enable all optional extensions, you can use `bin/spc build redis --with-suggested-exts`. +- If you want to enable all optional libraries, you can use `--with-suggested-libs`. + +## Extension Dependency Table + + + +## Library Dependency Table + + \ No newline at end of file diff --git a/docs-v2/en/guide/env-vars.md b/docs-v2/en/guide/env-vars.md new file mode 100644 index 000000000..11cf93399 --- /dev/null +++ b/docs-v2/en/guide/env-vars.md @@ -0,0 +1,121 @@ +# Environment variables + +All environment variables mentioned in the list on this page have default values unless otherwise noted. +You can override the default values by setting these environment variables. + +## Environment variables list + +Starting from version 2.3.5, we have centralized the environment variables in the `config/env.ini` file. +You can set environment variables by modifying this file. + +We divide the environment variables supported by static-php-cli into three types: + +- Global internal environment variables: declared after static-php-cli starts, you can use `getenv()` to get them internally in static-php-cli, and you can override them before starting static-php-cli. +- Fixed environment variables: declared after static-php-cli starts, you can only use `getenv()` to get them, but you cannot override them through shell scripts. +- Config file environment variables: declared before static-php-cli build, you can set these environment variables by modifying the `config/env.ini` file or through shell scripts. + +You can read the comments for each parameter in [config/env.ini](https://github.com/crazywhalecc/static-php-cli/blob/main/config/env.ini) to understand its purpose. + +## Custom environment variables + +Generally, you don't need to modify any of the following environment variables as they are already set to optimal values. +However, if you have special needs, you can set these environment variables to meet your needs +(for example, you need to debug PHP performance under different compilation parameters). + +If you want to use custom environment variables, you can use the `export` command in the terminal or set the environment variables directly before the command, for example: + +```shell +# export first +export SPC_CONCURRENCY=4 +bin/spc build mbstring,pcntl --build-cli + +# or direct use +SPC_CONCURRENCY=4 bin/spc build mbstring,pcntl --build-cli +``` + +Or, if you need to modify an environment variable for a long time, you can modify the `config/env.ini` file. + +`config/env.ini` is divided into three sections, `[global]` is globally effective, `[windows]`, `[macos]`, `[linux]` are only effective for the corresponding operating system. + +For example, if you need to modify the `./configure` command for compiling PHP, you can find the `SPC_CMD_PREFIX_PHP_CONFIGURE` environment variable in the `config/env.ini` file, and then modify its value. + +If your build conditions are more complex and require multiple `env.ini` files to switch, +we recommend that you use the `config/env.custom.ini` file. +In this way, you can specify your environment variables by writing additional override items +without modifying the default `config/env.ini` file. + +```ini +; This is an example of `config/env.custom.ini` file, +; we modify the `SPC_CONCURRENCY` and linux default CFLAGS passing to libs and PHP +[global] +SPC_CONCURRENCY=4 + +[linux] +SPC_DEFAULT_C_FLAGS="-O3" +``` + +## Library environment variables (Unix only) + +Starting from 2.2.0, static-php-cli supports custom environment variables for all compilation dependent library commands of macOS, Linux, FreeBSD and other Unix systems. + +In this way, you can adjust the behavior of compiling dependent libraries through environment variables at any time. +For example, you can set the optimization parameters for compiling the xxx library through `xxx_CFLAGS=-O0`. + +Of course, not every library supports the injection of environment variables. +We currently provide three wildcard environment variables with the suffixes: + +- `_CFLAGS`: CFLAGS for the compiler +- `_LDFLAGS`: LDFLAGS for the linker +- `_LIBS`: LIBS for the linker + +The prefix is the name of the dependent library, and the specific name of the library is subject to `lib.json`. +Among them, the library name with `-` needs to replace `-` with `_`. + +Here is an example of an optimization option that replaces the openssl library compilation: + +```shell +openssl_CFLAGS="-O0" +``` + +The library name uses the same name listed in `lib.json` and is case-sensitive. + +::: tip +When no relevant environment variables are specified, except for the following variables, the remaining values are empty by default: + +| var name | var default value | +|-----------------------|-------------------------------------------------------------------------------------------------| +| `pkg_config_CFLAGS` | macOS: `$SPC_DEFAULT_C_FLAGS -Wimplicit-function-declaration -Wno-int-conversion`, Other: empty | +| `pkg_config_LDFLAGS` | Linux: `--static`, Other: empty | +| `imagemagick_LDFLAGS` | Linux: `-static`, Other: empty | +| `imagemagick_LIBS` | macOS: `-liconv`, Other: empty | +| `ldap_LDFLAGS` | `-L$BUILD_LIB_PATH` | +| `openssl_CFLAGS` | Linux: `$SPC_DEFAULT_C_FLAGS`, Other: empty | +| others... | empty | +::: + +The following table is a list of library names that support customizing the above three variables: + +| lib name | +|-------------| +| brotli | +| bzip | +| curl | +| freetype | +| gettext | +| gmp | +| imagemagick | +| ldap | +| libargon2 | +| libavif | +| libcares | +| libevent | +| openssl | + +::: tip +Because adapting custom environment variables to each library is a particularly tedious task, +and in most cases you do not need custom environment variables for these libraries, +so we currently only support custom environment variables for some libraries. + +If the library you need to customize environment variables is not listed above, +you can submit your request through [GitHub Issue](https://github.com/crazywhalecc/static-php-cli/issues). +::: diff --git a/docs-v2/en/guide/extension-notes.md b/docs-v2/en/guide/extension-notes.md new file mode 100644 index 000000000..7096c0ee6 --- /dev/null +++ b/docs-v2/en/guide/extension-notes.md @@ -0,0 +1,168 @@ +# Extension Notes + +Because it is a static compilation, extensions will not compile 100% perfectly, +and different extensions have different requirements for PHP and the environment, +which will be listed one by one here. + +## curl + +HTTP3 support is not enabled by default, compile with `--with-libs="nghttp2,nghttp3,ngtcp2"` to enable HTTP3 support for PHP >= 8.4. + +When using curl to request HTTPS, there may be an `error:80000002:system library::No such file or directory` error. +For details on the solution, see [FAQ - Unable to use ssl](../faq/#unable-to-use-ssl). + +## phpmicro + +1. Only PHP >= 8.0 is supported. + +## swoole + +1. swoole >= 5.0 Only PHP >= 8.0 is supported. +2. swoole Currently, curl hooks are not supported for PHP 8.0.x (which may be fixed in the future). +3. When compiling, if only `swoole` extension is included, the supported Swoole database coroutine hook will not be fully enabled. + If you need to use it, please add the corresponding `swoole-hook-xxx` extension. +4. The `zend_mm_heap corrupted` problem may occur in swoole under some extension combinations. The cause has not yet been found. + +## swoole-hook-pgsql + +swoole-hook-pgsql is not an extension, it's a Hook feature of Swoole. +If you use `swoole,swoole-hook-pgsql`, you will enable Swoole's PostgreSQL client and the coroutine mode of the `pdo_pgsql` extension. + +swoole-hook-pgsql conflicts with the `pdo_pgsql` extension. If you want to use Swoole and `pdo_pgsql`, please delete the pdo_pgsql extension and enable `swoole` and `swoole-hook-pgsql`. +This extension contains an implementation of the coroutine environment for `pdo_pgsql`. + +On macOS systems, `pdo_pgsql` may not be able to connect to the postgresql server normally, please use it with caution. + +## swoole-hook-mysql + +swoole-hook-mysql is not an extension, it's a Hook feature of Swoole. +If you use `swoole,swoole-hook-mysql`, you will enable the coroutine mode of Swoole's `mysqlnd` and `pdo_mysql`. + +## swoole-hook-sqlite + +swoole-hook-sqlite is not an extension, it's a Hook feature of Swoole. +If you use `swoole,swoole-hook-sqlite`, you will enable the coroutine mode of Swoole's `pdo_sqlite` (Swoole must be 5.1 or above). + +swoole-hook-sqlite conflicts with the `pdo_sqlite` extension. If you want to use Swoole and `pdo_sqlite`, please delete the pdo_sqlite extension and enable `swoole` and `swoole-hook-sqlite`. +This extension contains an implementation of the coroutine environment for `pdo_sqlite`. + +## swoole-hook-odbc + +swoole-hook-odbc is not an extension, it's a Hook feature of Swoole. +If you use `swoole,swoole-hook-odbc`, you will enable the coroutine mode of Swoole's `odbc` extension. + +swoole-hook-odbc conflicts with the `pdo_odbc` extension. If you want to use Swoole and `pdo_odbc`, please delete the `pdo_odbc` extension and enable `swoole` and `swoole-hook-odbc`. +This extension contains an implementation of the coroutine environment for `pdo_odbc`. + +## swow + +1. Only PHP 8.0+ is supported. + +## imagick + +1. OpenMP support is disabled, this is recommended by the maintainers and also the case system packages. + +## imap + +1. Kerberos is not supported +2. ext-imap is not thread safe due to the underlying c-client. It's not possible to use it in `--enable-zts` builds. +3. The extension was dropped from php 8.4, we recommend you look for an alternative implementation, such as [Webklex/php-imap](https://github.com/Webklex/php-imap) + +## gd + +1. gd Extension relies on more additional Graphics library. By default, +using `bin/spc build gd` directly will not support some Graphics library, such as `libjpeg`, `libavif`, etc. +Currently, it supports four libraries: `freetype,libjpeg,libavif,libwebp`. +Therefore, the following command can be used to introduce them into the gd library: + +```bash +bin/spc build gd --with-libs=freetype,libjpeg,libavif,libwebp --build-cli +``` + +## mcrypt + +1. Currently not supported, and this extension will not be supported in the future. [#32](https://github.com/crazywhalecc/static-php-cli/issues/32) + +## oci8 + +1. oci8 is an extension of the Oracle database, because the library on which the extension provided by Oracle does not provide a statically compiled version (`.a`) or source code, +and this extension cannot be compiled into php by static linking, so it cannot be supported. + +## xdebug + +1. Xdebug is only buildable as a shared extension. On Linux, you'll need to use a SPC_TARGET like `native-native -dynamic` or `native-native-gnu`. +2. When using Linux/glibc or macOS, you can compile Xdebug as a shared extension using --build-shared="xdebug". + The compiled `./php` binary can be configured and run by specifying the INI, eg `./php -d 'zend_extension=/path/to/xdebug.so' your-code.php`. + +## xml + +1. xml includes xml, xmlreader, xmlwriter, xsl, dom, simplexml, etc. + When adding xml extensions, it is best to enable these extensions at the same time. +2. libxml is included in xml extension. Enabling xml is equivalent to enabling libxml. + +## glfw + +1. glfw depends on OpenGL, and linux environment also needs X11, which cannot be linked statically. +2. macOS platform, we can compile and link system builtin OpenGL and related libraries dynamically. + +## rar + +1. The rar extension currently has a problem when compiling phpmicro with the `common` extension collection in the macOS x86_64 environment. + +## pgsql + +~~pgsql ssl connection is not compatible with openssl 3.2.0. See:~~ + +- ~~~~ +- ~~~~ +- ~~~~ + +pgsql 16.2 has fixed this bug, now it's working. + +When pgsql uses SSL connection, there may be `error:80000002:system library::No such file or directory` error, +For details on the solution, see [FAQ - Unable to use ssl](../faq/#unable-to-use-ssl). + +## openssl + +When using openssl-based extensions (such as curl, pgsql and other network libraries), +there may be an `error:80000002:system library::No such file or directory` error. +For details on the solution, see [FAQ - Unable to use ssl](../faq/#unable-to-use-ssl). + +## password-argon2 + +1. password-argon2 is not a standard extension. The algorithm `PASSWORD_ARGON2ID` for the `password_hash` function needs libsodium or libargon2 to work. +2. using password-argon2 enables multithread support for this. + +## ffi + +1. Due to the limitation of musl libc's static linkage, you cannot use ffi because dynamic libraries cannot be loaded. + If you need to use the ffi extension, see [Compile PHP with GNU libc](./build-with-glibc). +2. macOS supports the ffi extension, but errors will occur when some kernels do not contain debugging symbols. +3. Windows x64 supports the ffi extension. + +## xhprof + +The xhprof extension consists of three parts: `xhprof_extension`, `xhprof_html`, `xhprof_libs`. +Only `xhprof_extension` is included in the compiled binary. +If you need to use xhprof, +please download the source code from [pecl.php.net/package/xhprof](http://pecl.php.net/package/xhprof) and specify the `xhprof_libs` and `xhprof_html` paths for use. + +## event + +If you enable event extension on macOS, the `openpty` will be disabled due to issue: + +- [static-php-cli#335](https://github.com/crazywhalecc/static-php-cli/issues/335) + +## parallel + +Parallel is only supported on PHP 8.0 ZTS and above. + +## spx + +1. SPX does not support Windows, and the official repository does not support static compilation. static-php-cli uses a [modified version](https://github.com/static-php/php-spx). + +## mimalloc + +1. This is not technically an extension, but a library. +2. Building with `--with-libs="mimalloc"` on Linux or macOS will override the default allocator. +3. This is experimental for now, but is recommended in threaded environments. diff --git a/docs-v2/en/guide/extensions.md b/docs-v2/en/guide/extensions.md new file mode 100644 index 000000000..9ce53f63d --- /dev/null +++ b/docs-v2/en/guide/extensions.md @@ -0,0 +1,23 @@ + + +# Extensions + +> - `yes`: supported +> - _blank_: not supported yet, or WIP +> - `no` with issue link: confirmed to be unavailable due to issue +> - `partial` with issue link: supported but not perfect due to issue + + + +::: tip +If an extension you need is missing, you can create a [Feature Request](https://github.com/crazywhalecc/static-php-cli/issues). + +Some extensions or libraries that the extension depends on will have some optional features. +For example, the gd library optionally supports libwebp, freetype, etc. +If you only use `bin/spc build gd --build-cli` they will not be included (static-php-cli defaults to the minimum dependency principle). + +For more information about optional libraries, see [Extensions, Library Dependency Map](./deps-map). +For optional libraries, you can also select an extension from the [Command Generator](./cli-generator) and then select optional libraries. +::: diff --git a/docs-v2/en/guide/index.md b/docs-v2/en/guide/index.md new file mode 100644 index 000000000..54e7840c5 --- /dev/null +++ b/docs-v2/en/guide/index.md @@ -0,0 +1,50 @@ +# Guide + +Static php cli is a tool used to build statically compiled PHP binaries, +currently supporting Linux and macOS systems. + +In the guide section, you will learn how to use static php cli to build standalone PHP programs. + +- [Build (local)](./manual-build) +- [Build (GitHub Actions)](./action-build) +- [Supported Extensions](./extensions) + +## Compilation Environment + +The following is the architecture support situation, where :gear: represents support for GitHub Action build, +:computer: represents support for local manual build, and empty represents temporarily not supported. + +| | x86_64 | aarch64 | +|---------|-------------------|-------------------| +| macOS | :gear: :computer: | :gear: :computer: | +| Linux | :gear: :computer: | :gear: :computer: | +| Windows | :gear: :computer: | | +| FreeBSD | :computer: | :computer: | + +Current supported PHP versions for compilation: + +> :warning: Partial support, there may be issues with new beta versions and old versions. +> +> :heavy_check_mark: Supported +> +> :x: Not supported + +| PHP Version | Status | Comment | +|-------------|--------------------|-------------------------------------------------------------------------------------------------------------------------| +| 7.2 | :x: | | +| 7.3 | :x: | phpmicro and many extensions do not support 7.3, 7.4 versions | +| 7.4 | :x: | phpmicro and many extensions do not support 7.3, 7.4 versions | +| 8.0 | :warning: | PHP official has stopped maintaining 8.0, we no longer handle 8.0 related backport support | +| 8.1 | :warning: | PHP official only provides security updates for 8.1, we no longer handle 8.1 related backport support after 8.5 release | +| 8.2 | :heavy_check_mark: | | +| 8.3 | :heavy_check_mark: | | +| 8.4 | :heavy_check_mark: | | +| 8.5 (beta) | :warning: | PHP 8.5 is currently in beta stage | + +> This table shows the support status of static-php-cli for building corresponding versions, not the PHP official support status for that version. + +## PHP Support Versions + +Currently, static-php-cli supports PHP versions 8.2 ~ 8.5, and theoretically supports PHP 8.1 and earlier versions, just select the earlier version when downloading. +However, due to some extensions and special components that have stopped supporting earlier versions of PHP, static-php-cli will not explicitly support earlier versions. +We recommend that you compile the latest PHP version possible for a better experience. diff --git a/docs/en/guide/manual-build.md b/docs-v2/en/guide/manual-build.md similarity index 100% rename from docs/en/guide/manual-build.md rename to docs-v2/en/guide/manual-build.md diff --git a/docs-v2/en/guide/troubleshooting.md b/docs-v2/en/guide/troubleshooting.md new file mode 100644 index 000000000..7fc79e35d --- /dev/null +++ b/docs-v2/en/guide/troubleshooting.md @@ -0,0 +1,42 @@ +# Troubleshooting + +Various failures may be encountered in the process of using static-php-cli, +here will describe how to check the errors by yourself and report Issue. + +## Download Failure + +Problems with downloading resources are one of the most common problems with spc. +The main reason is that the addresses used for SPC download resources are generally the official website of the corresponding project or GitHub, etc., +and these websites may occasionally go down and block IP addresses. +After encountering a download failure, +you can try to call the download command multiple times. + +When downloading extensions, you may eventually see errors like `curl: (56) The requested URL returned error: 403` which are often caused by github rate limiting. +You can verify this by adding `--debug` to the command and will see something like `[DEBU] Running command (no output) : curl -sfSL "https://api.github.com/repos/openssl/openssl/releases"`. + +To fix this, [create](https://github.com/settings/tokens) a personal access token on GitHub and set it as an environment variable `GITHUB_TOKEN=`. + +If you confirm that the address is indeed inaccessible, +you can submit an Issue or PR to update the url or download type. + +## Doctor Can't Fix Something + +In most cases, the doctor module can automatically repair and install missing system environments, +but there are also special circumstances where the automatic repair function cannot be used normally. + +Due to system limitations (for example, software such as Visual Studio cannot be automatically installed under Windows), +the automatic repair function cannot be used for some projects. +When encountering a function that cannot be automatically repaired, +if you encounter the words `Some check items can not be fixed`, +it means that it cannot be automatically repaired. +Please submit an issue according to the method displayed on the terminal or repair the environment yourself. + +## Compile Error + +When you encounter a compilation error, if the `--debug` log is not enabled, please enable the debug log first, +and then determine the command that reported the error. +The error terminal output is very important for fixing compilation errors. +When submitting an issue, please upload the last error fragment of the terminal log (or the entire terminal log output), +and include the `spc` command and parameters used. + +If you are rebuilding, please refer to the [Local Build - Multiple Builds](./manual-build#multiple-builds) section. diff --git a/docs-v2/en/index.md b/docs-v2/en/index.md new file mode 100644 index 000000000..fb4937ce0 --- /dev/null +++ b/docs-v2/en/index.md @@ -0,0 +1,145 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "StaticPHP" + tagline: "StaticPHP is a powerful tool designed for building portable executables including PHP, extensions, and more." + image: + src: /images/static-php_nobg.png + alt: StaticPHP Logo + actions: + - theme: brand + text: Get Started + link: /en/guide/ + - theme: alt + text: 中文文档 + link: /zh/ + +features: + - title: Static PHP Binary + details: You can easily compile a standalone php binary for general use. Including cli, fpm, cgi, frankenphp SAPI. + - title: Micro Self-Extracted Executable + details: You can compile a self-extracted executable and build with your php source code using micro SAPI. + - title: Dependency Management + details: StaticPHP comes with dependency management and supports installation of different types of PHP extensions, packages and libraries. +--- + + + +
+
+

Special Sponsors

+

+ Thank you to our amazing sponsors for supporting this project! +

+
+ +
+ + + + diff --git a/docs/extension-notes.md b/docs-v2/extension-notes.md similarity index 100% rename from docs/extension-notes.md rename to docs-v2/extension-notes.md diff --git a/docs/extensions.md b/docs-v2/extensions.md similarity index 100% rename from docs/extensions.md rename to docs-v2/extensions.md diff --git a/docs-v2/index.md b/docs-v2/index.md new file mode 100644 index 000000000..75a1c3940 --- /dev/null +++ b/docs-v2/index.md @@ -0,0 +1,147 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "StaticPHP" + tagline: "StaticPHP is a powerful tool designed for building portable executables including PHP, extensions, and more." + image: + src: /images/static-php_nobg.png + alt: StaticPHP Logo + actions: + - theme: brand + text: Get Started + link: /en/guide/ + - theme: alt + text: 中文文档 + link: /zh/ + +features: + - title: Static PHP Binary + details: You can easily compile a standalone php binary for general use. Including cli, fpm, cgi, frankenphp SAPI. + - title: Micro Self-Extracted Executable + details: You can compile a self-extracted executable and build with your php source code using micro SAPI. + - title: Dependency Management + details: StaticPHP comes with dependency management and supports installation of different types of PHP extensions, packages and libraries. +--- + + + +
+
+

Special Sponsors

+

+ Thank you to our amazing sponsors for supporting this project! +

+
+ +
+ + + + + diff --git a/docs/public/CNAME b/docs-v2/public/CNAME similarity index 100% rename from docs/public/CNAME rename to docs-v2/public/CNAME diff --git a/docs-v2/public/images/beyondcode-seeklogo.png b/docs-v2/public/images/beyondcode-seeklogo.png new file mode 100644 index 0000000000000000000000000000000000000000..cacfd720d8c99113aa3aaabf7d13d33bd9dd3cb5 GIT binary patch literal 29354 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 KKUPZ{2mcQrDPi6K literal 0 HcmV?d00001 diff --git a/docs-v2/public/images/nativephp-logo.svg b/docs-v2/public/images/nativephp-logo.svg new file mode 100644 index 000000000..2b60ec2a2 --- /dev/null +++ b/docs-v2/public/images/nativephp-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs-v2/public/images/static-php_nobg.png b/docs-v2/public/images/static-php_nobg.png new file mode 100644 index 0000000000000000000000000000000000000000..64b6695c06c767a80509e66de527dd8f4291a54d GIT binary patch literal 988058 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

)V{aD;2+L^F<5n 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

g!$rHph{xq(ja8xD#8P}cKKkGRpg6yRg4 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�+{cO^j;FE3bo1 zH5roIY(AGkwi2y&CS{hlc5=JM5GR86r1!u3BWc&p>(Z%HQ)%Vu6`0dw`pcw?=G~qR z;)GdlH}tdcsb`bK{&--?!}5ZB?S5@d@y*8`*|sA;+g=J?OI*#%GaopX)b;aK8!;@e zno}C+Sg)WHg~Ku>3Pi-HlTp-L#cKR?9nR zCwF{zPn6@z^Ug~@_0w62d{;LT2%Q{CCPcJJQF9?_L)_$1fAF}!uC$bnN~~^xX%(lD_fnkEbu+@{x4l(YKi)!yDK0!4rto~rmzgIQx^pQgJrtz! z{&IQRc$t?oeF4g+0-!Dg!BF5_N6AWtjZFo`n+!C?b>cx~UFQm`6A7*W*&tPQT8LRfCg9xfKO2%K_p=Kq&3B7RZLb+QRu11V~>xf^DeqPb#d66C%Sh+ zA6GU#A3z00XC2tq#7%|B$pgPfxJGEjik`G-<7N(MJelr(;AYI;ccf8Hb=PGM$~*)D zK`S9<>SWG~h-dMqD#=;IJad$K7`e6=;+C*_fBkCTB?1MBe-%wFlyu3oCX)Vq%LBdr z8&S?5PFG*@hV(Dq{Gqg}Z(BMFot9x*ZGGm6pxoEV1FqP4FqOAm$j`%EMTzRky~~s7 zTep2Oeg5V@Oue)<58d=|xU7(841Jf<^paJfpgw-B@R9s8!v5H6F8xW)9e54A&CS}h zk4{$4D3Os#{e0(eY zzU|5P(ymm1oMa8S_)9;-q8$eWQ%h^xSDp-r{ourPTnK#ew=OJqC^=O+%NXqrQcXG0 z#q6_Jb2^@lb9#QRPY*?$j1|!3v@N$3v~eqS7fQXiS)??~DeL>vp+k44eVog2-*>;1 zzV(g2OJD!eN7LuO`g`fF?|uda{o9x`Kay4wdrjX44sAylrT_py07*naR7f32Jso|i zqoorIm(#S1Beb!Tw676x_T~laF88pKsZ5%Rb`|97GvcJ)lerRJ_}%Y}-9oDRum#`6 zZzYYKL3LO>|JR)53V(wLC@`%v`zGtnwm~(Al|u((zozVcYgVUwAJmlnc)IH1^V7fi z#rLFlz2nX4xmR75)~@bmP#R04LnoN*(gLNGF~2t!2&QQ#b~tTJJVkC~8rwzaN40K} z2hH1wlQ?6bAO|1Gi@~SC=OUc@LahSD�QmE8o~|iTy0WQYole4yqusxC;H?U1jwQ%;z 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 g> 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%0Dyhnz-QouABBr$ym%g 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 yW2^xsBIpuusaF%Z(zj?(Pd91}YPOynQ znLP(DoI3Bck!)6

+hD>da!h4+U9``C0m%z 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�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�{VZa4c7+y|1}1jyE&M^_@DW!Ip=wO>|~w^FDXsn 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ϖaBU2mIfHzjdD6We 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≫ZmIOhF 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%0A14Ky`)+J%JsISUeCS)$WAq?>T;Or;z2V%t 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%0D=YP1 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 z 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

#C#`1y&mc98-gf&M_XP}Y6q=T@NRNXYb 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 Cqd*=2 literal 0 HcmV?d00001 diff --git a/docs-v2/zh/contributing/index.md b/docs-v2/zh/contributing/index.md new file mode 100644 index 000000000..88461ac2b --- /dev/null +++ b/docs-v2/zh/contributing/index.md @@ -0,0 +1,51 @@ +# 贡献指南 + +感谢你能够看到这里,本项目非常欢迎你的贡献! + +## 贡献方法 + +如果你有代码或文档要贡献,以下是你需要首先了解的内容。 + +1. 你要贡献什么类型的代码?(新扩展、修复 Bug、安全问题、项目框架优化、文档) +2. 如果你贡献了新文件或新片段,你的代码是否经过 `php-cs-fixer` 和 `phpstan` 的检查? +3. 在贡献代码前是否充分阅读了 [开发指南](../develop/)? + +如果你能回答上述问题并对代码进行了修改,可以及时在项目 GitHub 仓库发起 Pull Request。 +代码审查完成后,可以根据建议修改代码,或直接合并到主分支。 + +## 贡献类型 + +本项目的主要目的是编译静态链接的 PHP 二进制文件,命令行处理功能基于 `symfony/console` 编写。 +在开发之前,如果你对它不够熟悉,请先查看 [symfony/console 文档](https://symfony.com/doc/current/components/console.html)。 + +### 安全更新 + +因为本项目基本上是一个本地运行的 PHP 项目,一般来说不会有远程攻击。 +但如果你发现此类问题,请**不要**在 GitHub 仓库提交 PR 或 Issue, +你需要通过 [邮件](mailto:admin@zhamao.me) 联系项目维护者(crazywhalecc)。 + +### 修复 Bug + +修复 Bug 一般不涉及项目结构和框架的修改,所以如果你能定位到错误代码并直接修复它,请直接提交 PR。 + +### 新扩展 + +对于添加新扩展,你需要了解项目的一些基本结构以及如何根据现有逻辑添加新扩展。 +这将在本页的下一节中详细介绍。 +总的来说,你需要: + +1. 评估扩展是否可以内联编译到 PHP 中。 +2. 评估扩展的依赖库(如果有)是否可以静态编译。 +3. 编写不同平台的库编译命令。 +4. 验证扩展及其依赖项与现有扩展和依赖项兼容。 +5. 验证扩展在 `cli`、`micro`、`fpm`、`embed` SAPIs 中正常工作。 +6. 编写文档并添加你的扩展。 + +### 项目框架优化 + +如果你已经熟悉 `symfony/console` 的工作原理,并同时要对项目的框架进行一些修改或优化,请先了解以下事情: + +1. 添加扩展不属于项目框架优化,但如果你在添加新扩展时发现必须优化框架,则需要先修改框架本身,然后再添加扩展。 +2. 对于一些大规模逻辑修改(例如涉及 LibraryBase、Extension 对象等的修改),建议先提交 Issue 或 Draft PR 进行讨论。 +3. 在项目早期,它是一个纯私有开发项目,代码中有一些中文注释。项目国际化后,你可以提交 PR 将这些注释翻译为英语。 +4. 请不要在代码中提交更多无用的代码片段,例如大量未使用的变量、方法、类以及多次重写的代码。 diff --git a/docs-v2/zh/develop/craft-yml.md b/docs-v2/zh/develop/craft-yml.md new file mode 100644 index 000000000..b96842f91 --- /dev/null +++ b/docs-v2/zh/develop/craft-yml.md @@ -0,0 +1,7 @@ +--- +aside: false +--- + +# craft.yml 配置 + + diff --git a/docs-v2/zh/develop/doctor-module.md b/docs-v2/zh/develop/doctor-module.md new file mode 100644 index 000000000..88931227e --- /dev/null +++ b/docs-v2/zh/develop/doctor-module.md @@ -0,0 +1,60 @@ +# Doctor 模块 + +Doctor 模块是一个较为独立的用于检查系统环境的模块,可使用命令 `bin/spc doctor` 进入,入口的命令类在 `DoctorCommand.php` 中。 + +Doctor 模块是一个检查单,里面有一系列的检查项目和自动修复项目。这些项目都存放在 `src/SPC/doctor/item/` 目录中, +并且使用了两种 Attribute 用作检查项标记和自动修复项目标记:`#[AsCheckItem]` 和 `#[AsFixItem]`。 + +以现有的检查项 `if necessary tools are installed`,它是用于检查编译必需的包是否安装在 macOS 系统内,下面是它的源码: + +```php +use SPC\doctor\AsCheckItem; +use SPC\doctor\AsFixItem; +use SPC\doctor\CheckResult; + +#[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin', level: 997)] +public function checkCliTools(): ?CheckResult +{ + $missing = []; + foreach (self::REQUIRED_COMMANDS as $cmd) { + if ($this->findCommand($cmd) === null) { + $missing[] = $cmd; + } + } + if (!empty($missing)) { + return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]); + } + return CheckResult::ok(); +} +``` + +属性的第一个参数就是检查项目的名称,后面的 `limit_os` 参数是限制了该检查项仅在指定的系统下触发,`level` 是执行该检查项的优先级,数字越大,优先级越高。 + +里面用到的 `$this->findCommand()` 方法为 `SPC\builder\traits\UnixSystemUtilTrait` 的方法,用途是查找系统命令所在位置,找不到时返回 NULL。 + +每个检查项的方法都应该返回一个 `SPC\doctor\CheckResult`: + +- 在返回 `CheckResult::fail()` 时,第一个参数用于输出终端的错误提示,第二个参数是在这个检查项可自动修复时的修复项目名称。 +- 在返回 `CheckResult::ok()` 时,表明检查通过。你也可以传递一个参数,用于返回检查结果,例如:`CheckResult::ok('OS supported')`。 +- 在返回 `CheckResult::fail()` 时,如果包含了第三个参数,第三个参数的数组将被当作 `AsFixItem` 的参数。 + +下面是这个检查项对应的自动修复项的方法: + +```php +#[AsFixItem('build-tools')] +public function fixBuildTools(array $missing): bool +{ + foreach ($missing as $cmd) { + try { + shell(true)->exec('brew install ' . escapeshellarg($cmd)); + } catch (RuntimeException) { + return false; + } + } + return true; +} +``` + +`#[AsFixItem()]` 属性传入的参数即修复项的名称,该方法必须返回 True 或 False。当返回 False 时,表明自动修复失败,需要手动处理。 + +此处的代码中 `shell()->exec()` 是项目的执行命令的方法,用于替代 `exec()`、`system()`,同时提供了 debug、获取执行状态、进入目录等特性。 diff --git a/docs-v2/zh/develop/index.md b/docs-v2/zh/develop/index.md new file mode 100644 index 000000000..85c9ad5fe --- /dev/null +++ b/docs-v2/zh/develop/index.md @@ -0,0 +1,27 @@ +# 开发简介 + +开发本项目需要安装部署 PHP 环境,以及一些 PHP 项目常用的扩展和 Composer。 + +项目的开发环境和运行环境几乎完全一致。你可以参照 **手动构建** 部分安装系统 PHP 或使用本项目预构建的静态 PHP 作为环境。这里不再赘述。 + +抛开用途,本项目本身其实就是一个 `php-cli` 程序,你可以将它当作一个正常的 PHP 项目进行编辑和开发,同时你需要了解不同系统的 Shell 命令行。 + +本项目目前的目的就是为了编译静态编译的独立 PHP,但主体部分也包含编译很多依赖库的静态版本,所以你可以复用这套编译逻辑,用于构建其他程序的独立二进制版本,例如 Nginx 等。 + +## 环境准备 + +开发本项目需要 PHP 环境。你可以使用系统自带的 PHP,也可以使用本项目构建的静态 PHP。 + +无论是使用哪种 PHP,在开发环境,你需要安装这些扩展: + +``` +curl,dom,filter,mbstring,openssl,pcntl,phar,posix,sodium,tokenizer,xml,xmlwriter +``` + +static-php-cli 项目本身不需要这么多扩展,但在开发过程中,你会用到 Composer 和 PHPUnit 等工具,它们需要这些扩展。 + +> 对于 static-php-cli 自身构建的 micro 自执行二进制,仅需要 `pcntl,posix,mbstring,tokenizer,phar`。 + +## 开始开发 + +继续向下查看项目结构文档,你可以学习 `static-php-cli` 是如何工作的。 diff --git a/docs-v2/zh/develop/php-src-changes.md b/docs-v2/zh/develop/php-src-changes.md new file mode 100644 index 000000000..5fabd3018 --- /dev/null +++ b/docs-v2/zh/develop/php-src-changes.md @@ -0,0 +1,51 @@ +# 对 PHP 源码的修改 + +由于 static-php-cli 在静态编译过程中为了实现良好的兼容性、性能和安全性,对 PHP 源码进行了一些修改。下面是目前对 PHP 源码修改的说明。 + +## micro 相关补丁 + +基于 phpmicro 项目提供的补丁,static-php-cli 对 PHP 源码进行了一些修改,以适应静态编译的需求。[补丁列表](https://github.com/easysoft/phpmicro/tree/master/patches) 包含: + +目前 static-php-cli 在编译时用到的补丁有: + +- static_opcache +- static_extensions_win32 +- cli_checks +- disable_huge_page +- vcruntime140 +- win32 +- zend_stream +- cli_static +- macos_iconv +- phar + +## PHP <= 8.1 libxml 补丁 + +因为 PHP 官方仅对 8.1 进行安全更新,旧版本停止更新,所以 static-php-cli 对 PHP 8.1 及以下版本应用了在新版本 PHP 中已经应用的 libxml 编译补丁。 + +## gd 扩展 Windows 补丁 + +在 Windows 下编译 gd 扩展需要大幅改动 `config.w32` 文件,static-php-cli 对 gd 扩展进行了一些修改,使其在 Windows 下编译更加方便。 + +## yaml 扩展 Windows 补丁 + +yaml 扩展在 Windows 下编译需要修改 `config.w32` 文件,static-php-cli 对 yaml 扩展进行了一些修改,使其在 Windows 下编译更加方便。 + +## static-php-cli 版本信息插入 + +static-php-cli 在编译时会在 PHP 版本信息中插入 static-php-cli 的版本信息,以便于识别。 + +## 加入硬编码 INI 的选项 + +在使用 `-I` 参数硬编码 INI 到静态 PHP 的功能中,static-php-cli 会修改 PHP 源码以插入硬编码内容。 + +## Linux 系统修复补丁 + +部分编译环境可能缺少一些头文件或库,static-php-cli 会在编译时自动修复这些问题,如: + +- HAVE_STRLCAT missing problem +- HAVE_STRLCPY missing problem + +## Windows 系统下 Fiber 问题修复补丁 + +在 Windows 下编译 PHP 时,Fiber 扩展会出现一些问题,static-php-cli 会在编译时自动修复这些问题(修改 php-src 的 `config.w32`)。 diff --git a/docs-v2/zh/develop/source-module.md b/docs-v2/zh/develop/source-module.md new file mode 100644 index 000000000..769ffa08b --- /dev/null +++ b/docs-v2/zh/develop/source-module.md @@ -0,0 +1,350 @@ +# 资源模块 + +static-php-cli 的下载资源模块是一个主要的功能,它包含了所依赖的库、外部扩展、PHP 源码的下载方式和资源解压方式。 +下载的配置文件主要涉及 `source.json` 和 `pkg.json` 文件,这个文件记录了所有可下载的资源的下载方式。 + +下载功能主要涉及的命令有 `bin/spc download` 和 `bin/spc extract`。其中 `download` 命令是一个下载器,它会根据配置文件下载资源; +`extract` 命令是一个解压器,它会根据配置文件解压资源。 + +一般来说,下载资源可能会比较慢,因为这些资源来源于各个官网、GitHub 等不同位置,同时它们也占用了较大空间,所以你可以在一次下载资源后,可重复使用。 + +下载器的配置文件是 `source.json`,它包含了所有资源的下载方式,你可以在其中添加你需要的资源下载方式,也可以修改已有的资源下载方式。 + +每个资源的下载配置结构如下,下面是 `libevent` 扩展对应的资源下载配置: + +```json +{ + "libevent": { + "type": "ghrel", + "repo": "libevent/libevent", + "match": "libevent.+\\.tar\\.gz", + "provide-pre-built": true, + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +这里最主要的字段是 `type`,目前它支持的类型有: + +- `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`。 +- `pie`: 使用 PIE(PHP Installer for Extensions)标准从 Packagist 下载 PHP 扩展。 +- `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。 +- `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 +- `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 +- `bitbuckettag`: 使用 BitBucket API 下载,基本和 `ghtagtar` 相同,只是这个适用于 BitBucket。 +- `git`: 直接从一个 Git 地址克隆项目来下载资源,适用于任何公开 Git 仓库。 +- `filelist`: 使用爬虫爬取提供文件索引的 Web 下载站点,并获取最新版本的文件名并下载。 +- `custom`: 如果以上下载方式都不能满足,你可以编写 `custom` 后,在 `src/SPC/store/source/` 下新建一个类,并继承 `CustomSourceBase`,自己编写下载脚本。 + +## source.json 通用参数 + +source.json 中每个源文件拥有以下字段: + +- `license`: 源代码的开源许可证,见下方 **开源许可证** 章节 +- `type`: 必须为上面提到的类型之一 +- `path`(可选): 释放源码到指定目录而非 `source/{name}` +- `provide-pre-built`(可选): 是否提供预编译的二进制文件,如果为 `true`,则会在 `bin/spc download` 时尝试自动下载预编译的二进制文件 + +::: tip +`source.json` 中的 `path` 参数可指定相对路径或绝对路径。当指定为相对路径时,路径基于 `source/`。 +::: + +## 下载类型 - url + +url 类型的资源指的是从 URL 直接下载文件。 + +包含的参数有: + +- `url`: 文件的下载地址,如 `https://example.com/file.tgz` +- `filename`(可选): 保存到本地的文件名,如不指定,则使用 url 的文件名 + +例子(下载 imagick 扩展,并解压缩到 php 源码的扩展存放路径): + +```json +{ + "ext-imagick": { + "type": "url", + "url": "https://pecl.php.net/get/imagick", + "path": "php-src/ext/imagick", + "filename": "imagick.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## 下载类型 - pie + +PIE(PHP Installer for Extensions)类型的资源是从 Packagist 下载遵循 PIE 标准的 PHP 扩展。 +该方法会自动从 Packagist 仓库获取扩展信息,并下载相应的分发文件。 + +包含的参数有: + +- `repo`: Packagist 的 vendor/package 名称,如 `vendor/package-name` + +例子(使用 PIE 从 Packagist 下载 PHP 扩展): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +PIE 下载类型会自动从 Packagist 元数据中检测扩展信息,包括下载 URL、版本和分发类型。 +扩展必须在其 Packagist 包定义中标记为 `type: php-ext` 或包含 `php-ext` 元数据。 +::: + +## 下载类型 - ghrel + +ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。 + +包含的参数有: + +- `repo`: GitHub 仓库名称 +- `match`: 匹配 Assets 文件的正则表达式 +- `prefer-stable`: 是否优先下载稳定版本(默认为 `false`) + +例子(下载 libsodium 库,匹配 Release 中的 libsodium-x.y.tar.gz 文件): + +```json +{ + "libsodium": { + "type": "ghrel", + "repo": "jedisct1/libsodium", + "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## 下载类型 - ghtar + +ghtar 会从 GitHub Release Tag 下载文件,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 + +包含的参数有: + +- `repo`: GitHub 仓库名称 +- `prefer-stable`: 是否优先下载稳定版本(默认为 `false`) + +例子(brotli 库): + +```json +{ + "brotli": { + "type": "ghtar", + "repo": "google/brotli", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## 下载类型 - ghtagtar + +使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 + +包含的参数有: + +- `repo`: GitHub 仓库名称 +- `prefer-stable`: 是否优先下载稳定版本(默认为 `false`) + +例子(gmp 库): + +```json +{ + "gmp": { + "type": "ghtagtar", + "repo": "alisw/GMP", + "license": { + "type": "text", + "text": "EXAMPLE LICENSE" + } + } +} +``` + +## 下载类型 - bitbuckettag + +使用 BitBucket API 下载,基本和 `ghtagtar` 相同,只是这个适用于 BitBucket。 + +包含的参数有: + +- `repo`: BitBucket 仓库名称 + +## 下载类型 - git + +直接从一个 Git 地址克隆项目来下载资源,适用于任何公开 Git 仓库。 + +包含的参数有: + +- `url`: Git 链接(仅限 HTTPS) +- `rev`: 分支名称 + +```json +{ + "imap": { + "type": "git", + "url": "https://github.com/static-php/imap.git", + "rev": "master", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +## 下载类型 - filelist + +使用爬虫爬取提供文件索引的 Web 下载站点,并获取最新版本的文件名并下载。 + +注意,该方法仅限于镜像站、GNU 官网等具有页面 index 功能的静态站点使用。 + +包含的参数有: + +- `url`: 要爬取文件最新版本的页面 URL +- `regex`: 匹配文件名及下载链接的正则表达式 + +例子(从 GNU 官网下载 libiconv 库): + +```json +{ + "libiconv": { + "type": "filelist", + "url": "https://ftp.gnu.org/gnu/libiconv/", + "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/", + "license": { + "type": "file", + "path": "COPYING" + } + } +} +``` + +## 下载类型 - custom + +如果以上下载方式都不能满足,你可以编写 `custom` 后,在 `src/SPC/store/source/` 下新建一个类,并继承 `CustomSourceBase`,自己编写下载脚本。 + +这里不再赘述,你可以查看 `src/SPC/store/source/PhpSource.php` 或 `src/SPC/store/source/PostgreSQLSource.php` 作为例子。 + +## pkg.json 通用参数 + +pkg.json 存放的是非源码类型的文件资源,例如 musl-toolchain、UPX 等预编译的工具。它的使用包含: + +- `type`: 与 `source.json` 相同的类型及不同种类的参数。 +- `extract`(可选): 下载后解压缩的路径,默认为 `pkgroot/{pkg_name}`。 +- `extract-files`(可选): 下载后仅解压指定的文件到指定位置。 + +需要注意的是,`pkg.json` 不涉及源代码的编译和修改分发,所以没有 `license` 开源许可证字段。并且你不能同时使用 `extract` 和 `extract-files` 参数。 + +例子(下载 nasm 到本地,并只提取程序文件到 PHP SDK): + +```json +{ + "nasm-x86_64-win": { + "type": "url", + "url": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip", + "extract-files": { + "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + } + } +} +``` + +`extract-files` 中的键名为源文件夹下的文件,键值为存放的路径。存放的路径可以使用以下变量: + +- `{php_sdk_path}`: (仅限 Windows)PHP SDK 路径 +- `{pkg_root_path}`: `pkgroot/` +- `{working_dir}`: 当前工作目录 +- `{download_path}`: 下载目录 +- `{source_path}`: 源码解压缩目录 + +当 `extract-files` 不使用变量且为相对路径时,相对路径的目录为 `{working_dir}`。 + +## 开源许可证 + +对于 `source.json` 而言,每个源文件都应包含开源许可证。`license` 字段存放了开源许可证的信息。 + +每个 `license` 包含的参数有: + +- `type`: `file` 或 `text` +- `path`: 源代码目录中的许可证文件(当 `type` 为 `file` 时,此项必填) +- `text`: 许可证文本(当 `type` 为 `text` 时,此项必填) + +例子(yaml 扩展的源代码中带有 LICENSE 文件): + +```json +{ + "yaml": { + "type": "git", + "path": "php-src/ext/yaml", + "rev": "php7", + "url": "https://github.com/php/pecl-file_formats-yaml", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +当开源项目拥有多个许可证时,可指定多个文件: + +```json +{ + "libuv": { + "type": "ghtar", + "repo": "libuv/libuv", + "license": [ + { + "type": "file", + "path": "LICENSE" + }, + { + "type": "file", + "path": "LICENSE-extra" + } + ] + } +} +``` + +当一个开源项目的许可证在不同版本间使用不同的文件,`path` 参数可以使用数组将可能的许可证文件列出: + +```json +{ + "redis": { + "type": "git", + "path": "php-src/ext/redis", + "rev": "release/6.0.2", + "url": "https://github.com/phpredis/phpredis", + "license": { + "type": "file", + "path": [ + "LICENSE", + "COPYING" + ] + } + } +} +``` diff --git a/docs-v2/zh/develop/structure.md b/docs-v2/zh/develop/structure.md new file mode 100644 index 000000000..16c3a9e4c --- /dev/null +++ b/docs-v2/zh/develop/structure.md @@ -0,0 +1,163 @@ +# 项目结构简介 + +static-php-cli 主要包含三种逻辑组件:资源、依赖库、扩展。这三种组件四个配置文件:`source.json`、`lib.json`、`ext.json`、`pkg.json`。 + +一个完整的构建静态 PHP 流程是: + +1. 使用资源下载模块 `Downloader` 下载指定或所有资源,这些资源包含 PHP 源码、依赖库源码、扩展源码。 +2. 使用资源解压模块 `SourceExtractor` 解压下载的资源到编译目录。 +3. 使用依赖工具计算出当前加入的扩展的依赖扩展、依赖库,然后对每个需要编译的依赖库进行编译,按照依赖顺序。 +4. 使用对应操作系统下的 `Builder` 构建每个依赖库后,将其安装到 `buildroot` 目录。 +5. 如果包含外部扩展(源码没有包含在 PHP 内的扩展),将外部扩展拷贝到 `source/php-src/ext/` 目录。 +6. 使用 `Builder` 构建 PHP 源码,将其安装到 `buildroot` 目录。 + +项目主要分为几个文件夹: + +- `bin/`: 用于存放程序入口文件,包含 `bin/spc`、`bin/spc-alpine-docker`、`bin/setup-runtime`。 +- `config/`: 包含了所有项目支持的扩展、依赖库以及这些资源下载的地址、下载方式等,:`lib.json`、`ext.json`、`source.json`、`pkg.json`、`pre-built.json`。 +- `src/SPC/`: 项目的核心代码,包含了整个框架以及编译各种扩展和库的命令。 +- `src/globals/`: 项目的全局方法和常量、运行时需要的测试文件(例如:扩展的可用性检查代码)。 +- `vendor/`: Composer 依赖的目录,你无需对它做出任何修改。 + +其中运行原理就是启动一个 `symfony/console` 的 `ConsoleApplication`,然后解析用户在终端输入的命令。 + +## 基本命令行结构 + +`bin/spc` 是一个 PHP 代码入口文件,包含了 Unix 通用的 `#!/usr/bin/env php` 用来让系统自动以系统安装好的 PHP 解释器执行。 +在项目执行了 `new ConsoleApplication()` 后,框架会自动使用反射的方式,解析 `src/SPC/command` 目录下的所有类,并将其注册成为命令。 + +项目并没有直接使用 Symfony 推荐的 Command 注册方式和命令执行方式,这里做出了一点小变动: + +1. 每个命令都使用 `#[AsCommand()]` Attribute 来注册名称和简介。 +2. 将 `execute()` 抽象化,让所有命令基于 `BaseCommand`(它基于 `Symfony\Component\Console\Command\Command`),每个命令本身的执行代码写到了 `handle()` 方法中。 +3. `BaseCommand` 添加了变量 `$no_motd`,用于是否在该命令执行时显示 Figlet 欢迎词。 +4. `BaseCommand` 将 `InputInterface` 和 `OutputInterface` 保存为成员变量,你可以在命令的类内使用 `$this->input` 和 `$this->output`。 + +## 基本源码结构 + +项目的源码位于 `src/SPC` 目录,支持 PSR-4 标准的自动加载,包含以下子目录和类: + +- `src/SPC/builder/`: 用于不同操作系统下构建依赖库、PHP 及相关扩展的核心编译命令代码,还包含了一些编译的系统工具方法。 +- `src/SPC/command/`: 项目的所有命令都在这里。 +- `src/SPC/doctor/`: Doctor 模块,它是一个较为独立的用于检查系统环境的模块,可使用命令 `bin/spc doctor` 进入。 +- `src/SPC/exception/`: 异常类。 +- `src/SPC/store/`: 有关存储、文件和资源的类都在这里。 +- `src/SPC/util/`: 一些可以复用的工具方法都在这里。 +- `src/SPC/ConsoleApplication.php`: 命令行程序入口文件。 + +如果你阅读过源码,你可能会发现还有一个 `src/globals/` 目录,它是用于存放一些全局变量、全局方法、构建过程中依赖的非 PSR-4 标准的 PHP 源码,例如测试扩展代码等。 + +## Phar 应用目录问题 + +和其他 php-cli 项目一样,spc 自身对路径有额外的考虑。 +因为 spc 可以在 `php-cli directly`、`micro SAPI`、`php-cli with Phar`、`vendor with Phar` 等多种模式下运行,各类根目录存在歧义。这里会进行一个完整的说明。 +此问题一般常见于 PHP 项目中存取文件的基类路径选择问题,尤其是在配合 `micro.sfx` 使用时容易出现路径问题。 + +注意,此处仅对你在开发 Phar 项目或 PHP 框架时可能有用。 + +> 接下来我们都将 `static-php-cli`(也就是 spc)当作一个普通的 `php` 命令行程序来看,你可以将 spc 理解为你自己的任何 php-cli 应用以参考。 + +下面主要有三个基本的常量理论值,我们建议你在编写 php 项目时引入这三种常量: + +- `WORKING_DIR`:执行 PHP 脚本时的工作目录 +- `SOURCE_ROOT_DIR` 或 `ROOT_DIR`:项目文件夹的根目录,一般为 `composer.json` 所在目录 +- `FRAMEWORK_ROOT_DIR`:使用框架的根目录,自行开发的框架可能会用到,一般框架目录为只读 + +你可以在你的框架或者 cli 应用程序入口中定义这些常量,以方便在你的项目中使用路径。 + +下面是 PHP 内置的常量值,在 PHP 解释器内部已被定义: + +- `__DIR__`:当前执行脚本的文件所在目录 +- `__FILE__`:当前执行脚本的文件路径 + +### Git 项目模式(source) + +Git 项目模式指的是一个框架或程序本身在当前文件夹以纯文本形式存放,运行通过 `php path/to/entry.php` 方式。 + +假设你的项目存放在 `/home/example/static-php-cli/` 目录下,或你的项目就是框架本身,里面包含 `composer.json` 等项目文件: + +``` +composer.json +src/App/MyCommand.app +vendor/* +bin/entry.php +``` + +我们假设从 `src/App/MyCommand.php` 中获取以上常量: + +| Constant | Value | +|----------------------|------------------------------------------------------| +| `WORKING_DIR` | `/home/example/static-php-cli` | +| `SOURCE_ROOT_DIR` | `/home/example/static-php-cli` | +| `FRAMEWORK_ROOT_DIR` | `/home/example/static-php-cli` | +| `__DIR__` | `/home/example/static-php-cli/src/App` | +| `__FILE__` | `/home/example/static-php-cli/src/App/MyCommand.php` | + +这种情况下,`WORKING_DIR`、`SOURCE_ROOT_DIR`、`FRAMEWORK_ROOT_DIR` 的值是完全一致的:`/home/example/static-php-cli`。 +框架的源码和应用的源码都在当前路径下。 + +### Vendor 库模式(vendor) + +Vendor 库模式一般是指你的项目为框架类或者被其他应用作为 composer 依赖项安装到项目中,存放位置在 `vendor/author/XXX` 目录。 + +假设你的项目是 `crazywhalecc/static-php-cli`,你或其他人在另一个项目使用 `composer require` 安装了这个项目。 + +我们假设 static-php-cli 中包含同 `Git 模式` 的除 `vendor` 目录外的所有文件,并从 `src/App/MyCommand` 中获取常量值, +目录常量应该是: + +| Constant | Value | +|----------------------|--------------------------------------------------------------------------------------| +| `WORKING_DIR` | `/home/example/another-app` | +| `SOURCE_ROOT_DIR` | `/home/example/another-app` | +| `FRAMEWORK_ROOT_DIR` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli` | +| `__DIR__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App` | +| `__FILE__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | + + +这里的 `SOURCE_ROOT_DIR` 就指的是使用 `static-php-cli` 的项目的根目录。 + +### Git 项目 Phar 模式(source-phar) + +Git 项目 Phar 模式指的是将 Git 项目模式的项目目录打包为一个 `phar` 文件的模式。我们假设 `/home/example/static-php-cli` 将打包为一个 Phar 文件,目录有以下文件: + +``` +composer.json +src/App/MyCommand.app +vendor/* +bin/entry.php +``` + +打包为 `app.phar` 并存放到 `/home/example/static-php-cli` 目录下时,此时执行 `app.phar`,假设执行了 `src/App/MyCommand` 代码,常量在该文件内获取: + +| Constant | Value | +|----------------------|----------------------------------------------------------------------| +| `WORKING_DIR` | `/home/example/static-php-cli` | +| `SOURCE_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | +| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | +| `__DIR__` | `phar:///home/example/static-php-cli/app.phar/src/App` | +| `__FILE__` | `phar:///home/example/static-php-cli/app.phar/src/App/MyCommand.php` | + +因为在 phar 内读取自身 phar 的文件需要 `phar://` 协议进行,所以项目根目录和框架目录将会和 `WORKING_DIR` 不同。 + +### Vendor 库 Phar 模式(vendor-phar) + +Vendor 库 Phar 模式指的是你的项目作为框架安装在其他项目内,存储于 `vendor` 目录下。 + +我们假设你的项目目录结构如下: + +``` +composer.json # 当前项目的 Composer 配置文件 +box.json # 打包 Phar 的配置文件 +another-app.php # 另一个项目的入口文件 +vendor/crazywhalecc/static-php-cli/* # 你的项目被作为依赖库 +``` + +将该目录 `/home/example/another-app/` 下的这些文件打包为 `app.phar` 时,对于你的项目而言,下面常量的值应为: + +| Constant | Value | +|----------------------|------------------------------------------------------------------------------------------------------| +| `WORKING_DIR` | `/home/example/another-app` | +| `SOURCE_ROOT_DIR` | `phar:///home/example/another-app/app.phar/` | +| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli` | +| `__DIR__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App` | +| `__FILE__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | diff --git a/docs-v2/zh/develop/system-build-tools.md b/docs-v2/zh/develop/system-build-tools.md new file mode 100644 index 000000000..a96d4dace --- /dev/null +++ b/docs-v2/zh/develop/system-build-tools.md @@ -0,0 +1,204 @@ +# 系统编译工具 + +static-php-cli 在构建静态 PHP 时使用了许多系统编译工具,这些工具主要包括: + +- `autoconf`: 用于生成 `configure` 脚本。 +- `make`: 用于执行 `Makefile`。 +- `cmake`: 用于执行 `CMakeLists.txt`。 +- `pkg-config`: 用于查找依赖库的安装路径。 +- `gcc`: 用于在 Linux 下编译 C/C++ 语言代码。 +- `clang`: 用于在 macOS 下编译 C/C++ 语言代码。 + +对于 Linux 和 macOS 操作系统,这些工具通常可以通过包管理安装,这部分在 doctor 模块中编写了。 +理论上我们也可以通过编译和手动下载这些工具,但这样会增加编译的复杂度,所以我们不推荐这样做。 + +## Linux 环境编译工具 + +对于 Linux 系统来说,不同发行版的编译工具安装方式不同。而且对于静态编译来说,某些发行版的包管理无法安装用于纯静态编译的库和工具, +所以对于 Linux 平台及其不同发行版,我们目前提供了多种编译环境的部署措施。 + +### glibc 环境 + +glibc 环境指的是系统底层的 `libc` 库(即所有 C 语言编写的程序动态链接的 C 标准库)使用的是 `glibc`,这是大多数发行版的默认环境。 +例如:Ubuntu、Debian、CentOS、RHEL、openSUSE、Arch Linux 等。 + +而 glibc 环境下,我们使用的包管理、编译器都是默认指向 glibc 的,glibc 不能被良好地静态链接。它不能被静态链接的原因之一是它的网络库 `nss` 无法静态编译。 + +对于 glibc 环境,在 2.0 RC8 及以后的 static-php-cli 及 spc 中,你可以选择两种方式来构建静态 PHP: + +1. 使用 Docker 构建,这是最简单的方式,你可以使用 `bin/spc-alpine-docker` 来构建,它会在 Alpine Linux 环境下构建。 +2. 使用 `bin/spc doctor` 安装 musl-wrapper 和 musl-cross-make 套件,然后直接正常构建。([相关源码](https://github.com/crazywhalecc/static-php-cli/blob/main/src/SPC/doctor/item/LinuxMuslCheck.php)) + +一般来说,这两种构建方式的构建结果是一致的,你可以根据实际需求选择。 + +在 doctor 模块中,static-php-cli 会先检测当前的 Linux 发行版。如果当前发行版是 glibc 环境,会提示需要安装 musl-wrapper 和 musl-cross-make 套件。 + +在 glibc 环境下安装 musl-wrapper 的过程如下: + +1. 从 musl 官网下载特定版本的 [musl-wrapper 源码](https://musl.libc.org/releases/)。 +2. 使用从包管理安装的 `gcc` 编译 musl-wrapper 源码,生成 `musl-libc` 等库:`./configure --disable-gcc-wrapper && make -j && sudo make install`。 +3. musl-wrapper 相关库将被安装在 `/usr/local/musl` 目录。 + +在 glibc 环境下安装 musl-cross-make 的过程如下: + +1. 从 dl.static-php.dev 下载预编译好的 [musl-cross-make](https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/) 压缩包。 +2. 解压到 `/usr/local/musl` 目录。 + +::: tip +在 glibc 环境下,静态编译可以通过直接安装 musl-wrapper 来实现,但是 musl-wrapper 仅包含了 `musl-gcc`,而没有 `musl-g++`,这也就意味着无法编译 C++ 代码。 +所以我们需要 musl-cross-make 来提供 `musl-g++`。 + +而 musl-cross-make 套件无法在本地直接编译的原因是它的编译环境要求比较高(需要 36GB 以上内存,Alpine Linux 下编译),所以我们提供了预编译好的二进制包,可用于所有 Linux 发行版。 + +同时,部分发行版的包管理提供了 musl-wrapper,但 musl-cross-make 需要匹配对应的 musl-wrapper 版本,所以我们不使用包管理安装 musl-wrapper。 + +对于如何编译 musl-cross-make,将在本章节内的 **编译 musl-cross-make** 小节中介绍。 +::: + +### musl 环境 + +musl 环境指的是系统底层的 `libc` 库使用的是 `musl`,这是一种轻量级的 C 标准库,它的特点是可以被良好地静态链接。 + +对于目前流行的 Linux 发行版,Alpine Linux 使用的就是 musl 环境,所以 static-php-cli 在 Alpine Linux 下可以直接构建静态 PHP,仅需直接从包管理安装基础编译工具(如 gcc、cmake 等)即可。 + +对于其他发行版,如果你的发行版使用的是 musl 环境,那么你也可以在安装必要的编译工具后直接使用 static-php-cli 构建静态 PHP。 + +::: tip +在 musl 环境下,static-php-cli 会自动跳过 musl-wrapper 和 musl-cross-make 的安装。 +::: + +### Docker 环境 + +Docker 环境指的是使用 Docker 容器来构建静态 PHP,你可以使用 `bin/spc-alpine-docker` 来构建。 +执行这个命令前需要先安装 Docker,然后在项目根目录执行 `bin/spc-alpine-docker` 即可。 + +在执行 `bin/spc-alpine-docker` 后,static-php-cli 会自动下载 Alpine Linux 镜像,然后构建一个 `cwcc-spc-x86_64` 或 `cwcc-spc-aarch64` 的镜像。 +然后一切的构建都在这个镜像内进行,相当于在 Alpine Linux 内编译。总的来说,Docker 环境就是 musl 环境。 + +## musl-cross-make 工具链编译 + +在 Linux 中,尽管你不需要手动编译 musl-cross-make 工具,但是如果你想了解它的编译过程,可以参考这里。 +还有一个重要的原因就是,这个可能无法使用 CI、Actions 等自动化工具编译,因为现有的 CI 服务编译环境不满足 musl-cross-make 的编译要求,满足要求的配置价格太高。 + +musl-cross-make 的编译过程如下: + +准备一个 Alpine Linux 环境(直接安装或使用 Docker 均可),编译的过程需要 36GB 以上内存,所以你需要在内存较大的机器上编译。如果没有这么多内存,可能会导致编译失败。 + +然后将以下内容写入 `config.mak` 文件内: + +```makefile +STAT = -static --static +FLAG = -g0 -Os -Wno-error + +ifneq ($(NATIVE),) +COMMON_CONFIG += CC="$(HOST)-gcc ${STAT}" CXX="$(HOST)-g++ ${STAT}" +else +COMMON_CONFIG += CC="gcc ${STAT}" CXX="g++ ${STAT}" +endif + +COMMON_CONFIG += CFLAGS="${FLAG}" CXXFLAGS="${FLAG}" LDFLAGS="${STAT}" + +BINUTILS_CONFIG += --enable-gold=yes --enable-gprofng=no +GCC_CONFIG += --enable-static-pie --disable-cet --enable-default-pie +#--enable-default-pie + +CONFIG_SUB_REV = 888c8e3d5f7b +GCC_VER = 13.2.0 +BINUTILS_VER = 2.40 +MUSL_VER = 1.2.4 +GMP_VER = 6.2.1 +MPC_VER = 1.2.1 +MPFR_VER = 4.2.0 +LINUX_VER = 6.1.36 +``` + +同时,你需要新建一个 `gcc-13.2.0.tar.xz.sha1` 文件,文件内容如下: + +``` +5f95b6d042fb37d45c6cbebfc91decfbc4fb493c gcc-13.2.0.tar.xz +``` + +如果你使用的是 Docker 构建,新建一个 `Dockerfile` 文件,写入以下内容: + +```dockerfile +FROM alpine:edge + +RUN apk add --no-cache \ +gcc g++ git make curl perl \ +rsync patch wget libtool \ +texinfo autoconf automake \ +bison tar xz bzip2 zlib \ +file binutils flex \ +linux-headers libintl \ +gettext gettext-dev icu-libs pkgconf \ +pkgconfig icu-dev bash \ +ccache libarchive-tools zip + +WORKDIR /opt + +RUN git clone https://git.zv.io/toolchains/musl-cross-make.git +WORKDIR /opt/musl-cross-make +COPY config.mak /opt/musl-cross-make +COPY gcc-13.2.0.tar.xz.sha1 /opt/musl-cross-make/hashes + +RUN make TARGET=x86_64-linux-musl -j || : +RUN sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h +RUN make TARGET=x86_64-linux-musl -j +RUN make TARGET=x86_64-linux-musl install -j +RUN tar cvzf x86_64-musl-toolchain.tgz output/* +``` + +如果你使用的是非 Docker 环境的 Alpine Linux,可以直接执行 Dockerfile 中的命令,例如: + +```bash +apk add --no-cache \ +gcc g++ git make curl perl \ +rsync patch wget libtool \ +texinfo autoconf automake \ +bison tar xz bzip2 zlib \ +file binutils flex \ +linux-headers libintl \ +gettext gettext-dev icu-libs pkgconf \ +pkgconfig icu-dev bash \ +ccache libarchive-tools zip + +git clone https://git.zv.io/toolchains/musl-cross-make.git +# 将 config.mak 拷贝到 musl-cross-make 的工作目录内,你需要将 /path/to/config.mak 替换为你的 config.mak 文件路径 +cp /path/to/config.mak musl-cross-make/ +cp /path/to/gcc-13.2.0.tar.xz.sha1 musl-cross-make/hashes + +make TARGET=x86_64-linux-musl -j || : +sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h +make TARGET=x86_64-linux-musl -j +make TARGET=x86_64-linux-musl install -j +tar cvzf x86_64-musl-toolchain.tgz output/* +``` + +::: tip +以上所有脚本都适用于 x86_64 架构的 Linux。如果你需要构建 ARM 环境的 musl-cross-make,只需要将上方所有 `x86_64` 替换为 `aarch64` 即可。 +::: + +这个编译过程可能会因为内存不足、网络问题等原因导致编译失败,你可以多尝试几次,或者使用更大内存的机器来编译。 +如果遇到了问题,或者你有更好的改进方案,可以在 [讨论](https://github.com/crazywhalecc/static-php-cli-hosted/issues/1) 中提出。 + +## macOS 环境编译工具 + +对于 macOS 系统来说,我们使用的编译工具主要是 `clang`,它是 macOS 系统默认的编译器,同时也是 Xcode 的编译器。 + +在 macOS 下编译,主要依赖于 Xcode 或 Xcode Command Line Tools,你可以在 App Store 下载 Xcode,或者在终端执行 `xcode-select --install` 来安装 Xcode Command Line Tools。 + +此外,在 `doctor` 环境检查模块中,static-php-cli 会检查 macOS 系统是否安装了 Homebrew、编译工具等,如果没有,会提示你安装,这里不再赘述。 + +## FreeBSD 环境编译工具 + +FreeBSD 也是 Unix 系统,它的编译工具和 macOS 类似,你可以直接使用包管理 `pkg` 安装 `clang` 等编译工具,通过 `doctor` 命令。 + +## pkg-config 编译 + +如果你在使用 static-php-cli 构建静态 PHP 时仔细观察编译的日志,你会发现无论编译什么,都会先编译 `pkg-config`,这是因为 `pkg-config` 是一个用于查找依赖库的工具。 +在早期的 static-php-cli 版本中,我们直接使用了包管理安装的 `pkg-config` 工具,但是这样会导致一些问题,例如: + +- 即使指定了 `PKG_CONFIG_PATH`,`pkg-config` 也会尝试从系统路径中查找依赖包。 +- 由于 `pkg-config` 会从系统路径中查找依赖包,所以如果系统中存在同名的依赖包,可能会导致编译失败。 + +为了避免以上问题,我们将 `pkg-config` 编译到用户态的 `buildroot/bin` 内并使用,使用了 `--without-sysroot` 等参数来避免从系统路径中查找依赖包。 diff --git a/docs-v2/zh/faq/index.md b/docs-v2/zh/faq/index.md new file mode 100644 index 000000000..142a1503f --- /dev/null +++ b/docs-v2/zh/faq/index.md @@ -0,0 +1,96 @@ +# 常见问题 + +这里将会编写一些你容易遇到的问题。目前有很多,但是我需要花时间来整理一下。 + +## php.ini 的路径是什么? + +在 Linux、macOS 和 FreeBSD 上,`php.ini` 的路径是 `/usr/local/etc/php/php.ini`。 +在 Windows 中,路径是 `C:\windows\php.ini` 或 `php.exe` 所在的当前目录。 +可以在 *nix 系统中使用手动构建选项 `--with-config-file-path` 来更改查找 `php.ini` 的目录。 + +此外,在 Linux、macOS 和 FreeBSD 上,`/usr/local/etc/php/conf.d` 目录中的 `.ini` 文件也会被加载。 +在 Windows 中,该路径默认为空。 +可以使用手动构建选项 `--with-config-file-scan-dir` 更改该目录。 + +PHP 默认也会从 [其他标准位置](https://www.php.net/manual/zh/configuration.file.php) 中搜索 `php.ini`。 + +## 静态编译的 PHP 可以安装扩展吗? + +因为传统架构下的 PHP 安装扩展的原理是使用 `.so` 类型的动态链接的库方式安装新扩展,而使用本项目编译的静态链接的 PHP。但是静态链接在不同操作系统有不同的定义。 + +首先,对于 Linux 系统,静态链接的二进制文件不会链接系统的动态链接库。纯静态链接的二进制文件(`-all-static`)无法加载动态库,因此无法添加新扩展。 +同时,在纯静态模式下,你也不能使用 `ffi` 等扩展来加载外部 `.so` 模块。 + +你可以使用命令 `ldd buildroot/bin/php` 来检查你在 Linux 下构建的二进制文件是否为纯静态链接。 + +如果你 [构建基于 GNU libc 的 PHP](../guide/build-with-glibc),你可以使用 `ffi` 扩展来加载外部 `.so` 模块,并加载具有相同 ABI 的 `.so` 扩展。 + +例如,你可以使用以下命令构建一个与 glibc 动态链接的静态 PHP 二进制文件,支持 FFI 扩展并加载相同 PHP 版本和相同 TS 类型的 `xdebug.so` 扩展: + +```bash +bin/spc-gnu-docker download --for-extensions=ffi,xml --with-php=8.4 +bin/spc-gnu-docker build ffi,xml --build-cli --debug + +buildroot/bin/php -d "zend_extension=/path/to/php{PHP_VER}-{ts/nts}/xdebug.so" --ri xdebug +``` + +对于 macOS 平台,macOS 下的几乎所有二进制文件都无法真正纯静态链接,几乎所有二进制文件都会链接 macOS 系统库:`/usr/lib/libresolv.9.dylib` 和 `/usr/lib/libSystem.B.dylib`。 +因此,在 macOS 上,你可以**直接**使用 SPC 构建具有动态链接扩展的静态编译 PHP 二进制文件: + +1. 使用 `--build-shared=XXX` 选项构建共享扩展 `xxx.so`。例如:`bin/spc build bcmath,zlib --build-shared=xdebug --build-cli` +2. 你将获得 `buildroot/modules/xdebug.so` 和 `buildroot/bin/php`。 +3. `xdebug.so` 文件可用于版本和线程安全相同的 php。 + +对于 Windows 平台,由于官方构建的扩展(如 `php_yaml.dll`)强制使用了 `php8.dll` 动态库作为链接,静态构建的 PHP 不包含任何系统库以外的动态库, +所以 Windows 下无法加载官方构建的动态扩展。 由于 static-php-cli 还暂未支持构建动态扩展,所以目前还没有让 static-php 加载动态扩展的方法。 + +不过,Windows 可以正常使用 `FFI` 扩展加载其他的 dll 文件并调用。 + +## 可以支持 Oracle 数据库扩展吗? + +部分依赖库闭源的扩展,如 `oci8`、`sourceguardian` 等,它们没有提供纯静态编译的依赖库文件(`.a`),仅提供了动态依赖库文件(`.so`), +这些扩展无法使用源码的形式编译到 static-php-cli 中,所以本项目可能永远也不会支持这些扩展。不过,理论上你可以根据上面的问题在 macOS 和 Linux 下接入和使用这类扩展。 + +如果你对此类扩展有需求,或者大部分人都对这些闭源扩展使用有需求, +可以看看有关 [standalone-php-cli](https://github.com/crazywhalecc/static-php-cli/discussions/58) 的讨论。欢迎留言。 + +## 支持 Windows 吗? + +该项目目前支持 Windows,但支持的扩展数量较少。Windows 支持并不完美。主要有以下问题: + +1. Windows 的编译过程与 *nix 不同,使用的工具链也不同。用于编译每个扩展依赖库的编译工具也几乎完全不同。 +2. Windows 版本的需求也会根据所有使用本项目的人的需求推进。如果很多人需要,我会尽快支持相关扩展。 + +## 我可以使用 micro 保护我的源代码吗? + +不可以。micro.sfx 本质上是将 php 和 php 代码合并为一个文件,没有编译或加密 PHP 代码的过程。 + +首先,php-src 是 PHP 代码的官方解释器,市场上没有与主流分支兼容的 PHP 编译器。 +我在网上看到一个名为 BPC(Binary PHP Compiler?)的项目可以将 PHP 编译为二进制,但有很多限制。 + +加密和保护代码的方向与编译不同。编译后,也可以通过逆向工程等方法获得代码。真正的保护仍然通过打包和加密代码等手段进行。 + +因此,本项目(static-php-cli)和相关项目(lwmbs、swoole-cli)都提供了 php-src 源代码的便捷编译工具。 +本项目和相关项目引用的 phpmicro 只是 PHP 的 sapi 接口封装,而不是 PHP 代码的编译工具。 +PHP 代码的编译器是一个完全不同的项目,因此不考虑额外的情况。 +如果你对加密感兴趣,可以考虑使用现有的加密技术,如 Swoole Compiler、Source Guardian 等。 + +## 无法使用 ssl + +**更新:该问题已在最新版本的 static-php-cli 中修复,现在默认读取系统的证书文件。如果你仍然遇到问题,请尝试下面的解决方案。** + +使用 curl、pgsql 等请求 HTTPS 网站或建立 SSL 连接时,可能会出现 `error:80000002:system library::No such file or directory` 错误。 +此错误是由于静态编译的 PHP 未通过 `php.ini` 指定 `openssl.cafile` 导致的。 + +你可以通过在使用 PHP 前指定 `php.ini` 并在 INI 中添加 `openssl.cafile=/path/to/your-cert.pem` 来解决此问题。 + +对于 Linux 系统,你可以从 curl 官方网站下载 [cacert.pem](https://curl.se/docs/caextract.html) 文件,也可以使用系统自带的证书文件。 +有关不同发行版的证书位置,请参考 [Golang 文档](https://go.dev/src/crypto/x509/root_linux.go)。 + +> INI 配置 `openssl.cafile` 不能使用 `ini_set()` 函数动态设置,因为 `openssl.cafile` 是 `PHP_INI_SYSTEM` 类型的配置,只能在 `php.ini` 文件中设置。 + +## 为什么不支持旧版本的 PHP? + +因为旧版本的 PHP 有很多问题,如安全问题、性能问题和功能问题。此外,许多旧版本的 PHP 与最新的依赖库不兼容,这也是不支持旧版本 PHP 的原因之一。 + +你可以使用 static-php-cli 早期编译的旧版本,如 PHP 8.0,但不会明确支持早期版本。 diff --git a/docs/zh/guide/action-build.md b/docs-v2/zh/guide/action-build.md similarity index 100% rename from docs/zh/guide/action-build.md rename to docs-v2/zh/guide/action-build.md diff --git a/docs/zh/guide/build-on-windows.md b/docs-v2/zh/guide/build-on-windows.md similarity index 100% rename from docs/zh/guide/build-on-windows.md rename to docs-v2/zh/guide/build-on-windows.md diff --git a/docs/zh/guide/build-with-glibc.md b/docs-v2/zh/guide/build-with-glibc.md similarity index 100% rename from docs/zh/guide/build-with-glibc.md rename to docs-v2/zh/guide/build-with-glibc.md diff --git a/docs-v2/zh/guide/cli-generator.md b/docs-v2/zh/guide/cli-generator.md new file mode 100644 index 000000000..c3936dea2 --- /dev/null +++ b/docs-v2/zh/guide/cli-generator.md @@ -0,0 +1,15 @@ +--- +aside: false +--- + + + +# CLI 编译命令生成器 + +::: tip +下面选择扩展可能包含所选操作系统不支持的扩展,这可能导致编译失败。请先查阅 [支持的扩展](./extensions)。 +::: + + diff --git a/docs-v2/zh/guide/deps-map.md b/docs-v2/zh/guide/deps-map.md new file mode 100644 index 000000000..91ff57fd8 --- /dev/null +++ b/docs-v2/zh/guide/deps-map.md @@ -0,0 +1,22 @@ +--- +outline: 'deep' +--- + +# 依赖关系图表 + +在编译 PHP 时,每个扩展、库都有依赖关系,这些依赖关系可能是必需的,也可能是可选的。在编译 PHP 时,可以选择是否包含这些可选的依赖关系。 + +例如,在 Linux 下编译 `gd` 扩展时,会强制编译 `zlib,libpng` 库和 `zlib` 扩展,而 `libavif,libwebp,libjpeg,freetype` 库都是可选的库,默认不会编译,除非通过 `--with-libs=avif,webp,jpeg,freetype` 选项指定。 + +- 对于可选扩展(扩展的可选特性),需手动在编译时指定,例如启用 Redis 的 igbinary 支持:`bin/spc build redis,igbinary`。 +- 对于可选库,需通过 `--with-libs=XXX` 选项编译指定。 +- 如果想启用所有的可选扩展,可以使用 `bin/spc build redis --with-suggested-exts` 参数。 +- 如果想启用所有的可选库,可以使用 `--with-suggested-libs` 参数。 + +## 扩展的依赖图 + + + +## 库的依赖表 + + \ No newline at end of file diff --git a/docs-v2/zh/guide/env-vars.md b/docs-v2/zh/guide/env-vars.md new file mode 100644 index 000000000..9a2f9deb9 --- /dev/null +++ b/docs-v2/zh/guide/env-vars.md @@ -0,0 +1,112 @@ +# 环境变量 + +本页面的环境变量列表中所提到的所有环境变量都具有默认值,除非另有说明。你可以通过设置这些环境变量来覆盖默认值。 + +## 环境变量列表 + +在 2.3.5 版本之后,我们将环境变量集中到了 `config/env.ini` 文件中,你可以通过修改这个文件来设置环境变量。 + +我们将 static-php-cli 支持的环境变量分为三种: + +- 全局内部环境变量:在 static-php-cli 启动后即声明,你可以在 static-php-cli 的内部使用 `getenv()` 来获取他们,也可以在启动 static-php-cli 前覆盖。 +- 固定环境变量:在 static-php-cli 启动后声明,你仅可使用 `getenv()` 获取,但无法通过 shell 脚本对其覆盖。 +- 配置文件环境变量:在 static-php-cli 构建前声明,你可以通过修改 `config/env.ini` 文件或通过 shell 脚本来设置这些环境变量。 + +你可以阅读 [config/env.ini](https://github.com/crazywhalecc/static-php-cli/blob/main/config/env.ini) 中每项参数的注释来了解其作用(仅限英文版)。 + +## 自定义环境变量 + +一般情况下,你不需要修改任何以下环境变量,因为它们已经被设置为最佳值。 +但是,如果你有特殊需求,你可以通过设置这些环境变量来满足你的需求(比如你需要调试不同编译参数下的 PHP 性能表现)。 + +如需使用自定义环境变量,你可以在终端中使用 `export` 命令或者在命令前直接设置环境变量,例如: + +```shell +# export 方式 +export SPC_CONCURRENCY=4 +bin/spc build mbstring,pcntl --build-cli + +# 直接设置方式 +SPC_CONCURRENCY=4 bin/spc build mbstring,pcntl --build-cli +``` + +或者,如果你需要长期修改某个环境变量,你可以通过修改 `config/env.ini` 文件来实现。 + +`config/env.ini` 分为三段,其中 `[global]` 全局有效,`[windows]`、`[macos]`、`[linux]` 仅对应的操作系统有效。 + +例如,你需要修改编译 PHP 的 `./configure` 命令,你可以在 `config/env.ini` 文件中找到 `SPC_CMD_PREFIX_PHP_CONFIGURE` 环境变量,然后修改其值即可。 + +但如果你的构建条件比较复杂,需要多种 env.ini 进行切换,我们推荐你使用 `config/env.custom.ini` 文件,这样你可以在不修改默认的 `config/env.ini` 文件的情况下, +通过写入额外的重载项目指定你的环境变量。 + +```ini +; This is an example of `config/env.custom.ini` file, +; we modify the `SPC_CONCURRENCY` and linux default CFLAGS passing to libs and PHP +[global] +SPC_CONCURRENCY=4 + +[linux] +SPC_DEFAULT_C_FLAGS="-O3" +``` + +## 编译依赖库的环境变量(仅限 Unix 系统) + +从 2.2.0 开始,static-php-cli 对所有 macOS、Linux、FreeBSD 等 Unix 系统的编译依赖库的命令均支持自定义环境变量。 + +这样你就可以随时通过环境变量来调整编译依赖库的行为。例如你可以通过 `xxx_CFLAGS=-O0` 来设置编译 xxx 库的优化参数。 + +当然,不是每个依赖库都支持注入环境变量,我们目前提供了三个通配的环境变量,后缀分别为: + +- `_CFLAGS`: C 编译器的参数 +- `_LDFLAGS`: 链接器的参数 +- `_LIBS`: 额外的链接库 + +前缀为依赖库的名称,具体依赖库的名称以 `lib.json` 为准。其中,带有 `-` 的依赖库名称需要将 `-` 替换为 `_`。 + +下面是一个替换 openssl 库编译的优化选项示例: + +```shell +openssl_CFLAGS="-O0" +``` + +库名称使用同 `lib.json` 中列举的名称,区分大小写。 + +::: tip +当未指定相关环境变量时,除以下变量外,其余值均默认为空: + +| var name | var default value | +|-----------------------|-------------------------------------------------------------------------------------------------| +| `pkg_config_CFLAGS` | macOS: `$SPC_DEFAULT_C_FLAGS -Wimplicit-function-declaration -Wno-int-conversion`, Other: empty | +| `pkg_config_LDFLAGS` | Linux: `--static`, Other: empty | +| `imagemagick_LDFLAGS` | Linux: `-static`, Other: empty | +| `imagemagick_LIBS` | macOS: `-liconv`, Other: empty | +| `ldap_LDFLAGS` | `-L$BUILD_LIB_PATH` | +| `openssl_CFLAGS` | Linux: `$SPC_DEFAULT_C_FLAGS`, Other: empty | +| others... | empty | + +::: + +下表是支持自定义以上三种变量的依赖库名称列表: + +| lib name | +|-------------| +| brotli | +| bzip | +| curl | +| freetype | +| gettext | +| gmp | +| imagemagick | +| ldap | +| libargon2 | +| libavif | +| libcares | +| libevent | +| openssl | + +::: tip +因为给每个库适配自定义环境变量是一项特别繁琐的工作,且大部分情况下你都不需要这些库的自定义环境变量,所以我们目前只支持了部分库的自定义环境变量。 + +如果你需要自定义环境变量的库不在上方列表,可以通过 [GitHub Issue](https://github.com/crazywhalecc/static-php-cli/issues) +来提出需求。 +::: diff --git a/docs-v2/zh/guide/extension-notes.md b/docs-v2/zh/guide/extension-notes.md new file mode 100644 index 000000000..70d60d1c9 --- /dev/null +++ b/docs-v2/zh/guide/extension-notes.md @@ -0,0 +1,158 @@ +# 扩展注意事项 + +因为是静态编译,扩展不会 100% 完美编译,而且不同扩展对 PHP、环境都有不同的要求,这里将一一列举。 + +## curl + +HTTP3 支持默认未启用,需在编译时添加 `--with-libs="nghttp2,nghttp3,ngtcp2"` 以启用 PHP 8.4 及以上版本的 HTTP3 支持。 + +使用 curl 请求 HTTPS 时,可能存在 `error:80000002:system library::No such file or directory` 错误, +解决办法详见 [FAQ - 无法使用 ssl](../faq/#无法使用-ssl)。 + +## phpmicro + +1. phpmicro SAPI 仅支持 PHP >= 8.0 版本。 + +## swoole + +1. swoole >= 5.0 版本仅支持 PHP >= 8.0 版本。 +2. swoole 目前不支持 PHP 8.0 版本 curl 的 hook(后续有可能会修复)。 +3. 编译时只包含 `swoole` 扩展时不会完整开启支持的 Swoole 数据库协程 hook,如需使用请加入对应的 `swoole-hook-xxx` 扩展。 +4. swoole 在部分扩展组合下可能出现 `zend_mm_heap corrupted` 问题,暂未找到是什么原因导致的。 + +## swoole-hook-pgsql + +swoole-hook-pgsql 不是一个扩展,而是 Swoole 的 Hook 特性。 +如果你在编译时添加了 `swoole,swoole-hook-pgsql`,你将启用 Swoole 的 PostgreSQL 客户端和 `pdo_pgsql` 扩展的协程模式。 + +swoole-hook-pgsql 与 `pdo_pgsql` 扩展冲突。如需使用 Swoole 和 `pdo_pgsql`,请删除 pdo_pgsql 扩展,启用 `swoole` 和 `swoole-hook-pgsql` 即可。 +该扩展包含了 `pdo_pgsql` 的协程环境的实现。 + +在 macOS 系统,`pdo_pgsql` 可能无法正常连接到 postgresql 服务器,请谨慎使用。 + +## swoole-hook-mysql + +swoole-hook-mysql 不是一个扩展,而是 Swoole 的 Hook 特性。 +如果你在编译时添加了 `swoole,swoole-hook-mysql`,你将启用 Swoole 的 `mysqlnd` 和 `pdo_mysql` 的协程模式。 + +## swoole-hook-sqlite + +swoole-hook-sqlite 不是一个扩展,而是 Swoole 的 Hook 特性。 +如果你在编译时添加了 `swoole,swoole-hook-sqlite`,你将启用 Swoole 的 `pdo_sqlite` 的协程模式(Swoole 必须为 5.1 以上)。 + +swoole-hook-sqlite 与 `pdo_sqlite` 扩展冲突。如需使用 Swoole 和 `pdo_sqlite`,请删除 pdo_sqlite 扩展,启用 `swoole` 和 `swoole-hook-sqlite` 即可。 +该扩展包含了 `pdo_sqlite` 的协程环境的实现。 + +## swoole-hook-odbc + +swoole-hook-odbc 不是一个扩展,而是 Swoole 的 Hook 特性。 +如果你在编译时添加了 `swoole,swoole-hook-odbc`,你将启用 Swoole 的 `odbc` 扩展的协程模式。 + +swoole-hook-odbc 与 `pdo_odbc` 扩展冲突。如需使用 Swoole 和 `pdo_odbc`,请删除 `pdo_odbc` 扩展,启用 `swoole` 和 `swoole-hook-odbc` 即可。 +该扩展包含了 `pdo_odbc` 的协程环境的实现。 + +## swow + +1. swow 仅支持 PHP 8.0+ 版本。 + +## imagick + +1. OpenMP 支持已被禁用,这是维护者推荐的做法,系统软件包也是如此配置。 + +## imap + +1. 该扩展目前不支持 Kerberos。 +2. 由于底层的 c-client、ext-imap 不是线程安全的。 无法在 `--enable-zts` 构建中使用它。 +3. 该扩展已在 PHP 8.4 中被移除,因此我们建议您寻找替代实现,例如 [Webklex/php-imap](https://github.com/Webklex/php-imap)。 + +## gd + +1. gd 扩展依赖了较多的额外图形库,默认情况下,直接使用 `bin/spc build gd` 不会引入和支持部分图形库,例如 `libjpeg`、`libavif` 等, +需要使用 `--with-libs` 参数补全。目前支持 `freetype,libjpeg,libavif,libwebp` 四个库的支持,所以这里可以使用以下命令来让 gd 库引入它们: + +```bash +bin/spc build gd --with-libs=freetype,libjpeg,libavif,libwebp --build-cli +``` + +## mcrypt + +1. 目前未支持,未来也不计划支持此扩展。[#32](https://github.com/crazywhalecc/static-php-cli/issues/32) + +## oci8 + +1. oci8 是 Oracle 数据库的扩展,因为 Oracle 提供的扩展所依赖的库未提供静态编译版本(`.a`)或源代码,无法使用静态链接的方式将此扩展编译到 php 内,故无法支持。 + +## xdebug + +1. Xdebug 只能作为共享扩展进行构建。您需要使用除了 `musl-static` 外的其他 `SPC_TARGET` 构建目标。 +2. 使用 Linux/glibc 或 macOS 时,您可以使用 `--build-shared=xdebug` 将 Xdebug 编译为共享扩展。 + 编译后的 `./php` 二进制文件可以通过指定 INI 文件进行配置和运行,例如 `./php -d 'zend_extension=/path/to/xdebug.so' your-code.php`。 + +## xml + +1. xml包括 xmlreader、xmlwriter、dom、simplexml 等,添加 xml 扩展时最好同时启用这些扩展。 +2. libxml 包含在 xml 扩展中。 启用 xml 相当于启用 libxml。 + +## glfw + +1. glfw 扩展依赖 OpenGL,在 Linux 平台还依赖 X11 等环境,这些库都无法被轻易地动态链接。 +2. 在 macOS 系统下,我们可以动态链接系统的 OpenGL 和一些相关的库。 + +## rar + +1. rar 扩展目前在 macOS x86_64 环境下与 `common` 扩展集合编译 phpmicro 存在问题。 + +## pgsql + +~~pgsql ssl 连接与 openssl 3.2.0 不兼容。相关链接:~~ + +- ~~~~ +- ~~~~ +- ~~~~ + +pgsql 16.2 修复了这个 Bug,现在正常工作了。 + +在 pgsql 使用 SSL 连接时,可能存在 `error:80000002:system library::No such file or directory` 错误, +解决办法详见 [FAQ - 无法使用 ssl](../faq/#无法使用-ssl)。 + +## openssl + +使用基于 openssl 的扩展(如 curl、pgsql 等网络库)时,可能存在 `error:80000002:system library::No such file or directory` 错误, +解决办法详见 [FAQ - 无法使用 ssl](../faq/#无法使用-ssl)。 + +## password-argon2 + +1. password-argon2不是一个标准的扩展。`password_hash` 函数的 `PASSWORD_ARGON2ID` 算法需要 libsodium 或 libargon2 才能工作。 +2. 使用 password-argon2 可以为此启用多线程支持。 + +## ffi + +1. 由于 musl libc 静态链接的限制,无法加载动态库,因此无法使用 ffi。 + 如果您需要使用 ffi 扩展,请参阅 [使用 GNU libc 编译 PHP](./build-with-glibc)。 +2. macOS 支持 ffi 扩展,但某些内核不包含调试符号时会出现错误。 +3. Windows x64 支持 ffi 扩展。 + +## xhprof + +xhprof 扩展包含三部分:`xhprof_extension`、`xhprof_html`、`xhprof_libs`。编译的二进制中只包含 `xhprof_extension`。 +如果需要使用 xhprof,请到 [pecl.php.net/package/xhprof](http://pecl.php.net/package/xhprof) 下载源码,指定 `xhprof_libs` 和 `xhprof_html` 路径来使用。 + +## event + +event 扩展在 macOS 系统下编译后暂无法使用 `openpty` 特性。相关 Issue: + +- [static-php-cli#335](https://github.com/crazywhalecc/static-php-cli/issues/335) + +## parallel + +parallel 扩展只支持 PHP 8.0 及以上版本,并只支持 ZTS 构建(`--enable-zts`)。 + +## spx + +1. SPX 目前不支持 Windows,且官方仓库也不支持静态编译,static-php-cli 使用了 [修改版本](https://github.com/static-php/php-spx)。 + +## mimalloc + +1. 从技术上讲,这不是扩展,而是一个库。 +2. 在 Linux 或 macOS 上使用 `--with-libs="mimalloc"` 进行构建将覆盖默认分配器。 +3. 目前,这还处于实验阶段,但建议在线程环境中使用。 diff --git a/docs-v2/zh/guide/extensions.md b/docs-v2/zh/guide/extensions.md new file mode 100644 index 000000000..43095a077 --- /dev/null +++ b/docs-v2/zh/guide/extensions.md @@ -0,0 +1,22 @@ + + +# 扩展列表 + +> - `yes`: 已支持 +> - 空白: 目前还不支持,或正在支持中 +> - `no` with issue link: 确定不支持或无法支持 +> - `partial` with issue link: 已支持,但是无法完美工作 + + + + +::: tip +如果缺少您需要的扩展,您可以创建 [功能请求](https://github.com/crazywhalecc/static-php-cli/issues)。 + +有些扩展或扩展依赖的库会有一些可选的特性,例如 gd 库可选支持 libwebp、freetype 等。 +如果你只使用 `bin/spc build gd --build-cli` 是不会包含它们(static-php-cli 默认为最小依赖原则)。 + +有关编译可选库,请参考 [扩展、库的依赖关系图表](./deps-map)。对于可选的库,你也可以从 [编译命令生成器](./cli-generator) 中选择扩展后展开选择可选库。 +::: diff --git a/docs-v2/zh/guide/index.md b/docs-v2/zh/guide/index.md new file mode 100644 index 000000000..8e5727c2a --- /dev/null +++ b/docs-v2/zh/guide/index.md @@ -0,0 +1,48 @@ +# 指南 + +static-php-cli 是一个用于构建静态编译的 PHP 二进制的工具,目前支持 Linux 和 macOS 系统。 + +在指南章节中,你将了解到如何使用 static-php-cli 构建独立的 php 程序。 + +- [本地构建](./manual-build) +- [Action 构建](./action-build) +- [扩展列表](./extensions) + +## 编译环境 + +下面是架构支持情况,:gear: 代表支持 GitHub Action 构建,:computer: 代表支持本地构建,空 代表暂不支持。 + +| | x86_64 | aarch64 | +|---------|-------------------|-------------------| +| macOS | :gear: :computer: | :gear: :computer: | +| Linux | :gear: :computer: | :gear: :computer: | +| Windows | :gear: :computer: | | +| FreeBSD | :computer: | :computer: | + +当前支持编译的 PHP 版本: + +> :warning: 部分支持,对于新的测试版和旧版本可能存在问题。 +> +> :heavy_check_mark: 支持 +> +> :x: 不支持 + +| PHP Version | Status | Comment | +|-------------|--------------------|---------------------------------------------------------| +| 7.2 | :x: | | +| 7.3 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | +| 7.4 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | +| 8.0 | :warning: | PHP 官方已停止 8.0 的维护,我们不再处理 8.0 相关的 backport 支持 | +| 8.1 | :warning: | PHP 官方仅对 8.1 提供安全更新,在 8.5 发布后我们不再处理 8.1 相关的 backport 支持 | +| 8.2 | :heavy_check_mark: | | +| 8.3 | :heavy_check_mark: | | +| 8.4 | :heavy_check_mark: | | +| 8.5 (beta) | :warning: | PHP 8.5 目前处于 beta 阶段 | + +> 这个表格的支持状态是 static-php-cli 对构建对应版本的支持情况,不是 PHP 官方对该版本的支持情况。 + +## PHP 支持版本 + +目前,static-php-cli 对 PHP 8.2 ~ 8.5 版本是支持的,对于 PHP 8.1 及更早版本理论上支持,只需下载时选择早期版本即可。 +但由于部分扩展和特殊组件已对早期版本的 PHP 停止了支持,所以 static-php-cli 不会明确支持早期版本。 +我们推荐你编译尽可能新的 PHP 版本,以获得更好的体验。 diff --git a/docs/zh/guide/manual-build.md b/docs-v2/zh/guide/manual-build.md similarity index 100% rename from docs/zh/guide/manual-build.md rename to docs-v2/zh/guide/manual-build.md diff --git a/docs-v2/zh/guide/troubleshooting.md b/docs-v2/zh/guide/troubleshooting.md new file mode 100644 index 000000000..c65236547 --- /dev/null +++ b/docs-v2/zh/guide/troubleshooting.md @@ -0,0 +1,31 @@ +# 故障排除 + +使用 static-php-cli 过程中可能会碰到各种各样的故障,这里将讲述如何自行查看错误并反馈 Issue。 + +## 下载失败问题 + +下载资源问题是 spc 最常见的问题之一。主要是由于 spc 下载资源使用的地址一般均为对应项目的官方网站或 GitHub 等,而这些网站可能偶尔会宕机、屏蔽 IP 地址。 +在遇到下载失败后,可以多次尝试调用下载命令。 + +当下载资源时,你可能最终会看到类似 `curl: (56) The requested URL returned error: 403` 的错误,这通常是由于 GitHub 限制导致的。 +你可以通过在命令中添加 `--debug` 来验证,会看到类似 `[DEBU] Running command (no output) : curl -sfSL "https://api.github.com/repos/openssl/openssl/releases"` 的输出。 + +要解决这个问题,可以在 GitHub 上 [创建](https://github.com/settings/tokens) 一个个人访问令牌,并将其设置为环境变量 `GITHUB_TOKEN=`。 + +如果确认地址确实无法正常访问,可以提交 Issue 或 PR 更新地址或下载类型。 + +## Doctor 无法修复某些问题 + +在绝大部分情况下,doctor 模块都可以对缺失的系统环境进行自动修复和安装,但也存在特殊的环境无法正常使用自动修复功能。 + +由于系统限制(例如,Windows 下无法自动安装 Visual Studio 等软件),自动修复功能无法用于某些项目。 +在遇到无法自动修复功能时,如果遇到 `Some check items can not be fixed` 字样,则表明无法自动修复。 +请根据终端显示的方法提交 Issue 或自行修复环境。 + +## 编译错误 + +遇到编译错误时,如果没有开启 `--debug` 日志,请先开启调试日志,然后确定报错的命令。 +报错的终端输出对于修复编译错误非常重要。 +在提交 Issue 时,请上传终端日志的最后报错片段(或整个终端日志输出),并且包含使用的 `spc` 命令和参数。 + +如果你是重复构建,请参考 [本地构建 - 多次构建](./manual-build#多次构建) 章节。 diff --git a/docs-v2/zh/index.md b/docs-v2/zh/index.md new file mode 100644 index 000000000..71d12be00 --- /dev/null +++ b/docs-v2/zh/index.md @@ -0,0 +1,145 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "StaticPHP" + tagline: "StaticPHP 是一款强大的工具,旨在构建包含 PHP、扩展等在内的可移植可执行文件。" + image: + src: /images/static-php_nobg.png + alt: StaticPHP Logo + actions: + - theme: brand + text: 开始使用 + link: /zh/guide/ + - theme: alt + text: English Docs + link: /en/ + +features: + - title: 静态 CLI 二进制 + details: 您可以轻松编译一个可独立运行的 PHP 二进制文件用于通用场景,支持 CLI、FPM、CGI、FrankenPHP SAPI。 + - title: Micro 自解压可执行文件 + details: 您可以编译一个自解压可执行文件,并通过 Micro SAPI 将其与 PHP 源代码一起构建。 + - title: 依赖管理 + details: StaticPHP 内置依赖管理,支持安装不同类型的 PHP 扩展、包和库。 +--- + + + +

+ + + + diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8ddc9c1f2..46e556388 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,57 +1,69 @@ import sidebarEn from "./sidebar.en"; import sidebarZh from "./sidebar.zh"; - // https://vitepress.dev/reference/site-config export default { - title: "Static PHP", - description: "Build single static PHP binary, with PHP project together, with popular extensions included.", + title: "StaticPHP", + description: "A powerful tool designed for building portable executables including PHP, extensions, and more.", locales: { en: { label: 'English', lang: 'en', themeConfig: { nav: [ - {text: 'Guide', link: '/en/guide/',}, - {text: 'Advanced', link: '/en/develop/'}, - {text: 'Contributing', link: '/en/contributing/'}, - {text: 'FAQ', link: '/en/faq/'}, + { text: 'Guide', link: '/en/guide/' }, + { text: 'Develop', link: '/en/develop/' }, + { text: 'Contributing', link: '/en/contributing/' }, + { text: 'FAQ', link: '/en/faq/' }, + { + text: 'v3 (alpha)', + items: [ + { text: 'v3 (alpha)', link: '/en/' }, + { text: 'v2', link: '/v2/en/guide/' }, + ], + }, ], sidebar: sidebarEn, footer: { message: 'Released under the MIT License.', - copyright: 'Copyright © 2023-present crazywhalecc' - } + copyright: 'Copyright © 2023-present crazywhalecc', + }, }, }, zh: { label: '简体中文', - lang: 'zh', // optional, will be added as `lang` attribute on `html` tag + lang: 'zh', themeConfig: { nav: [ - {text: '构建指南', link: '/zh/guide/'}, - {text: '进阶', link: '/zh/develop/'}, - {text: '贡献', link: '/zh/contributing/'}, - {text: 'FAQ', link: '/zh/faq/'}, + { text: '构建指南', link: '/zh/guide/' }, + { text: '开发者', link: '/zh/develop/' }, + { text: '贡献', link: '/zh/contributing/' }, + { text: 'FAQ', link: '/zh/faq/' }, + { + text: 'v3 (alpha)', + items: [ + { text: 'v3 (alpha)', link: '/zh/' }, + { text: 'v2', link: '/v2/zh/guide/' }, + ], + }, ], sidebar: sidebarZh, footer: { message: 'Released under the MIT License.', - copyright: 'Copyright © 2023-present crazywhalecc' - } + copyright: 'Copyright © 2023-present crazywhalecc', + }, }, - } + }, }, themeConfig: { - // https://vitepress.dev/reference/default-theme-config logo: '/images/static-php_nobg.png', nav: [], socialLinks: [ - {icon: 'github', link: 'https://github.com/crazywhalecc/static-php-cli'} + { icon: 'github', link: 'https://github.com/crazywhalecc/static-php-cli' }, ], footer: { message: 'Released under the MIT License.', - copyright: 'Copyright © 2023-present crazywhalecc' + copyright: 'Copyright © 2023-present crazywhalecc', }, search: { provider: 'algolia', @@ -59,7 +71,13 @@ export default { appId: 'IHJHUB1SF1', apiKey: '8266d31cc2ffbd0e059f1c6e5bdaf8fc', indexName: 'static-php docs', + askAi: { + assistantId: 'b72369b2-60a5-461d-902c-5c18d8c05902', + agentStudio: true, + sidePanel: true, + }, }, }, - } + }, } + diff --git a/docs/.vitepress/sidebar.en.ts b/docs/.vitepress/sidebar.en.ts index 54bcb0e1a..129275f59 100644 --- a/docs/.vitepress/sidebar.en.ts +++ b/docs/.vitepress/sidebar.en.ts @@ -1,57 +1,82 @@ export default { - '/en/guide/': [ - { - text: 'Basic Build Guides', - items: [ - {text: 'Guide', link: '/en/guide/'}, - {text: 'Build (Local)', link: '/en/guide/manual-build'}, - {text: 'Build (CI)', link: '/en/guide/action-build'}, - {text: 'Supported Extensions', link: '/en/guide/extensions'}, - {text: 'Extension Notes', link: '/en/guide/extension-notes'}, - {text: 'Build Command Generator', link: '/en/guide/cli-generator'}, - {text: 'Environment Variables', link: '/en/guide/env-vars', collapsed: true,}, - {text: 'Dependency Table', link: '/en/guide/deps-map'}, - ] - }, - { - text: 'Extended Build Guides', - items: [ - {text: 'Troubleshooting', link: '/en/guide/troubleshooting'}, - {text: 'Build on Windows', link: '/en/guide/build-on-windows'}, - {text: 'Build with GNU libc', link: '/en/guide/build-with-glibc'}, - ], - } - ], - '/en/develop/': [ - { - text: 'Development', - items: [ - {text: 'Get Started', link: '/en/develop/'}, - {text: 'Project Structure', link: '/en/develop/structure'}, - {text: 'PHP Source Modification', link: '/en/develop/php-src-changes'}, - ], - }, - { - text: 'Module', - items: [ - {text: 'Doctor ', link: '/en/develop/doctor-module'}, - {text: 'Source', link: '/en/develop/source-module'}, - ] - }, - { - text: 'Extra', - items: [ - {text: 'Compilation Tools', link: '/en/develop/system-build-tools'}, - {text: 'craft.yml Configuration', link: '/zh/develop/craft-yml'}, - ] - } - ], - '/en/contributing/': [ - { - text: 'Contributing', - items: [ - {text: 'Contributing', link: '/en/contributing/'}, - ], - } - ], + '/en/guide/': [ + { + text: 'Getting Started', + items: [ + { text: 'Overview', link: '/en/guide/' }, + { text: 'Installation', link: '/en/guide/installation' }, + { text: 'First Build', link: '/en/guide/first-build' }, + { text: 'CLI Reference', link: '/en/guide/cli-reference' }, + ], + }, + { + text: 'Extensions', + items: [ + { text: 'Supported Extensions', link: '/en/guide/extensions' }, + { text: 'Extension Notes', link: '/en/guide/extension-notes' }, + { text: 'Build Command Generator', link: '/en/guide/cli-generator' }, + ], + }, + { + text: 'Reference', + items: [ + { text: 'Environment Variables', link: '/en/guide/env-vars' }, + { text: 'Dependency Table', link: '/en/guide/deps-map' }, + { text: 'Troubleshooting', link: '/en/guide/troubleshooting' }, + ], + }, + ], + '/en/develop/': [ + { + text: 'Developer Guide', + items: [ + { text: 'Get Started', link: '/en/develop/' }, + { text: 'Project Structure', link: '/en/develop/structure' }, + { text: 'PHP Source Modifications', link: '/en/develop/php-src-changes' }, + ], + }, + { + text: 'Concepts', + items: [ + { text: 'Package Model', link: '/en/develop/package-model' }, + { text: 'Registry & Plugin System', link: '/en/develop/registry' }, + { text: 'Build Lifecycle', link: '/en/develop/build-lifecycle' }, + ], + }, + { + text: 'Modules', + items: [ + { text: 'Doctor', link: '/en/develop/doctor-module' }, + { text: 'Source', link: '/en/develop/source-module' }, + { text: 'craft.yml', link: '/en/develop/craft-yml' }, + { text: 'Compilation Tools', link: '/en/develop/system-build-tools' }, + ], + }, + { + text: 'Vendor Mode', + items: [ + { text: 'Introduction', link: '/en/develop/vendor-mode/' }, + { text: 'Writing Package Classes', link: '/en/develop/vendor-mode/package-classes' }, + { text: 'Annotations Reference', link: '/en/develop/vendor-mode/annotations' }, + { text: 'Dependency Injection', link: '/en/develop/vendor-mode/dependency-injection' }, + { text: 'Lifecycle Hooks', link: '/en/develop/vendor-mode/lifecycle-hooks' }, + ], + }, + ], + '/en/contributing/': [ + { + text: 'Contributing', + items: [ + { text: 'Contributing Guide', link: '/en/contributing/' }, + ], + }, + ], + '/en/faq/': [ + { + text: 'FAQ', + items: [ + { text: 'Frequently Asked Questions', link: '/en/faq/' }, + ], + }, + ], }; diff --git a/docs/.vitepress/sidebar.zh.ts b/docs/.vitepress/sidebar.zh.ts index 63592b93b..b52fe8618 100644 --- a/docs/.vitepress/sidebar.zh.ts +++ b/docs/.vitepress/sidebar.zh.ts @@ -1,57 +1,82 @@ export default { - '/zh/guide/': [ - { - text: '构建指南', - items: [ - {text: '指南', link: '/zh/guide/'}, - {text: '本地构建', link: '/zh/guide/manual-build'}, - {text: 'Actions 构建', link: '/zh/guide/action-build'}, - {text: '扩展列表', link: '/zh/guide/extensions'}, - {text: '扩展注意事项', link: '/zh/guide/extension-notes'}, - {text: '编译命令生成器', link: '/zh/guide/cli-generator'}, - {text: '环境变量列表', link: '/zh/guide/env-vars'}, - {text: '依赖关系图表', link: '/zh/guide/deps-map'}, - ] - }, - { - text: '扩展构建指南', - items: [ - {text: '故障排除', link: '/zh/guide/troubleshooting'}, - {text: '在 Windows 上构建', link: '/zh/guide/build-on-windows'}, - {text: '构建 GNU libc 兼容的二进制', link: '/zh/guide/build-with-glibc'}, - ], - } - ], - '/zh/develop/': [ - { - text: '开发指南', - items: [ - {text: '开发简介', link: '/zh/develop/'}, - {text: '项目结构简介', link: '/zh/develop/structure'}, - {text: '对 PHP 源码的修改', link: '/zh/develop/php-src-changes'}, - ], - }, - { - text: '模块', - items: [ - {text: 'Doctor 环境检查工具', link: '/zh/develop/doctor-module'}, - {text: '资源模块', link: '/zh/develop/source-module'}, - ] - }, - { - text: '其他', - items: [ - {text: '系统编译工具', link: '/zh/develop/system-build-tools'}, - {text: 'craft.yml 配置详解', link: '/zh/develop/craft-yml'}, - ] - } - ], - '/zh/contributing/': [ - { - text: '贡献指南', - items: [ - {text: '贡献指南', link: '/zh/contributing/'}, - ], - } - ], + '/zh/guide/': [ + { + text: '快速上手', + items: [ + { text: '概览', link: '/zh/guide/' }, + { text: '安装', link: '/zh/guide/installation' }, + { text: '第一次构建', link: '/zh/guide/first-build' }, + { text: '命令行参考', link: '/zh/guide/cli-reference' }, + ], + }, + { + text: '扩展', + items: [ + { text: '支持的扩展列表', link: '/zh/guide/extensions' }, + { text: '扩展注意事项', link: '/zh/guide/extension-notes' }, + { text: '命令生成器', link: '/zh/guide/cli-generator' }, + ], + }, + { + text: '参考', + items: [ + { text: '环境变量', link: '/zh/guide/env-vars' }, + { text: '依赖关系图', link: '/zh/guide/deps-map' }, + { text: '故障排除', link: '/zh/guide/troubleshooting' }, + ], + }, + ], + '/zh/develop/': [ + { + text: '开发者指南', + items: [ + { text: '开发简介', link: '/zh/develop/' }, + { text: '项目结构', link: '/zh/develop/structure' }, + { text: '对 PHP 源码的修改', link: '/zh/develop/php-src-changes' }, + ], + }, + { + text: '核心概念', + items: [ + { text: 'Package 模型', link: '/zh/develop/package-model' }, + { text: 'Registry 与插件系统', link: '/zh/develop/registry' }, + { text: '构建生命周期', link: '/zh/develop/build-lifecycle' }, + ], + }, + { + text: '模块', + items: [ + { text: 'Doctor 环境检查', link: '/zh/develop/doctor-module' }, + { text: '资源模块', link: '/zh/develop/source-module' }, + { text: 'craft.yml 配置', link: '/zh/develop/craft-yml' }, + { text: '编译工具', link: '/zh/develop/system-build-tools' }, + ], + }, + { + text: 'Vendor 模式', + items: [ + { text: '简介', link: '/zh/develop/vendor-mode/' }, + { text: '编写 Package 类', link: '/zh/develop/vendor-mode/package-classes' }, + { text: '注解参考', link: '/zh/develop/vendor-mode/annotations' }, + { text: '依赖注入', link: '/zh/develop/vendor-mode/dependency-injection' }, + { text: '生命周期 Hook', link: '/zh/develop/vendor-mode/lifecycle-hooks' }, + ], + }, + ], + '/zh/contributing/': [ + { + text: '贡献指南', + items: [ + { text: '贡献指南', link: '/zh/contributing/' }, + ], + }, + ], + '/zh/faq/': [ + { + text: 'FAQ', + items: [ + { text: '常见问题', link: '/zh/faq/' }, + ], + }, + ], }; diff --git a/docs/en/contributing/index.md b/docs/en/contributing/index.md index 6dc5ecf55..5438cc30c 100644 --- a/docs/en/contributing/index.md +++ b/docs/en/contributing/index.md @@ -1,63 +1,5 @@ # Contributing -Thank you for being here, this project welcomes your contributions! - -## Contribution Guide - -If you have code or documentation to contribute, here's what you need to know first. - -1. What type of code are you contributing? (new extensions, bug fixes, security issues, project framework optimizations, documentation) -2. If you contribute new files or new snippets, is your code checked by `php-cs-fixer` and `phpstan`? -3. Have you fully read the [Developer Guide](../develop/) before contributing code? - -If you can answer the above questions and have made changes to the code, -you can initiate a Pull Request in the project GitHub repository in time. -After the code review is completed, the code can be modified according to the suggestion, or directly merged into the main branch. - -## Contribution Type - -The main purpose of this project is to compile statically linked PHP binaries, -and the command line processing function is written based on `symfony/console`. -Before development, if you are not familiar with it, -Check out the [symfony/console documentation](https://symfony.com/doc/current/components/console.html) first. - -### Security Update - -Because this project is basically a PHP project running locally, generally speaking, there will be no remote attacks. -But if you find such a problem, please **DO NOT submit a PR or Issue in the GitHub repository, -You need to contact the project maintainer (crazywhalecc) via [mail](mailto:admin@zhamao.me). - -### Fix Bugs - -Fixing bugs generally does not involve modification of the project structure and framework, -so if you can locate the wrong code and fix it directly, please submit a PR directly. - -### New Extensions - -For adding a new extension, -you need to understand some basic structure of the project and how to add a new extension according to the existing logic. -It will be covered in detail in the next section on this page. -In general, you will need: - -1. Evaluate whether the extension can be compiled inline into PHP. -2. Evaluate whether the extension's dependent libraries (if any) can be compiled statically. -3. Write library compile commands on different platforms. -4. Verify that the extension and its dependencies are compatible with existing extensions and dependencies. -5. Verify that the extension works normally in `cli`, `micro`, `fpm`, `embed` SAPIs. -6. Write documentation and add your extension. - -### Project Framework Optimization - -If you are already familiar with the working principle of `symfony/console`, -and at the same time want to make some modifications or optimizations to the framework of the project, -please understand the following things first: - -1. Adding extensions does not belong to project framework optimization, -but if you find that you have to optimize the framework when adding new extensions, -you need to modify the framework itself before adding extensions. -2. For some large-scale logical modifications (such as those involving LibraryBase, Extension objects, etc.), -it is recommended to submit an Issue or Draft PR for discussion first. -3. In the early stage of the project, it was a pure private development project, and there were some Chinese comments in the code. -After internationalizing your project you can submit a PR to translate these comments into English. -4. Please do not submit more useless code fragments in the code, -such as a large number of unused variables, methods, classes, and code that has been rewritten many times. + diff --git a/docs/en/develop/build-lifecycle.md b/docs/en/develop/build-lifecycle.md new file mode 100644 index 000000000..f80e402fd --- /dev/null +++ b/docs/en/develop/build-lifecycle.md @@ -0,0 +1,6 @@ +# Build Lifecycle + + diff --git a/docs/en/develop/craft-yml.md b/docs/en/develop/craft-yml.md index e0aea228f..10f8be47d 100644 --- a/docs/en/develop/craft-yml.md +++ b/docs/en/develop/craft-yml.md @@ -4,4 +4,4 @@ aside: false # craft.yml Configuration - + diff --git a/docs/en/develop/doctor-module.md b/docs/en/develop/doctor-module.md index 64aed4afc..3bd284297 100644 --- a/docs/en/develop/doctor-module.md +++ b/docs/en/develop/doctor-module.md @@ -1,70 +1,4 @@ -# Doctor module +# Doctor Module -The Doctor module is a relatively independent module used to check the system environment, which can be entered with the command `bin/spc doctor`, and the entry command class is in `DoctorCommand.php`. - -The Doctor module is a checklist with a series of check items and automatic repair items. -These items are stored in the `src/SPC/doctor/item/` directory, -And two Attributes are used as check item tags and auto-fix item tags: `#[AsCheckItem]` and `#[AsFixItem]`. - -Take the existing check item `if necessary tools are installed`, -which is used to check whether the packages necessary for compilation are installed in the macOS system. -The following is its source code: - -```php -use SPC\doctor\AsCheckItem; -use SPC\doctor\AsFixItem; -use SPC\doctor\CheckResult; - -#[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin', level: 997)] -public function checkCliTools(): ?CheckResult -{ - $missing = []; - foreach (self::REQUIRED_COMMANDS as $cmd) { - if ($this->findCommand($cmd) === null) { - $missing[] = $cmd; - } - } - if (!empty($missing)) { - return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]); - } - return CheckResult::ok(); -} -``` - -The first parameter of the attribute is the name of the check item, -and the following `limit_os` parameter restricts the check item to be triggered only under the specified system, -and `level` is the priority of executing the check item, the larger the number, the higher the priority higher. - -The `$this->findCommand()` method used in it is the method of `SPC\builder\traits\UnixSystemUtilTrait`, -the purpose is to find the location of the system command, and return NULL if it cannot be found. - -Each check item method should return a `SPC\doctor\CheckResult`: - -- When returning `CheckResult::fail()`, the first parameter is used to output the error prompt of the terminal, - and the second parameter is the name of the repair item when this check item can be automatically repaired. -- When `CheckResult::ok()` is returned, the check passed. You can also pass a parameter to return the check result, for example: `CheckResult::ok('OS supported')`. -- When returning `CheckResult::fail()`, if the third parameter is included, the array of the third parameter will be used as the parameter of `AsFixItem`. - -The following is the method for automatically repairing items corresponding to this check item: - -```php -#[AsFixItem('build-tools')] -public function fixBuildTools(array $missing): bool -{ - foreach ($missing as $cmd) { - try { - shell(true)->exec('brew install ' . escapeshellarg($cmd)); - } catch (RuntimeException) { - return false; - } - } - return true; -} -``` - -`#[AsFixItem()]` first parameter is the name of the fix item, and this method must return True or False. -When False is returned, the automatic repair failed and manual handling is required. - -In the code here, `shell()->exec()` is the method of executing commands of the project, -which is used to replace `exec()` and `system()`, and also provides debugging, obtaining execution status, -entering directories, etc. characteristic. + diff --git a/docs/en/develop/index.md b/docs/en/develop/index.md index 33e4163f5..717f4cd4c 100644 --- a/docs/en/develop/index.md +++ b/docs/en/develop/index.md @@ -1,35 +1,4 @@ # Start Developing -Developing this project requires the installation and deployment of a PHP environment, -as well as some extensions and Composer commonly used in PHP projects. - -The development environment and running environment of the project are almost exactly the same. -You can refer to the **Manual Build** section to install system PHP or use the pre-built static PHP of this project as the environment. -I will not go into details here. - -Regardless of its purpose, this project itself is actually a `php-cli` program. You can edit and develop it as a normal PHP project. -At the same time, you need to understand the Shell languages of different systems. - -The current purpose of this project is to compile statically compiled independent PHP, -but the main part also includes compiling static versions of many dependent libraries, -so you can reuse this set of compilation logic to build independent binary versions of other programs, such as Nginx, etc. - -## Environment preparation - -A PHP environment is required to develop this project. You can use the PHP that comes with the system, -or you can use the static PHP built by this project. - -Regardless of which PHP you use, in your development environment you need to install these extensions: - -``` -curl,dom,filter,mbstring,openssl,pcntl,phar,posix,sodium,tokenizer,xml,xmlwriter -``` - -The static-php-cli project itself does not require so many extensions, but during the development process, -you will use tools such as Composer and PHPUnit, which require these extensions. - -> For micro self-executing binaries built by static-php-cli itself, only `pcntl,posix,mbstring,tokenizer,phar` is required. - -## Start development - -Continuing down to see the project structure documentation, you can learn how `static-php-cli` works. + diff --git a/docs/en/develop/package-model.md b/docs/en/develop/package-model.md new file mode 100644 index 000000000..654153659 --- /dev/null +++ b/docs/en/develop/package-model.md @@ -0,0 +1,6 @@ +# Package Model + + diff --git a/docs/en/develop/php-src-changes.md b/docs/en/develop/php-src-changes.md index 190702594..bb150ee87 100644 --- a/docs/en/develop/php-src-changes.md +++ b/docs/en/develop/php-src-changes.md @@ -1,59 +1,4 @@ -# Modifications to PHP source code +# PHP Source Modifications -During the static compilation process, static-php-cli made some modifications to the PHP source code -in order to achieve good compatibility, performance, and security. -The following is a description of the current modifications to the PHP source code. - -## Micro related patches - -Based on the patches provided by the phpmicro project, -static-php-cli has made some modifications to the PHP source code to meet the needs of static compilation. -The patches currently used by static-php-cli during compilation in the [patch list](https://github.com/easysoft/phpmicro/tree/master/patches) are: - -- static_opcache -- static_extensions_win32 -- cli_checks -- disable_huge_page -- vcruntime140 -- win32 -- zend_stream -- cli_static -- macos_iconv -- phar - -## PHP <= 8.1 libxml patch - -Because PHP only provides security updates for 8.1 and stops updating older versions, -static-php-cli applies the libxml compilation patch that has been applied in newer versions of PHP to PHP 8.1 and below. - -## gd extension Windows patch - -Compiling the gd extension under Windows requires major changes to the `config.w32` file. -static-php-cli has made some changes to the gd extension to make it easier to compile under Windows. - -## YAML extension Windows patch - -YAML extension needs to modify the `config.w32` file to compile under Windows. -static-php-cli has made some modifications to the YAML extension to make it easier to compile under Windows. - -## static-php-cli version information insertion - -When compiling, static-php-cli will insert the static-php-cli version information into the PHP version information for easy identification. - -## Add option to hardcode INI - -When using the `-I` parameter to hardcode INI into static PHP functionality, -static-php-cli will modify the PHP source code to insert the hardcoded content. - -## Linux system repair patch - -Some compilation environments may lack some system header files or libraries. -static-php-cli will automatically fix these problems during compilation, such as: - -- HAVE_STRLCAT missing problem -- HAVE_STRLCPY missing problem - -## Fiber issue fix patch for Windows - -When compiling PHP on Windows, there will be some issues with the Fiber extension. -static-php-cli will automatically fix these issues during compilation (modify `config.w32` in php-src). + diff --git a/docs/en/develop/registry.md b/docs/en/develop/registry.md new file mode 100644 index 000000000..e21b5aa5b --- /dev/null +++ b/docs/en/develop/registry.md @@ -0,0 +1,6 @@ +# Registry & Plugin System + + diff --git a/docs/en/develop/source-module.md b/docs/en/develop/source-module.md index 51c3ba3ca..f2825f329 100644 --- a/docs/en/develop/source-module.md +++ b/docs/en/develop/source-module.md @@ -1,372 +1,5 @@ -# Source module +# Source Module -The download source module of static-php-cli is a major module. -It includes dependent libraries, external extensions, PHP source code download methods and file decompression methods. -The download configuration file mainly involves the `source.json` and `pkg.json` file, which records the download method of all downloadable sources. - -The main commands involved in the download function are `bin/spc download` and `bin/spc extract`. -The `download` command is a downloader that downloads sources according to the configuration file, -and the `extract` command is an extractor that extract sources from downloaded files. - -Generally speaking, downloading sources may be slow because these sources come from various official websites, GitHub, -and other different locations. -At the same time, they also occupy a large space, so you can download the sources once and reuse them. - -The configuration file of the downloader is `source.json`, which contains the download methods of all sources. -You can add the source download methods you need, or modify the existing source download methods. - -The download configuration structure of each source is as follows. -The following is the source download configuration corresponding to the `libevent` extension: - -```json -{ - "libevent": { - "type": "ghrel", - "repo": "libevent/libevent", - "match": "libevent.+\\.tar\\.gz", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -The most important field here is `type`. Currently, the types it supports are: - -- `url`: Directly use URL to download, for example: `https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`. -- `pie`: Download PHP extensions from Packagist using the PIE (PHP Installer for Extensions) standard. -- `ghrel`: Use the GitHub Release API to download, download the artifacts uploaded from the latest version released by maintainers. -- `ghtar`: Use the GitHub Release API to download. - Different from `ghrel`, `ghtar` is downloaded from the `source code (tar.gz)` in the latest Release of the project. -- `ghtagtar`: Use GitHub Release API to download. - Compared with `ghtar`, `ghtagtar` can find the latest one from the `tags` list and download the source code in `tar.gz` format - (because some projects only use `tag` release version). -- `bitbuckettag`: Download using BitBucket API, basically the same as `ghtagtar`, except this one applies to BitBucket. -- `git`: Clone the project directly from a Git address to download sources, applicable to any public Git repository. -- `filelist`: Use a crawler to crawl the Web download site that provides file index, - and get the latest version of the file name and download it. -- `custom`: If none of the above download methods are satisfactory, you can write `custom`, - create a new class under `src/SPC/store/source/`, extends `CustomSourceBase`, and write the download script yourself. - -## source.json Common parameters - -Each source file in source.json has the following params: - -- `license`: the open source license of the source code, see **Open Source License** section below -- `type`: must be one of the types mentioned above -- `path` (optional): release the source code to the specified directory instead of `source/{name}` -- `provide-pre-built` (optional): whether to provide precompiled binary files. - If `true`, it will automatically try to download precompiled binary files when running `bin/spc download` - -::: tip -The `path` parameter in `source.json` can specify a relative or absolute path. When specified as a relative path, the path is based on `source/`. -::: - -## Download type - url - -URL type sources refer to downloading files directly from the URL. - -The parameters included are: - -- `url`: The download address of the file, such as `https://example.com/file.tgz` -- `filename` (optional): The file name saved to the local area. If not specified, the file name of the url will be used. - -Example (download the imagick extension and extract it to the extension storage path of the php source code): - -```json -{ - "ext-imagick": { - "type": "url", - "url": "https://pecl.php.net/get/imagick", - "path": "php-src/ext/imagick", - "filename": "imagick.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## Download type - pie - -PIE (PHP Installer for Extensions) type sources refer to downloading PHP extensions from Packagist that follow the PIE standard. -This method automatically fetches extension information from the Packagist repository and downloads the appropriate distribution file. - -The parameters included are: - -- `repo`: The Packagist vendor/package name, such as `vendor/package-name` - -Example (download a PHP extension from Packagist using PIE): - -```json -{ - "ext-example": { - "type": "pie", - "repo": "vendor/example-extension", - "path": "php-src/ext/example", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -::: tip -The PIE download type will automatically detect the extension information from Packagist metadata, -including the download URL, version, and distribution type. -The extension must be marked as `type: php-ext` or contain `php-ext` metadata in its Packagist package definition. -::: - -## Download type - ghrel - -ghrel will download files from Assets uploaded in GitHub Release. -First use the GitHub Release API to get the latest version, and then download the corresponding files according to the regular matching method. - -The parameters included are: - -- `repo`: GitHub repository name -- `match`: regular expression matching Assets files -- `prefer-stable`: Whether to download stable versions first (default is `false`) - -Example (download the libsodium library, matching the libsodium-x.y.tar.gz file in Release): - -```json -{ - "libsodium": { - "type": "ghrel", - "repo": "jedisct1/libsodium", - "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## Download type - ghtar - -ghtar will download the file from the GitHub Release Tag. -Unlike `ghrel`, `ghtar` will download the `source code (tar.gz)` from the latest Release of the project. - -The parameters included are: - -- `repo`: GitHub repository name -- `prefer-stable`: Whether to download stable versions first (default is `false`) - -Example (brotli library): - -```json -{ - "brotli": { - "type": "ghtar", - "repo": "google/brotli", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## Download type - ghtagtar - -Use the GitHub Release API to download. -Compared with `ghtar`, `ghtagtar` can find the latest one from the `tags` list and download the source code in `tar.gz` format -(because some projects only use the `tag` version). - -The parameters included are: - -- `repo`: GitHub repository name -- `prefer-stable`: Whether to download stable versions first (default is `false`) - -Example (gmp library): - -```json -{ - "gmp": { - "type": "ghtagtar", - "repo": "alisw/GMP", - "license": { - "type": "text", - "text": "EXAMPLE LICENSE" - } - } -} -``` - -## Download Type - bitbuckettag - -Download using BitBucket API, basically the same as `ghtagtar`, except this one works with BitBucket. - -The parameters included are: - -- `repo`: BitBucket repository name - -## Download type - git - -Clone the project directly from a Git address to download sources, applicable to any public Git repository. - -The parameters included are: - -- `url`: Git link (HTTPS only) -- `rev`: branch name - -```json -{ - "imap": { - "type": "git", - "url": "https://github.com/static-php/imap.git", - "rev": "master", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## Download type - filelist - -Use a crawler to crawl a web download site that provides a file index and get the latest version of the file name and download it. - -Note that this method is only applicable to static sites with page index functions such as mirror sites and GNU official websites. - -The parameters included are: - -- `url`: The URL of the page to crawl the latest version of the file -- `regex`: regular expression matching file names and download links - -Example (download the libiconv library from the GNU official website): - -```json -{ - "libiconv": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/libiconv/", - "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/", - "license": { - "type": "file", - "path": "COPYING" - } - } -} -``` - -## Download type - custom - -If the above downloading methods are not satisfactory, you can write `custom`, -create a new class under `src/SPC/store/source/`, extends `CustomSourceBase`, and write the download script yourself. - -I won’t go into details here, you can look at `src/SPC/store/source/PhpSource.php` or `src/SPC/store/source/PostgreSQLSource.php` as examples. - -## pkg.json General parameters - -pkg.json stores non-source-code files, such as precompiled tools musl-toolchain and UPX. It includes: - -- `type`: The same type as `source.json` and different kinds of parameters. -- `extract` (optional): The path to decompress after downloading, the default is `pkgroot/{pkg_name}`. -- `extract-files` (optional): Extract only the specified files to the specified location after downloading. - -It should be noted that `pkg.json` does not involve compilation, modification and distribution of source code, -so there is no `license` open source license field. -And you cannot use the `extract` and `extract-files` parameters at the same time. - -Example (download nasm locally and extract only program files to PHP SDK): - -```json -{ - "nasm-x86_64-win": { - "type": "url", - "url": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip", - "extract-files": { - "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" - } - } -} -``` - -The key name in `extract-files` is the file in the source folder, and the key value is the storage path. The storage path can use the following variables: - -- `{php_sdk_path}`: (Windows only) PHP SDK path -- `{pkg_root_path}`: `pkgroot/` -- `{working_dir}`: current working directory -- `{download_path}`: download directory -- `{source_path}`: source code decompression directory - -When `extract-files` does not use variables and is a relative path, the directory of the relative path is `{working_dir}`. - -## Open source license - -For `source.json`, each source file should contain an open source license. -The `license` field stores the open source license information. - -Each `license` contains the following parameters: - -- `type`: `file` or `text` -- `path`: the license file in the source code directory (required when `type` is `file`) -- `text`: License text (required when `type` is `text`) - -Example (yaml extension source code with LICENSE file): - -```json -{ - "yaml": { - "type": "git", - "path": "php-src/ext/yaml", - "rev": "php7", - "url": "https://github.com/php/pecl-file_formats-yaml", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -When an open source project has multiple licenses, multiple files can be specified: - -```json -{ - "libuv": { - "type": "ghtar", - "repo": "libuv/libuv", - "license": [ - { - "type": "file", - "path": "LICENSE" - }, - { - "type": "file", - "path": "LICENSE-extra" - } - ] - } -} -``` - -When the license of an open source project uses different files between versions, -`path` can be used as an array to list the possible license files: - -```json -{ - "redis": { - "type": "git", - "path": "php-src/ext/redis", - "rev": "release/6.0.2", - "url": "https://github.com/phpredis/phpredis", - "license": { - "type": "file", - "path": [ - "LICENSE", - "COPYING" - ] - } - } -} -``` + diff --git a/docs/en/develop/structure.md b/docs/en/develop/structure.md index e43b6d6d7..cadef786d 100644 --- a/docs/en/develop/structure.md +++ b/docs/en/develop/structure.md @@ -1,180 +1,5 @@ -# Introduction to project structure +# Project Structure -static-php-cli mainly contains three logical components: sources, dependent libraries, and extensions. -These components contains 4 configuration files: `source.json`, `pkg.json`, `lib.json`, and `ext.json`. - -A complete process for building standalone static PHP is: - -1. Use the source download module `Downloader` to download specified or all source codes. - These sources include PHP source code, dependent library source code, and extension source code. -2. Use the source decompression module `SourceExtractor` to decompress the downloaded sources to the compilation directory. -3. Use the dependency tool to calculate the dependent extensions and dependent libraries of the currently added extension, - and then compile each library that needs to be compiled in the order of dependencies. -4. After building each dependent library using `Builder` under the corresponding operating system, install it to the `buildroot` directory. -5. If external extensions are included (the source code does not contain extensions within PHP), - copy the external extensions to the `source/php-src/ext/` directory. -6. Use `Builder` to build the PHP source code and build target to the `buildroot` directory. - -The project is mainly divided into several folders: - -- `bin/`: used to store program entry files, including `bin/spc`, `bin/spc-alpine-docker`, `bin/setup-runtime`. -- `config/`: Contains all the extensions and dependent libraries supported by the project, - as well as the download link and download methods of these sources. It is divided into files: `lib.json`, `ext.json`, `source.json`, `pkg.json`, `pre-built.json` . -- `src/`: The core code of the project, including the entire framework and commands for compiling various extensions and libraries. -- `vendor/`: The directory that Composer depends on, you do not need to make any modifications to it. - -The operating principle is to start a `ConsoleApplication` of `symfony/console`, and then parse the commands entered by the user in the terminal. - -## Basic command line structure - -`bin/spc` is an entry file, including the Unix common `#!/usr/bin/env php`, -which is used to allow the system to automatically execute with the PHP interpreter installed on the system. -After the project executes `new ConsoleApplication()`, the framework will automatically register them as commands. - -The project does not directly use the Command registration method and command execution method recommended by Symfony. Here are small changes: - -1. Each command uses the `#[AsCommand()]` Attribute to register the name and description. -2. Abstract `execute()` so that all commands are based on `BaseCommand` (which is based on `Symfony\Component\Console\Command\Command`), - and the execution code of each command itself is written in the `handle()` method . -3. Added variable `$no_motd` to `BaseCommand`, which is used to display the Figlet greeting when the command is executed. -4. `BaseCommand` saves `InputInterface` and `OutputInterface` as member variables. You can use `$this->input` and `$this->output` within the command class. - -## Basic source code structure - -The source code of the project is located in the `src/SPC` directory, -supports automatic loading of the PSR-4 standard, and contains the following subdirectories and classes: - -- `src/SPC/builder/`: The core compilation command code used to build libraries, - PHP and related extensions under different operating systems, and also includes some compilation system tool methods. -- `src/SPC/command/`: All commands of the project are here. -- `src/SPC/doctor/`: Doctor module, which is a relatively independent module used to check the system environment. - It can be entered using the command `bin/spc doctor`. -- `src/SPC/exception/`: exception class. -- `src/SPC/store/`: Classes related to storage, files and sources are all here. -- `src/SPC/util/`: Some reusable tool methods are here. -- `src/SPC/ConsoleApplication.php`: command line program entry file. - -If you have read the source code, you may find that there is also a `src/globals/` directory, -which is used to store some global variables, global methods, -and non-PSR-4 standard PHP source code that is relied upon during the build process, such as extension sanity check code etc. - -## Phar application directory issue - -Like other php-cli projects, spc itself has additional considerations for paths. -Because spc can run in multiple modes such as `php-cli directly`, `micro SAPI`, `php-cli with Phar`, `vendor with Phar`, etc., -there are ambiguities in various root directories. A complete explanation is given here. -This problem is generally common in the base class path selection problem of accessing files in PHP projects, especially when used with `micro.sfx`. - -Note that this may only be useful for you when developing Phar projects or PHP frameworks. - -> Next, we will treat `static-php-cli` (that is, spc) as a normal `php` command line program. You can understand spc as any of your own php-cli applications for reference. - -There are three basic constant theoretical values below. We recommend that you introduce these three constants when writing PHP projects: - -- `WORKING_DIR`: the working directory when executing PHP scripts - -- `SOURCE_ROOT_DIR` or `ROOT_DIR`: the root directory of the project folder, generally the directory where `composer.json` is located - -- `FRAMEWORK_ROOT_DIR`: the root directory of the framework used, which may be used by self-developed frameworks. Generally, the framework directory is read-only - -You can define these constants in your framework entry or cli applications to facilitate the use of paths in your project. - -The following are PHP built-in constant values, which have been defined inside the PHP interpreter: - -- `__DIR__`: the directory where the file of the currently executed script is located - -- `__FILE__`: the file path of the currently executed script - -### Git project mode (source) - -Git project mode refers to a framework or program itself stored in plain text in the current folder, and running through `php path/to/entry.php`. - -Assume that your project is stored in the `/home/example/static-php-cli/` directory, or your project is the framework itself, -which contains project files such as `composer.json`: - -``` -composer.json -src/App/MyCommand.app -vendor/* -bin/entry.php -``` - -We assume that the above constants are obtained from `src/App/MyCommand.php`: - -| Constant | Value | -|----------------------|------------------------------------------------------| -| `WORKING_DIR` | `/home/example/static-php-cli` | -| `SOURCE_ROOT_DIR` | `/home/example/static-php-cli` | -| `FRAMEWORK_ROOT_DIR` | `/home/example/static-php-cli` | -| `__DIR__` | `/home/example/static-php-cli/src/App` | -| `__FILE__` | `/home/example/static-php-cli/src/App/MyCommand.php` | - -In this case, the values of `WORKING_DIR`, `SOURCE_ROOT_DIR`, and `FRAMEWORK_ROOT_DIR` are exactly the same: `/home/example/static-php-cli`. - -The source code of the framework and the source code of the application are both in the current path. - -### Vendor library mode (vendor) - -The vendor library mode generally means that your project is a framework or is installed into the project as a composer dependency by other applications, -and the storage location is in the `vendor/author/XXX` directory. - -Suppose your project is `crazywhalecc/static-php-cli`, and you or others install this project in another project using `composer require`. - -We assume that static-php-cli contains all files except the `vendor` directory with the same `Git mode`, and get the constant value from `src/App/MyCommand`, -Directory constant should be: - -| Constant | Value | -|----------------------|--------------------------------------------------------------------------------------| -| `WORKING_DIR` | `/home/example/another-app` | -| `SOURCE_ROOT_DIR` | `/home/example/another-app` | -| `FRAMEWORK_ROOT_DIR` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli` | -| `__DIR__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App` | -| `__FILE__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | - -Here `SOURCE_ROOT_DIR` refers to the root directory of the project using `static-php-cli`. - -### Git project Phar mode (source-phar) - -Git project Phar mode refers to the mode of packaging the project directory of the Git project mode into a `phar` file. We assume that `/home/example/static-php-cli` will be packaged into a Phar file, and the directory has the following files: - -``` -composer.json -src/App/MyCommand.app -vendor/* -bin/entry.php -``` - -When packaged into `app.phar` and stored in the `/home/example/static-php-cli` directory, `app.phar` is executed at this time. Assuming that the `src/App/MyCommand` code is executed, the constant is obtained in the file: - -| Constant | Value | -|----------------------|----------------------------------------------------------------------| -| `WORKING_DIR` | `/home/example/static-php-cli` | -| `SOURCE_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | -| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | -| `__DIR__` | `phar:///home/example/static-php-cli/app.phar/src/App` | -| `__FILE__` | `phar:///home/example/static-php-cli/app.phar/src/App/MyCommand.php` | - -Because the `phar://` protocol is required to read files in the phar itself, the project root directory and the framework directory will be different from `WORKING_DIR`. - -### Vendor Library Phar Mode (vendor-phar) - -Vendor Library Phar Mode means that your project is installed as a framework in other projects and stored in the `vendor` directory. - -We assume that your project directory structure is as follows: - -``` -composer.json # Composer configuration file of the current project -box.json # Configuration file for packaging Phar -another-app.php # Entry file of another project -vendor/crazywhalecc/static-php-cli/* # Your project is used as a dependent library -``` - -When packaging these files under the directory `/home/example/another-app/` into `app.phar`, the value of the following constant for your project should be: - -| Constant | Value | -|----------------------|------------------------------------------------------------------------------------------------------| -| `WORKING_DIR` | `/home/example/another-app` | -| `SOURCE_ROOT_DIR` | `phar:///home/example/another-app/app.phar/` | -| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli` | -| `__DIR__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App` | -| `__FILE__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | + diff --git a/docs/en/develop/system-build-tools.md b/docs/en/develop/system-build-tools.md index 48273f639..a5ac6edfe 100644 --- a/docs/en/develop/system-build-tools.md +++ b/docs/en/develop/system-build-tools.md @@ -1,242 +1,4 @@ # Compilation Tools -static-php-cli uses many system compilation tools when building static PHP. These tools mainly include: - -- `autoconf`: used to generate `configure` scripts. -- `make`: used to execute `Makefile`. -- `cmake`: used to execute `CMakeLists.txt`. -- `pkg-config`: Used to find the installation path of dependent libraries. -- `gcc`: used to compile C/C++ projects under Linux. -- `clang`: used to compile C/C++ projects under macOS. - -For Linux and macOS operating systems, -these tools can usually be installed through the package manager, which is written in the doctor module. -Theoretically we can also compile and download these tools manually, -but this will increase the complexity of compilation, so we do not recommend this. - -## Linux Compilation Tools - -For Linux systems, different distributions have different installation methods for compilation tools. -And for static compilation, the package management of some distributions cannot install libraries and tools for pure static compilation. -Therefore, for the Linux platform and its different distributions, -we currently provide a variety of compilation environment preparations. - -### Glibc Environment - -The glibc environment refers to the underlying `libc` library of the system -(that is, the C standard library that all programs written in C language are dynamically linked to) uses `glibc`, -which is the default environment for most distributions. -For example: Ubuntu, Debian, CentOS, RHEL, openSUSE, Arch Linux, etc. - -In the glibc environment, the package management and compiler we use point to glibc by default, -and glibc cannot be statically linked well. -One of the reasons it cannot be statically linked is that its network library `nss` cannot be compiled statically. - -For the glibc environment, in static-php-cli and spc in 2.0-RC8 and later, you can choose two ways to build static PHP: - -1. Use Docker to build, you can use `bin/spc-alpine-docker` to build, it will build an Alpine Linux docker image. -2. Use `bin/spc doctor --auto-fix` to install the `musl-wrapper` and `musl-cross-make` packages, and then build directly. -([Related source code](https://github.com/crazywhalecc/static-php-cli/blob/main/src/SPC/doctor/item/LinuxMuslCheck.php)) - -Generally speaking, the build results in these two environments are consistent, and you can choose according to actual needs. - -In the doctor module, static-php-cli will first detect the current Linux distribution. -If the current distribution is a glibc environment, you will be prompted to install the musl-wrapper and musl-cross-make packages. - -The process of installing `musl-wrapper` in the glibc environment is as follows: - -1. Download the specific version of [musl-wrapper source code](https://musl.libc.org/releases/) from the musl official website. -2. Use `gcc` installed from the package management to compile the musl-wrapper source code and generate `musl-libc` and other libraries: `./configure --disable-gcc-wrapper && make -j && sudo make install`. -3. The musl-wrapper related libraries will be installed in the `/usr/local/musl` directory. - -The process of installing `musl-cross-make` in the glibc environment is as follows: - -1. Download the precompiled [musl-cross-make](https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/) compressed package from dl.static-php.dev . -2. Unzip to the `/usr/local/musl` directory. - -::: tip -In the glibc environment, static compilation can be achieved by directly installing musl-wrapper, -but musl-wrapper only contains `musl-gcc` and not `musl-g++`, which means that C++ code cannot be compiled. -So we need musl-cross-make to provide `musl-g++`. - -The reason why the musl-cross-make package cannot be compiled directly locally is that -its compilation environment requirements are relatively high (requires more than 36GB of memory, compiled under Alpine Linux), -so we provide precompiled binary packages that can be used for all Linux distributions. - -At the same time, the package management of some distributions provides musl-wrapper, -but musl-cross-make needs to match the corresponding musl-wrapper version, -so we do not use package management to install musl-wrapper. - -Compiling musl-cross-make will be introduced in the **musl-cross-make Toolchain Compilation** section of this chapter. -::: - -### Musl Environment - -The musl environment refers to the system's underlying `libc` library that uses `musl`, -which is a lightweight C standard library that can be well statically linked. - -For the currently popular Linux distributions, Alpine Linux uses the musl environment, -so static-php-cli can directly build static PHP under Alpine Linux. -You only need to install basic compilation tools (such as `gcc`, `cmake`, etc.) directly from the package management. - -For other distributions, if your distribution uses the musl environment, -you can also use static-php-cli to build static PHP directly after installing the necessary compilation tools. - -::: tip -In the musl environment, static-php-cli will automatically skip the installation of musl-wrapper and musl-cross-make. -::: - -### Docker Environment - -The Docker environment refers to using Docker containers to build static PHP. You can use `bin/spc-alpine-docker` to build. -Before executing this command, you need to install Docker first, and then execute `bin/spc-alpine-docker` in the project root directory. - -After executing `bin/spc-alpine-docker`, static-php-cli will automatically download the Alpine Linux image and then build a `cwcc-spc-x86_64` or `cwcc-spc-aarch64` image. -Then all build process is performed within this image, which is equivalent to compiling in Alpine Linux. - -## musl-cross-make Toolchain Compilation - -In Linux, although you do not need to manually compile the musl-cross-make tool, -if you want to understand its compilation process, you can refer here. -Another important reason is that this may not be compiled using automated tools such as CI and Actions, -because the existing CI service compilation environment does not meet the compilation requirements of musl-cross-make, -and the configuration that meets the requirements is too expensive. - -The compilation process of musl-cross-make is as follows: - -Prepare an Alpine Linux environment (either directly installed or using Docker). -The compilation process requires more than **36GB** of memory, -so you need to compile on a machine with larger memory. -Without this much memory, compilation may fail. - -Then write the following content into the `config.mak` file: - -```makefile -STAT = -static --static -FLAG = -g0 -Os -Wno-error - -ifneq ($(NATIVE),) -COMMON_CONFIG += CC="$(HOST)-gcc ${STAT}" CXX="$(HOST)-g++ ${STAT}" -else -COMMON_CONFIG += CC="gcc ${STAT}" CXX="g++ ${STAT}" -endif - -COMMON_CONFIG += CFLAGS="${FLAG}" CXXFLAGS="${FLAG}" LDFLAGS="${STAT}" - -BINUTILS_CONFIG += --enable-gold=yes --enable-gprofng=no -GCC_CONFIG += --enable-static-pie --disable-cet --enable-default-pie -#--enable-default-pie - -CONFIG_SUB_REV = 888c8e3d5f7b -GCC_VER = 13.2.0 -BINUTILS_VER = 2.40 -MUSL_VER = 1.2.4 -GMP_VER = 6.2.1 -MPC_VER = 1.2.1 -MPFR_VER = 4.2.0 -LINUX_VER = 6.1.36 -``` - -And also you need to add `gcc-13.2.0.tar.xz.sha1` file, contents here: - -``` -5f95b6d042fb37d45c6cbebfc91decfbc4fb493c gcc-13.2.0.tar.xz -``` - -If you are using Docker to build, create a new `Dockerfile` file and write the following content: - -```dockerfile -FROM alpine:edge - -RUN apk add --no-cache \ -gcc g++ git make curl perl \ -rsync patch wget libtool \ -texinfo autoconf automake \ -bison tar xz bzip2 zlib \ -file binutils flex \ -linux-headers libintl \ -gettext gettext-dev icu-libs pkgconf \ -pkgconfig icu-dev bash \ -ccache libarchive-tools zip - -WORKDIR /opt - -RUN git clone https://git.zv.io/toolchains/musl-cross-make.git -WORKDIR /opt/musl-cross-make -COPY config.mak /opt/musl-cross-make -COPY gcc-13.2.0.tar.xz.sha1 /opt/musl-cross-make/hashes - -RUN make TARGET=x86_64-linux-musl -j || : -RUN sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h -RUN make TARGET=x86_64-linux-musl -j -RUN make TARGET=x86_64-linux-musl install -j -RUN tar cvzf x86_64-musl-toolchain.tgz output/* -``` - -If you are using Alpine Linux in a non-Docker environment, you can directly execute the commands in the Dockerfile, for example: - -```bash -apk add --no-cache \ -gcc g++ git make curl perl \ -rsync patch wget libtool \ -texinfo autoconf automake \ -bison tar xz bzip2 zlib \ -file binutils flex \ -linux-headers libintl \ -gettext gettext-dev icu-libs pkgconf \ -pkgconfig icu-dev bash \ -ccache libarchive-tools zip - -git clone https://git.zv.io/toolchains/musl-cross-make.git -# Copy config.mak to the working directory of musl-cross-make. -# You need to replace /path/to/config.mak with your config.mak file path. -cp /path/to/config.mak musl-cross-make/ -cp /path/to/gcc-13.2.0.tar.xz.sha1 musl-cross-make/hashes - -make TARGET=x86_64-linux-musl -j || : -sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h -make TARGET=x86_64-linux-musl -j -make TARGET=x86_64-linux-musl install -j -tar cvzf x86_64-musl-toolchain.tgz output/* -``` - -::: tip -All the above scripts are suitable for x86_64 architecture Linux. -If you need to build musl-cross-make for the ARM environment, just replace all `x86_64` above with `aarch64`. -::: - -This compilation process may fail due to insufficient memory, network problems, etc. -You can try a few more times, or use a machine with larger memory to compile. -If you encounter problems or you have better improvement solutions, go to [Discussion](https://github.com/crazywhalecc/static-php-cli-hosted/issues/1). - -## macOS Environment - -For macOS systems, the main compilation tool we use is `clang`, -which is the default compiler for macOS systems and is also the compiler of Xcode. - -Compiling under macOS mainly relies on Xcode or Xcode Command Line Tools. -You can download Xcode from the App Store, -or execute `xcode-select --install` in the terminal to install Xcode Command Line Tools. - -In addition, in the `doctor` environment check module, static-php-cli will check whether Homebrew, -compilation tools, etc. are installed on the macOS system. -If not, you will be prompted to install them. I will not go into details here. - -## FreeBSD Environment - -FreeBSD is also a Unix system, and its compilation tools are similar to macOS. -You can directly use the package management `pkg` to install `clang` and other compilation tools through the `doctor` command. - -## pkg-config Compilation (*nix only) - -If you observe the compilation log when using static-php-cli to build static PHP, you will find that no matter what is compiled, -`pkg-config` will be compiled first. This is because `pkg-config` is a library used to find dependencies. -In earlier versions of static-php-cli, we directly used the `pkg-config` tool installed by package management, -but this would cause some problems, such as: - -- Even if `PKG_CONFIG_PATH` is specified, `pkg-config` will try to find dependent packages from the system path. -- Since `pkg-config` will look for dependent packages from the system path, - if a dependent package with the same name exists in the system, compilation may fail. - -In order to avoid the above problems, we compile `pkg-config` into `buildroot/bin` in user mode and use it. -We use parameters such as `--without-sysroot` to avoid looking for dependent packages from the system path. + diff --git a/docs/en/develop/vendor-mode/annotations.md b/docs/en/develop/vendor-mode/annotations.md new file mode 100644 index 000000000..fa1790699 --- /dev/null +++ b/docs/en/develop/vendor-mode/annotations.md @@ -0,0 +1,6 @@ +# Annotations Reference + + diff --git a/docs/en/develop/vendor-mode/dependency-injection.md b/docs/en/develop/vendor-mode/dependency-injection.md new file mode 100644 index 000000000..2fb4be676 --- /dev/null +++ b/docs/en/develop/vendor-mode/dependency-injection.md @@ -0,0 +1,6 @@ +# Dependency Injection + + diff --git a/docs/en/develop/vendor-mode/index.md b/docs/en/develop/vendor-mode/index.md new file mode 100644 index 000000000..47c2e19c5 --- /dev/null +++ b/docs/en/develop/vendor-mode/index.md @@ -0,0 +1,6 @@ +# Vendor Mode + + diff --git a/docs/en/develop/vendor-mode/lifecycle-hooks.md b/docs/en/develop/vendor-mode/lifecycle-hooks.md new file mode 100644 index 000000000..d5502dbac --- /dev/null +++ b/docs/en/develop/vendor-mode/lifecycle-hooks.md @@ -0,0 +1,6 @@ +# Lifecycle Hooks + + diff --git a/docs/en/develop/vendor-mode/package-classes.md b/docs/en/develop/vendor-mode/package-classes.md new file mode 100644 index 000000000..3c56c0a08 --- /dev/null +++ b/docs/en/develop/vendor-mode/package-classes.md @@ -0,0 +1,6 @@ +# Writing Package Classes + + diff --git a/docs/en/faq/index.md b/docs/en/faq/index.md index b65dca463..ca3c4f842 100644 --- a/docs/en/faq/index.md +++ b/docs/en/faq/index.md @@ -1,108 +1,10 @@ -# FAQ - -Here will be some questions that you may encounter easily. There are currently many, but I need to take time to organize them. - -## What is the path of php.ini ? - -On Linux, macOS and FreeBSD, the path of `php.ini` is `/usr/local/etc/php/php.ini`. -On Windows, the path is `C:\windows\php.ini` or the current directory of `php.exe`. -The directory where to look for `php.ini` can be changed on *nix using the manual build option `--with-config-file-path`. - -In addition, on Linux, macOS and FreeBSD, `.ini` files present in the `/usr/local/etc/php/conf.d` directory will also be loaded. -On Windows, this path is empty by default. -The directory can be changed using the manual build option `--with-config-file-scan-dir`. - -`php.ini` will also be searched for in [the other standard locations](https://www.php.net/manual/configuration.file.php). - -## Can statically-compiled PHP install extensions? - -Because the principle of installing PHP extensions under the normal mode is to use `.so` type dynamic link library to install new extensions, -and we use the static link PHP compiled by this project. However, static linking has different definitions in different operating systems. - -First of all, for Linux systems, statically linked binaries will not link the system's dynamic link library. -Purely statically linked binaries (`build with -all-static`) cannot load dynamic libraries, so new extensions cannot be added. -At the same time, in pure static mode, you cannot use extensions such as `ffi` to load external `.so` modules. - -You can use the command `ldd buildroot/bin/php` to check whether the binary you built under Linux is purely statically linked. - -If you [build GNU libc based PHP](../guide/build-with-glibc), you can use the `ffi` extension to load external `.so` modules and load `.so` extensions with the same ABI. - -For example, you can use the following command to build a static PHP binary dynamically linked with glibc, -supporting FFI extensions and loading the `xdebug.so` extension of the same PHP version and the same TS type: - -```bash -bin/spc-gnu-docker download --for-extensions=ffi,xml --with-php=8.4 -bin/spc-gnu-docker build ffi,xml --build-cli --debug - -buildroot/bin/php -d "zend_extension=/path/to/php{PHP_VER}-{ts/nts}/xdebug.so" --ri xdebug -``` - -For macOS platform, almost all binaries under macOS cannot be truly purely statically linked, and almost all binaries will link macOS system libraries: `/usr/lib/libresolv.9.dylib` and `/usr/lib/libSystem.B.dylib`. -So on macOS, you can **directly** use SPC to build statically compiled PHP binaries with dynamically linked extensions: - -1. Build shared extension `xxx.so` using: `--build-shared=XXX` option. e.g. `bin/spc build bcmath,zlib --build-shared=xdebug --build-cli` -2. You will get `buildroot/modules/xdebug.so` and `buildroot/bin/php`. -3. The `xdebug.so` file could be used for php that version and thread-safe are the same. - -For the Windows platform, since officially built extensions (such as `php_yaml.dll`) force the use of the `php8.dll` dynamic library as a link, and statically built PHP does not include any dynamic libraries other than system libraries, -php.exe built by static-php cannot load officially built dynamic extensions. Since static-php-cli does not yet support building dynamic extensions, there is currently no way to load dynamic extensions with static-php. - -However, Windows can normally use the `FFI` extension to load other dll files and call them. - -## Can it support Oracle database extension? - -Some extensions that rely on closed source libraries, such as `oci8`, `sourceguardian`, etc., -they do not provide purely statically compiled dependent library files (`.a`), only dynamic dependent library files (`.so`). -These extensions cannot be compiled into static-php-cli using source code, so this project may never support these extensions. -However, in theory you can access and use such extensions under macOS and Linux according to the above questions. - -If you have a need for such extensions, or most people have needs for these closed-source extensions, -see the discussion on [standalone-php-cli](https://github.com/crazywhalecc/static-php-cli/discussions/58). Welcome to leave a message. - -## Does it support Windows? - -The project currently supports Windows, but the number of supported extensions is small. Windows support is not perfect. There are mainly the following problems: - -1. The compilation process of Windows is different from that of *nix, and the toolchain used is also different. The compilation tools used to compile the dependent libraries of each extension are almost completely different. -2. The demand for the Windows version will also be advanced based on the needs of all people who use this project. If many people need it, I will support related extensions as soon as possible. - -## Can I protect my source code with micro? - -You can't. micro.sfx is essentially combining php and php code into one file, -there is no process of compiling or encrypting the PHP code. - -First of all, php-src is the official interpreter of PHP code, and there is no PHP compiler compatible with mainstream branches on the market. -I saw on the Internet that there is a project called BPC (Binary PHP Compiler?) that can compile PHP into binary, -but there are many restrictions. - -The direction of encrypting and protecting the code is not the same as compiling. -After compiling, the code can also be obtained through reverse engineering and other methods. -The real protection is still carried out by means of packing and encrypting the code. - -Therefore, this project (static-php-cli) and related projects (lwmbs, swoole-cli) all provide a convenient compilation tool for php-src source code. -The phpmicro referenced by this project and related projects is only a package of PHP's sapi interface, not a compilation tool for PHP code. -The compiler for PHP code is a completely different project, so the extra cases are not taken into account. -If you are interested in encryption, you can consider using existing encryption technologies, -such as Swoole Compiler, Source Guardian, etc. - -## Unable to use ssl - -**Update: This issue has been fixed in the latest version of static-php-cli, which now reads the system's certificate file by default. If you still have problems, try the solution below.** - -When using curl, pgsql, etc. to request an HTTPS website or establish an SSL connection, there may be an `error:80000002:system library::No such file or directory` error. -This error is caused by statically compiled PHP without specifying `openssl.cafile` via `php.ini`. - -You can solve this problem by specifying `php.ini` before using PHP and adding `openssl.cafile=/path/to/your-cert.pem` in the INI. - -For Linux systems, you can download the [cacert.pem](https://curl.se/docs/caextract.html) file from the curl official website, or you can use the certificate file that comes with the system. -For the certificate locations of different distros, please refer to [Golang docs](https://go.dev/src/crypto/x509/root_linux.go). - -> INI configuration `openssl.cafile` cannot be set dynamically using the `ini_set()` function, because `openssl.cafile` is a `PHP_INI_SYSTEM` type configuration and can only be set in the `php.ini` file. - -## Why don't we support older versions of PHP? - -Because older versions of PHP have many problems, such as security issues, performance issues, and functional issues. -In addition, many older versions of PHP are not compatible with the latest dependency libraries, -which is one of the reasons why older versions of PHP are not supported. - -You can use older versions compiled earlier by static-php-cli, such as PHP 8.0, but earlier versions will not be explicitly supported. +# Frequently Asked Questions + + diff --git a/docs/en/guide/cli-generator.md b/docs/en/guide/cli-generator.md index 87163d000..93cad96df 100644 --- a/docs/en/guide/cli-generator.md +++ b/docs/en/guide/cli-generator.md @@ -2,15 +2,6 @@ aside: false --- - +# Build Command Generator -# CLI Build Command Generator - -::: tip -The extensions selected below may contain extensions that are not supported by the selected operating system, -which may cause compilation to fail. Please check [Supported Extensions](./extensions) first. -::: - - + diff --git a/docs/en/guide/cli-reference.md b/docs/en/guide/cli-reference.md new file mode 100644 index 000000000..a5a259498 --- /dev/null +++ b/docs/en/guide/cli-reference.md @@ -0,0 +1,6 @@ +# CLI Reference + + diff --git a/docs/en/guide/deps-map.md b/docs/en/guide/deps-map.md index 79100041c..d18973939 100644 --- a/docs/en/guide/deps-map.md +++ b/docs/en/guide/deps-map.md @@ -1,26 +1,4 @@ ---- -outline: 'deep' ---- - # Dependency Table -When compiling PHP, each extension and library has dependencies, which may be required or optional. -You can choose whether to include these optional dependencies. - -For example, when compiling the `gd` extension under Linux, -the `zlib,libpng` libraries and the `zlib` extension are forced to be compiled, -while the `libavif,libwebp,libjpeg,freetype` libraries are optional libraries and will not be compiled by default -unless specified by the `--with-libs=avif,webp,jpeg,freetype` option. - -- For optional extensions (optional features of extensions), you need to specify them manually at compile time, for example, to enable igbinary support for Redis: `bin/spc build redis,igbinary`. -- For optional libraries, you need to compile and specify them through the `--with-libs=XXX` option. -- If you want to enable all optional extensions, you can use `bin/spc build redis --with-suggested-exts`. -- If you want to enable all optional libraries, you can use `--with-suggested-libs`. - -## Extension Dependency Table - - - -## Library Dependency Table - - \ No newline at end of file + diff --git a/docs/en/guide/env-vars.md b/docs/en/guide/env-vars.md index 11cf93399..6f1267396 100644 --- a/docs/en/guide/env-vars.md +++ b/docs/en/guide/env-vars.md @@ -1,121 +1,4 @@ -# Environment variables +# Environment Variables -All environment variables mentioned in the list on this page have default values unless otherwise noted. -You can override the default values by setting these environment variables. - -## Environment variables list - -Starting from version 2.3.5, we have centralized the environment variables in the `config/env.ini` file. -You can set environment variables by modifying this file. - -We divide the environment variables supported by static-php-cli into three types: - -- Global internal environment variables: declared after static-php-cli starts, you can use `getenv()` to get them internally in static-php-cli, and you can override them before starting static-php-cli. -- Fixed environment variables: declared after static-php-cli starts, you can only use `getenv()` to get them, but you cannot override them through shell scripts. -- Config file environment variables: declared before static-php-cli build, you can set these environment variables by modifying the `config/env.ini` file or through shell scripts. - -You can read the comments for each parameter in [config/env.ini](https://github.com/crazywhalecc/static-php-cli/blob/main/config/env.ini) to understand its purpose. - -## Custom environment variables - -Generally, you don't need to modify any of the following environment variables as they are already set to optimal values. -However, if you have special needs, you can set these environment variables to meet your needs -(for example, you need to debug PHP performance under different compilation parameters). - -If you want to use custom environment variables, you can use the `export` command in the terminal or set the environment variables directly before the command, for example: - -```shell -# export first -export SPC_CONCURRENCY=4 -bin/spc build mbstring,pcntl --build-cli - -# or direct use -SPC_CONCURRENCY=4 bin/spc build mbstring,pcntl --build-cli -``` - -Or, if you need to modify an environment variable for a long time, you can modify the `config/env.ini` file. - -`config/env.ini` is divided into three sections, `[global]` is globally effective, `[windows]`, `[macos]`, `[linux]` are only effective for the corresponding operating system. - -For example, if you need to modify the `./configure` command for compiling PHP, you can find the `SPC_CMD_PREFIX_PHP_CONFIGURE` environment variable in the `config/env.ini` file, and then modify its value. - -If your build conditions are more complex and require multiple `env.ini` files to switch, -we recommend that you use the `config/env.custom.ini` file. -In this way, you can specify your environment variables by writing additional override items -without modifying the default `config/env.ini` file. - -```ini -; This is an example of `config/env.custom.ini` file, -; we modify the `SPC_CONCURRENCY` and linux default CFLAGS passing to libs and PHP -[global] -SPC_CONCURRENCY=4 - -[linux] -SPC_DEFAULT_C_FLAGS="-O3" -``` - -## Library environment variables (Unix only) - -Starting from 2.2.0, static-php-cli supports custom environment variables for all compilation dependent library commands of macOS, Linux, FreeBSD and other Unix systems. - -In this way, you can adjust the behavior of compiling dependent libraries through environment variables at any time. -For example, you can set the optimization parameters for compiling the xxx library through `xxx_CFLAGS=-O0`. - -Of course, not every library supports the injection of environment variables. -We currently provide three wildcard environment variables with the suffixes: - -- `_CFLAGS`: CFLAGS for the compiler -- `_LDFLAGS`: LDFLAGS for the linker -- `_LIBS`: LIBS for the linker - -The prefix is the name of the dependent library, and the specific name of the library is subject to `lib.json`. -Among them, the library name with `-` needs to replace `-` with `_`. - -Here is an example of an optimization option that replaces the openssl library compilation: - -```shell -openssl_CFLAGS="-O0" -``` - -The library name uses the same name listed in `lib.json` and is case-sensitive. - -::: tip -When no relevant environment variables are specified, except for the following variables, the remaining values are empty by default: - -| var name | var default value | -|-----------------------|-------------------------------------------------------------------------------------------------| -| `pkg_config_CFLAGS` | macOS: `$SPC_DEFAULT_C_FLAGS -Wimplicit-function-declaration -Wno-int-conversion`, Other: empty | -| `pkg_config_LDFLAGS` | Linux: `--static`, Other: empty | -| `imagemagick_LDFLAGS` | Linux: `-static`, Other: empty | -| `imagemagick_LIBS` | macOS: `-liconv`, Other: empty | -| `ldap_LDFLAGS` | `-L$BUILD_LIB_PATH` | -| `openssl_CFLAGS` | Linux: `$SPC_DEFAULT_C_FLAGS`, Other: empty | -| others... | empty | -::: - -The following table is a list of library names that support customizing the above three variables: - -| lib name | -|-------------| -| brotli | -| bzip | -| curl | -| freetype | -| gettext | -| gmp | -| imagemagick | -| ldap | -| libargon2 | -| libavif | -| libcares | -| libevent | -| openssl | - -::: tip -Because adapting custom environment variables to each library is a particularly tedious task, -and in most cases you do not need custom environment variables for these libraries, -so we currently only support custom environment variables for some libraries. - -If the library you need to customize environment variables is not listed above, -you can submit your request through [GitHub Issue](https://github.com/crazywhalecc/static-php-cli/issues). -::: + diff --git a/docs/en/guide/extension-notes.md b/docs/en/guide/extension-notes.md index 7096c0ee6..087b7822e 100644 --- a/docs/en/guide/extension-notes.md +++ b/docs/en/guide/extension-notes.md @@ -1,168 +1,3 @@ # Extension Notes -Because it is a static compilation, extensions will not compile 100% perfectly, -and different extensions have different requirements for PHP and the environment, -which will be listed one by one here. - -## curl - -HTTP3 support is not enabled by default, compile with `--with-libs="nghttp2,nghttp3,ngtcp2"` to enable HTTP3 support for PHP >= 8.4. - -When using curl to request HTTPS, there may be an `error:80000002:system library::No such file or directory` error. -For details on the solution, see [FAQ - Unable to use ssl](../faq/#unable-to-use-ssl). - -## phpmicro - -1. Only PHP >= 8.0 is supported. - -## swoole - -1. swoole >= 5.0 Only PHP >= 8.0 is supported. -2. swoole Currently, curl hooks are not supported for PHP 8.0.x (which may be fixed in the future). -3. When compiling, if only `swoole` extension is included, the supported Swoole database coroutine hook will not be fully enabled. - If you need to use it, please add the corresponding `swoole-hook-xxx` extension. -4. The `zend_mm_heap corrupted` problem may occur in swoole under some extension combinations. The cause has not yet been found. - -## swoole-hook-pgsql - -swoole-hook-pgsql is not an extension, it's a Hook feature of Swoole. -If you use `swoole,swoole-hook-pgsql`, you will enable Swoole's PostgreSQL client and the coroutine mode of the `pdo_pgsql` extension. - -swoole-hook-pgsql conflicts with the `pdo_pgsql` extension. If you want to use Swoole and `pdo_pgsql`, please delete the pdo_pgsql extension and enable `swoole` and `swoole-hook-pgsql`. -This extension contains an implementation of the coroutine environment for `pdo_pgsql`. - -On macOS systems, `pdo_pgsql` may not be able to connect to the postgresql server normally, please use it with caution. - -## swoole-hook-mysql - -swoole-hook-mysql is not an extension, it's a Hook feature of Swoole. -If you use `swoole,swoole-hook-mysql`, you will enable the coroutine mode of Swoole's `mysqlnd` and `pdo_mysql`. - -## swoole-hook-sqlite - -swoole-hook-sqlite is not an extension, it's a Hook feature of Swoole. -If you use `swoole,swoole-hook-sqlite`, you will enable the coroutine mode of Swoole's `pdo_sqlite` (Swoole must be 5.1 or above). - -swoole-hook-sqlite conflicts with the `pdo_sqlite` extension. If you want to use Swoole and `pdo_sqlite`, please delete the pdo_sqlite extension and enable `swoole` and `swoole-hook-sqlite`. -This extension contains an implementation of the coroutine environment for `pdo_sqlite`. - -## swoole-hook-odbc - -swoole-hook-odbc is not an extension, it's a Hook feature of Swoole. -If you use `swoole,swoole-hook-odbc`, you will enable the coroutine mode of Swoole's `odbc` extension. - -swoole-hook-odbc conflicts with the `pdo_odbc` extension. If you want to use Swoole and `pdo_odbc`, please delete the `pdo_odbc` extension and enable `swoole` and `swoole-hook-odbc`. -This extension contains an implementation of the coroutine environment for `pdo_odbc`. - -## swow - -1. Only PHP 8.0+ is supported. - -## imagick - -1. OpenMP support is disabled, this is recommended by the maintainers and also the case system packages. - -## imap - -1. Kerberos is not supported -2. ext-imap is not thread safe due to the underlying c-client. It's not possible to use it in `--enable-zts` builds. -3. The extension was dropped from php 8.4, we recommend you look for an alternative implementation, such as [Webklex/php-imap](https://github.com/Webklex/php-imap) - -## gd - -1. gd Extension relies on more additional Graphics library. By default, -using `bin/spc build gd` directly will not support some Graphics library, such as `libjpeg`, `libavif`, etc. -Currently, it supports four libraries: `freetype,libjpeg,libavif,libwebp`. -Therefore, the following command can be used to introduce them into the gd library: - -```bash -bin/spc build gd --with-libs=freetype,libjpeg,libavif,libwebp --build-cli -``` - -## mcrypt - -1. Currently not supported, and this extension will not be supported in the future. [#32](https://github.com/crazywhalecc/static-php-cli/issues/32) - -## oci8 - -1. oci8 is an extension of the Oracle database, because the library on which the extension provided by Oracle does not provide a statically compiled version (`.a`) or source code, -and this extension cannot be compiled into php by static linking, so it cannot be supported. - -## xdebug - -1. Xdebug is only buildable as a shared extension. On Linux, you'll need to use a SPC_TARGET like `native-native -dynamic` or `native-native-gnu`. -2. When using Linux/glibc or macOS, you can compile Xdebug as a shared extension using --build-shared="xdebug". - The compiled `./php` binary can be configured and run by specifying the INI, eg `./php -d 'zend_extension=/path/to/xdebug.so' your-code.php`. - -## xml - -1. xml includes xml, xmlreader, xmlwriter, xsl, dom, simplexml, etc. - When adding xml extensions, it is best to enable these extensions at the same time. -2. libxml is included in xml extension. Enabling xml is equivalent to enabling libxml. - -## glfw - -1. glfw depends on OpenGL, and linux environment also needs X11, which cannot be linked statically. -2. macOS platform, we can compile and link system builtin OpenGL and related libraries dynamically. - -## rar - -1. The rar extension currently has a problem when compiling phpmicro with the `common` extension collection in the macOS x86_64 environment. - -## pgsql - -~~pgsql ssl connection is not compatible with openssl 3.2.0. See:~~ - -- ~~~~ -- ~~~~ -- ~~~~ - -pgsql 16.2 has fixed this bug, now it's working. - -When pgsql uses SSL connection, there may be `error:80000002:system library::No such file or directory` error, -For details on the solution, see [FAQ - Unable to use ssl](../faq/#unable-to-use-ssl). - -## openssl - -When using openssl-based extensions (such as curl, pgsql and other network libraries), -there may be an `error:80000002:system library::No such file or directory` error. -For details on the solution, see [FAQ - Unable to use ssl](../faq/#unable-to-use-ssl). - -## password-argon2 - -1. password-argon2 is not a standard extension. The algorithm `PASSWORD_ARGON2ID` for the `password_hash` function needs libsodium or libargon2 to work. -2. using password-argon2 enables multithread support for this. - -## ffi - -1. Due to the limitation of musl libc's static linkage, you cannot use ffi because dynamic libraries cannot be loaded. - If you need to use the ffi extension, see [Compile PHP with GNU libc](./build-with-glibc). -2. macOS supports the ffi extension, but errors will occur when some kernels do not contain debugging symbols. -3. Windows x64 supports the ffi extension. - -## xhprof - -The xhprof extension consists of three parts: `xhprof_extension`, `xhprof_html`, `xhprof_libs`. -Only `xhprof_extension` is included in the compiled binary. -If you need to use xhprof, -please download the source code from [pecl.php.net/package/xhprof](http://pecl.php.net/package/xhprof) and specify the `xhprof_libs` and `xhprof_html` paths for use. - -## event - -If you enable event extension on macOS, the `openpty` will be disabled due to issue: - -- [static-php-cli#335](https://github.com/crazywhalecc/static-php-cli/issues/335) - -## parallel - -Parallel is only supported on PHP 8.0 ZTS and above. - -## spx - -1. SPX does not support Windows, and the official repository does not support static compilation. static-php-cli uses a [modified version](https://github.com/static-php/php-spx). - -## mimalloc - -1. This is not technically an extension, but a library. -2. Building with `--with-libs="mimalloc"` on Linux or macOS will override the default allocator. -3. This is experimental for now, but is recommended in threaded environments. + diff --git a/docs/en/guide/extensions.md b/docs/en/guide/extensions.md index 9ce53f63d..c170d331c 100644 --- a/docs/en/guide/extensions.md +++ b/docs/en/guide/extensions.md @@ -1,23 +1,3 @@ - +# Supported Extensions -# Extensions - -> - `yes`: supported -> - _blank_: not supported yet, or WIP -> - `no` with issue link: confirmed to be unavailable due to issue -> - `partial` with issue link: supported but not perfect due to issue - - - -::: tip -If an extension you need is missing, you can create a [Feature Request](https://github.com/crazywhalecc/static-php-cli/issues). - -Some extensions or libraries that the extension depends on will have some optional features. -For example, the gd library optionally supports libwebp, freetype, etc. -If you only use `bin/spc build gd --build-cli` they will not be included (static-php-cli defaults to the minimum dependency principle). - -For more information about optional libraries, see [Extensions, Library Dependency Map](./deps-map). -For optional libraries, you can also select an extension from the [Command Generator](./cli-generator) and then select optional libraries. -::: + diff --git a/docs/en/guide/first-build.md b/docs/en/guide/first-build.md new file mode 100644 index 000000000..6817021e2 --- /dev/null +++ b/docs/en/guide/first-build.md @@ -0,0 +1,190 @@ +# Your First Build + +This page walks you through building a static PHP binary from scratch, end to end. + +::: tip +If you installed spc as a pre-built binary, replace every `spc` in this page with `./spc` (or `.\spc.exe` on Windows). + +If you installed from source, use `bin/spc` instead. +::: + +## Two Approaches + +StaticPHP supports two build workflows — pick the one that fits your situation: + +| Approach | When to use | +|---|---| +| `craft` (one-shot) | Everyday use, getting started quickly | +| Step-by-step | CI/CD pipelines, when you need to separate download and build phases | + +## Option 1: One-Shot Build with `craft` (Recommended) + +The `craft` command reads a `craft.yml` file and handles everything automatically — downloading dependencies, compiling libraries, and building PHP — in a single run. + +### Write craft.yml + +Create a `craft.yml` in your working directory and declare the PHP version, extensions, and target SAPIs: + +```yaml +php-version: 8.4 +extensions: bcmath,posix,phar,zlib,openssl,curl,fileinfo,tokenizer +sapi: + - cli + - micro +``` + +Not sure which extensions you need? Use the [command generator](./cli-generator) to produce a `craft.yml` automatically. + +### Run the Build + +```bash +spc craft +``` + +The build pipeline runs in order: download dependencies → compile libraries → compile PHP. No interaction required. + +To see more detail, pass `-v`, `-vv`, or `-vvv`: + +```bash +spc craft -v +``` + +### Inspect the Output + +On success, binaries land in `buildroot/bin/`: + +| SAPI | Output path | +|---|---| +| cli | `buildroot/bin/php` (Windows: `buildroot/bin/php.exe`) | +| fpm | `buildroot/bin/php-fpm` | +| micro | `buildroot/bin/micro.sfx` | +| embed | `buildroot/lib/libphp.a` | +| frankenphp | `buildroot/bin/frankenphp` | + +Give the CLI binary a quick smoke-test: + +```bash +./buildroot/bin/php -v +./buildroot/bin/php -m +``` + +## Option 2: Step-by-Step Build + +This approach lets you run download and compile as separate steps — useful when you want to cache downloads in CI and reuse them across builds. + +### Step 1: Download Dependencies + +```bash +# Download only what the chosen extensions need (recommended) +spc download --for-extensions=bcmath,posix,phar,zlib,openssl,curl,fileinfo,tokenizer --with-php=8.4 + +# Download by specific libraries +spc download --for-libs=curl,openssl --with-php=8.4 +``` + +Downloads are cached in `downloads/` and reused across builds automatically. + +```bash +# Slow connection? Increase parallelism and retries +spc download --for-extensions=bcmath,openssl,curl -P 4 --retry=3 + +# Use pre-built binaries where available — skips compiling those dependencies +spc download --for-extensions=bcmath,openssl,curl --prefer-binary +``` + +### Step 2: Build PHP + +```bash +# Build the cli SAPI +spc build:php bcmath,posix,phar,zlib,openssl,curl,fileinfo,tokenizer --build-cli + +# Build multiple SAPIs in one go +spc build:php bcmath,posix,phar,zlib,openssl,curl --build-cli --build-micro + +# Build all SAPIs +spc build:php bcmath,posix,phar,zlib,openssl,curl --build-all +``` + +`build:php` will automatically fetch any missing dependencies before building. If you already ran `download`, pass `--no-download` to skip that step: + +```bash +spc build:php bcmath,openssl,curl --build-cli --no-download +``` + +#### Common Build Options + +| Option | Description | +|---|---| +| `--build-cli` | Build the cli SAPI | +| `--build-fpm` | Build php-fpm (not available on Windows) | +| `--build-micro` | Build micro.sfx | +| `--build-embed` | Build the embed SAPI (not available on Windows) | +| `--build-frankenphp` | Build FrankenPHP (not available on Windows) | +| `--build-all` | Build all SAPIs | +| `--enable-zts` | Enable thread-safe (ZTS) mode | +| `--no-strip` | Keep debug symbols; do not strip the binary | +| `-I key=value` | Hard-compile an INI option into PHP | +| `--with-upx-pack` | Compress output with UPX (run `spc install-pkg upx` first) | + +Example — baking in a larger memory limit and disabling the `system` function: + +```bash +spc build:php bcmath,pcntl,posix --build-all -I "memory_limit=4G" -I "disable_functions=system" +``` + +## Packaging a micro App + +Once you have `micro.sfx`, use `micro:combine` to bundle your PHP code into a single self-contained executable: + +```bash +echo " hello.php +spc micro:combine hello.php --output=hello +./hello +``` + +Works with `.phar` files too, and you can inject INI settings at packaging time: + +```bash +# Bundle a phar +spc micro:combine your-app.phar --output=your-app + +# Inject INI via command-line options +spc micro:combine your-app.phar --output=your-app -I "memory_limit=512M" + +# Inject INI from a file +spc micro:combine your-app.phar --output=your-app -N /path/to/custom.ini +``` + +## Debugging and Rebuilding + +If a build fails or you want to trace what's happening, use `-v` / `-vv` / `-vvv`: + +```bash +spc build:php bcmath,openssl --build-cli -vv +``` + +- `-v` shows `INFO`-level logs: which modules are running and what build commands are being executed. +- `-vv` shows `DEBUG`-level logs: all internal debug output from StaticPHP. +- `-vvv` shows `DEBUG`-level logs and also pipes the stdout of every shell command directly to your terminal. + +To wipe compiled artifacts and start fresh without re-downloading, run `reset`: + +```bash +spc reset +# Then rebuild +spc build:php bcmath,openssl --build-cli +``` + +::: tip +`reset` only removes `buildroot/` and `source/`. Your `downloads/` cache is preserved. +Add `--with-download` if you also want to clear the download cache. +::: + +If you're stuck, open an [Issue](https://github.com/static-php/static-php-cli/issues) and include your `craft.yml` (if any) and a zip of the `log/` directory. + +## What's Next + +- [CLI Reference](./cli-reference) — Full documentation for every command and option +- [Extensions](./extensions) — Browse supported extensions and their dependencies +- [Troubleshooting](./troubleshooting) — Diagnose common build failures + diff --git a/docs/en/guide/index.md b/docs/en/guide/index.md index 54e7840c5..99e5a2a80 100644 --- a/docs/en/guide/index.md +++ b/docs/en/guide/index.md @@ -1,50 +1,45 @@ # Guide -Static php cli is a tool used to build statically compiled PHP binaries, -currently supporting Linux and macOS systems. - -In the guide section, you will learn how to use static php cli to build standalone PHP programs. - -- [Build (local)](./manual-build) -- [Build (GitHub Actions)](./action-build) -- [Supported Extensions](./extensions) - -## Compilation Environment - -The following is the architecture support situation, where :gear: represents support for GitHub Action build, -:computer: represents support for local manual build, and empty represents temporarily not supported. - -| | x86_64 | aarch64 | -|---------|-------------------|-------------------| -| macOS | :gear: :computer: | :gear: :computer: | -| Linux | :gear: :computer: | :gear: :computer: | -| Windows | :gear: :computer: | | -| FreeBSD | :computer: | :computer: | - -Current supported PHP versions for compilation: - -> :warning: Partial support, there may be issues with new beta versions and old versions. -> -> :heavy_check_mark: Supported -> -> :x: Not supported - -| PHP Version | Status | Comment | -|-------------|--------------------|-------------------------------------------------------------------------------------------------------------------------| -| 7.2 | :x: | | -| 7.3 | :x: | phpmicro and many extensions do not support 7.3, 7.4 versions | -| 7.4 | :x: | phpmicro and many extensions do not support 7.3, 7.4 versions | -| 8.0 | :warning: | PHP official has stopped maintaining 8.0, we no longer handle 8.0 related backport support | -| 8.1 | :warning: | PHP official only provides security updates for 8.1, we no longer handle 8.1 related backport support after 8.5 release | -| 8.2 | :heavy_check_mark: | | -| 8.3 | :heavy_check_mark: | | -| 8.4 | :heavy_check_mark: | | -| 8.5 (beta) | :warning: | PHP 8.5 is currently in beta stage | - -> This table shows the support status of static-php-cli for building corresponding versions, not the PHP official support status for that version. - -## PHP Support Versions - -Currently, static-php-cli supports PHP versions 8.2 ~ 8.5, and theoretically supports PHP 8.1 and earlier versions, just select the earlier version when downloading. -However, due to some extensions and special components that have stopped supporting earlier versions of PHP, static-php-cli will not explicitly support earlier versions. -We recommend that you compile the latest PHP version possible for a better experience. +## What is StaticPHP? + +StaticPHP is a build tool that compiles the PHP interpreter together with any extensions you need into a single self-contained binary. The target system doesn't need PHP or any runtime libraries installed — just copy the binary and run it. Builds target Linux, macOS, and Windows. + +## Why bother with a static PHP binary? + +A typical PHP installation is tightly coupled to the system: you install PHP, then extensions, then spend time dealing with version mismatches across distros. A static binary sidesteps all of that — what you get is a single executable that runs on any machine of the same architecture, no setup required. + +Common use cases: + +- **Distributing CLI tools** — Ship tools like Composer, PHPStan, or your own CLI as a single file. Users don't need PHP installed. +- **Leaner containers** — Replace a bloated `php:8.x` base image with a minimal image (or even `FROM scratch`) carrying just a static binary. +- **Server applications** — Build a static binary with FPM or FrankenPHP baked in. Deployment becomes a file copy, with no dependency on the host environment. + +## phpmicro: ship PHP and your code as one file + +[phpmicro](https://github.com/easysoft/phpmicro) is a third-party PHP SAPI that StaticPHP supports out of the box. It merges the PHP interpreter with your `.php` source or `.phar` archive into a single self-extracting executable (`.sfx`). + +``` +micro.sfx + your-app.phar = your-app # one file, zero dependencies +``` + +This is ideal for distributing PHP-based CLI tools: the end user just gets an ordinary executable with no idea PHP is involved. + +## Improving how you ship and deploy PHP projects + +**Drop the heavy Docker base image** + +The official `php:8.x` image can be hundreds of megabytes, most of which is just the PHP runtime. Swap it for a static PHP binary with a minimal base image — or `FROM scratch` — and you can get container sizes down to single-digit megabytes with noticeably faster startup times. + +**Ship PHP CLI tools like native binaries** + +Build your CLI with [symfony/console](https://symfony.com/doc/current/components/console.html) or [Laravel Zero](https://laravel-zero.com), bundle it into a `.phar` with [Box](https://github.com/box-project/box), then merge it with phpmicro. The result is a single distributable executable — the same experience users expect from Go or Rust tools, with no PHP runtime required on their end. + +**Single-file web apps with FrankenPHP** + +[FrankenPHP](https://frankenphp.dev) is a modern PHP app server with built-in HTTP/2, HTTP/3, and automatic HTTPS. StaticPHP can compile FrankenPHP together with your chosen extensions into one binary. The result is a complete web server in a single file — no Nginx, no PHP-FPM, just deploy and run. + +## Next steps + +- [Installation](./installation) — Get the StaticPHP build tool +- [First Build](./first-build) — Full walkthrough: from downloading sources to a working executable +- [CLI Reference](./cli-reference) — Every command and option, in one place diff --git a/docs/en/guide/installation.md b/docs/en/guide/installation.md new file mode 100644 index 000000000..f2df13e78 --- /dev/null +++ b/docs/en/guide/installation.md @@ -0,0 +1,121 @@ +# Installation + +## Requirements + +| Platform | Architecture | Notes | +|---|---|---| +| Linux | x86_64, aarch64 | Major distros supported (Alpine, Debian/Ubuntu, RHEL/CentOS, etc.) | +| macOS | x86_64 (Intel), arm64 (Apple Silicon) | macOS 12 or later | +| Windows | x86_64 | Windows 10 Build 17063 or later | + +::: tip +Both glibc-based distros (Debian, Ubuntu, Arch, etc.) and musl-based ones (Alpine) are supported on Linux. +The `doctor` command will detect your environment and guide you through installing the right toolchain if needed. +::: + +Pick the installation method that fits your use case: + +| Method | Best for | +|---|---| +| Pre-built binary | Most users — download and run, no dependencies | +| From source | Contributors, or anyone who needs to modify core build logic | +| Vendor mode | Integrating StaticPHP into an existing PHP project | + +## Pre-built binary + +`spc` has no runtime dependencies — download the binary for your platform and it's ready to go. + +> Fun fact: `spc` itself is a static PHP binary built with StaticPHP. We use StaticPHP to build StaticPHP's own build tool. + +```shell +# Linux x86_64 +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-x86_64 -o spc +# Linux arm64 +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-aarch64 -o spc +# macOS x86_64 (Intel) +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-x86_64 -o spc +# macOS arm64 (Apple Silicon) +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-aarch64 -o spc +# Windows x86_64 (PowerShell) +curl.exe -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-windows-x86_64.exe -o spc.exe +``` + +On Linux and macOS, mark the binary as executable before running it: + +```bash +chmod +x spc && ./spc --version +``` + +## From source + +This is the right path if you want to contribute to StaticPHP, or need to modify the core registry and build scripts. You'll need PHP >= 8.4, Composer, and the `mbstring,posix,pcntl,iconv,phar,zlib` extensions. + +```bash +git clone https://github.com/crazywhalecc/static-php-cli.git --branch v3 +cd static-php-cli +composer install +``` + +If you don't have PHP or Composer installed, use the bundled setup script to install a self-contained runtime: + +::: code-group +```bash [Linux / macOS] +bin/setup-runtime +``` +```powershell [Windows] +.\bin\setup-runtime.ps1 +.\bin\setup-runtime.ps1 add-path # add runtime/ to PATH +``` +::: + +The script downloads `php` and `composer` into a `runtime/` subdirectory. You then have two options: + +1. **Call them directly** (no PATH changes needed): + ```bash + runtime/php bin/spc --help + runtime/php runtime/composer install + ``` + +2. **Add `runtime/` to your PATH** so you can use `php`, `composer`, and `bin/spc` without prefixes: + ```bash + export PATH="/path/to/static-php-cli/runtime:$PATH" + # Add this to ~/.bashrc or ~/.zshrc to make it permanent + ``` + +::: tip +In regions with restricted access to GitHub or getcomposer.org, pass `--mirror china` to use a mirror: +```bash +bin/setup-runtime --mirror china +``` +::: + +## Vendor mode + +If you already have a PHP project and want to call StaticPHP's build APIs directly, or use a custom registry to support private libraries and extensions, pull it in as a Composer dependency: + +```bash +composer require crazywhalecc/static-php-cli +``` + +See the [Vendor Mode guide](../develop/vendor-mode/) for details. + +## Verify your build environment + +> **Vendor mode users can skip this step.** + +Once installed, run `doctor` to check that your system has the required build tools (cmake, make, a C compiler, etc.): + +```bash +# Using the spc binary +./spc doctor +# From source +bin/spc doctor +``` + +If anything is missing, `--auto-fix` will attempt to install it for you: + +```bash +./spc doctor --auto-fix +``` + +Once `doctor` reports everything is good, head over to [First Build](./first-build). diff --git a/docs/en/guide/troubleshooting.md b/docs/en/guide/troubleshooting.md index 7fc79e35d..7a946efe8 100644 --- a/docs/en/guide/troubleshooting.md +++ b/docs/en/guide/troubleshooting.md @@ -1,42 +1,5 @@ # Troubleshooting -Various failures may be encountered in the process of using static-php-cli, -here will describe how to check the errors by yourself and report Issue. - -## Download Failure - -Problems with downloading resources are one of the most common problems with spc. -The main reason is that the addresses used for SPC download resources are generally the official website of the corresponding project or GitHub, etc., -and these websites may occasionally go down and block IP addresses. -After encountering a download failure, -you can try to call the download command multiple times. - -When downloading extensions, you may eventually see errors like `curl: (56) The requested URL returned error: 403` which are often caused by github rate limiting. -You can verify this by adding `--debug` to the command and will see something like `[DEBU] Running command (no output) : curl -sfSL "https://api.github.com/repos/openssl/openssl/releases"`. - -To fix this, [create](https://github.com/settings/tokens) a personal access token on GitHub and set it as an environment variable `GITHUB_TOKEN=`. - -If you confirm that the address is indeed inaccessible, -you can submit an Issue or PR to update the url or download type. - -## Doctor Can't Fix Something - -In most cases, the doctor module can automatically repair and install missing system environments, -but there are also special circumstances where the automatic repair function cannot be used normally. - -Due to system limitations (for example, software such as Visual Studio cannot be automatically installed under Windows), -the automatic repair function cannot be used for some projects. -When encountering a function that cannot be automatically repaired, -if you encounter the words `Some check items can not be fixed`, -it means that it cannot be automatically repaired. -Please submit an issue according to the method displayed on the terminal or repair the environment yourself. - -## Compile Error - -When you encounter a compilation error, if the `--debug` log is not enabled, please enable the debug log first, -and then determine the command that reported the error. -The error terminal output is very important for fixing compilation errors. -When submitting an issue, please upload the last error fragment of the terminal log (or the entire terminal log output), -and include the `spc` command and parameters used. - -If you are rebuilding, please refer to the [Local Build - Multiple Builds](./manual-build#multiple-builds) section. + diff --git a/docs/zh/contributing/index.md b/docs/zh/contributing/index.md index 88461ac2b..385897116 100644 --- a/docs/zh/contributing/index.md +++ b/docs/zh/contributing/index.md @@ -1,51 +1,5 @@ # 贡献指南 -感谢你能够看到这里,本项目非常欢迎你的贡献! - -## 贡献方法 - -如果你有代码或文档要贡献,以下是你需要首先了解的内容。 - -1. 你要贡献什么类型的代码?(新扩展、修复 Bug、安全问题、项目框架优化、文档) -2. 如果你贡献了新文件或新片段,你的代码是否经过 `php-cs-fixer` 和 `phpstan` 的检查? -3. 在贡献代码前是否充分阅读了 [开发指南](../develop/)? - -如果你能回答上述问题并对代码进行了修改,可以及时在项目 GitHub 仓库发起 Pull Request。 -代码审查完成后,可以根据建议修改代码,或直接合并到主分支。 - -## 贡献类型 - -本项目的主要目的是编译静态链接的 PHP 二进制文件,命令行处理功能基于 `symfony/console` 编写。 -在开发之前,如果你对它不够熟悉,请先查看 [symfony/console 文档](https://symfony.com/doc/current/components/console.html)。 - -### 安全更新 - -因为本项目基本上是一个本地运行的 PHP 项目,一般来说不会有远程攻击。 -但如果你发现此类问题,请**不要**在 GitHub 仓库提交 PR 或 Issue, -你需要通过 [邮件](mailto:admin@zhamao.me) 联系项目维护者(crazywhalecc)。 - -### 修复 Bug - -修复 Bug 一般不涉及项目结构和框架的修改,所以如果你能定位到错误代码并直接修复它,请直接提交 PR。 - -### 新扩展 - -对于添加新扩展,你需要了解项目的一些基本结构以及如何根据现有逻辑添加新扩展。 -这将在本页的下一节中详细介绍。 -总的来说,你需要: - -1. 评估扩展是否可以内联编译到 PHP 中。 -2. 评估扩展的依赖库(如果有)是否可以静态编译。 -3. 编写不同平台的库编译命令。 -4. 验证扩展及其依赖项与现有扩展和依赖项兼容。 -5. 验证扩展在 `cli`、`micro`、`fpm`、`embed` SAPIs 中正常工作。 -6. 编写文档并添加你的扩展。 - -### 项目框架优化 - -如果你已经熟悉 `symfony/console` 的工作原理,并同时要对项目的框架进行一些修改或优化,请先了解以下事情: - -1. 添加扩展不属于项目框架优化,但如果你在添加新扩展时发现必须优化框架,则需要先修改框架本身,然后再添加扩展。 -2. 对于一些大规模逻辑修改(例如涉及 LibraryBase、Extension 对象等的修改),建议先提交 Issue 或 Draft PR 进行讨论。 -3. 在项目早期,它是一个纯私有开发项目,代码中有一些中文注释。项目国际化后,你可以提交 PR 将这些注释翻译为英语。 -4. 请不要在代码中提交更多无用的代码片段,例如大量未使用的变量、方法、类以及多次重写的代码。 + diff --git a/docs/zh/develop/build-lifecycle.md b/docs/zh/develop/build-lifecycle.md new file mode 100644 index 000000000..789c886e6 --- /dev/null +++ b/docs/zh/develop/build-lifecycle.md @@ -0,0 +1,5 @@ +# 构建生命周期 + + diff --git a/docs/zh/develop/craft-yml.md b/docs/zh/develop/craft-yml.md index b96842f91..a1cc0b58b 100644 --- a/docs/zh/develop/craft-yml.md +++ b/docs/zh/develop/craft-yml.md @@ -2,6 +2,6 @@ aside: false --- -# craft.yml 配置 +# craft.yml 配置详解 - + diff --git a/docs/zh/develop/doctor-module.md b/docs/zh/develop/doctor-module.md index 88931227e..1290f8753 100644 --- a/docs/zh/develop/doctor-module.md +++ b/docs/zh/develop/doctor-module.md @@ -1,60 +1,4 @@ -# Doctor 模块 +# Doctor 环境检查 -Doctor 模块是一个较为独立的用于检查系统环境的模块,可使用命令 `bin/spc doctor` 进入,入口的命令类在 `DoctorCommand.php` 中。 - -Doctor 模块是一个检查单,里面有一系列的检查项目和自动修复项目。这些项目都存放在 `src/SPC/doctor/item/` 目录中, -并且使用了两种 Attribute 用作检查项标记和自动修复项目标记:`#[AsCheckItem]` 和 `#[AsFixItem]`。 - -以现有的检查项 `if necessary tools are installed`,它是用于检查编译必需的包是否安装在 macOS 系统内,下面是它的源码: - -```php -use SPC\doctor\AsCheckItem; -use SPC\doctor\AsFixItem; -use SPC\doctor\CheckResult; - -#[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin', level: 997)] -public function checkCliTools(): ?CheckResult -{ - $missing = []; - foreach (self::REQUIRED_COMMANDS as $cmd) { - if ($this->findCommand($cmd) === null) { - $missing[] = $cmd; - } - } - if (!empty($missing)) { - return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]); - } - return CheckResult::ok(); -} -``` - -属性的第一个参数就是检查项目的名称,后面的 `limit_os` 参数是限制了该检查项仅在指定的系统下触发,`level` 是执行该检查项的优先级,数字越大,优先级越高。 - -里面用到的 `$this->findCommand()` 方法为 `SPC\builder\traits\UnixSystemUtilTrait` 的方法,用途是查找系统命令所在位置,找不到时返回 NULL。 - -每个检查项的方法都应该返回一个 `SPC\doctor\CheckResult`: - -- 在返回 `CheckResult::fail()` 时,第一个参数用于输出终端的错误提示,第二个参数是在这个检查项可自动修复时的修复项目名称。 -- 在返回 `CheckResult::ok()` 时,表明检查通过。你也可以传递一个参数,用于返回检查结果,例如:`CheckResult::ok('OS supported')`。 -- 在返回 `CheckResult::fail()` 时,如果包含了第三个参数,第三个参数的数组将被当作 `AsFixItem` 的参数。 - -下面是这个检查项对应的自动修复项的方法: - -```php -#[AsFixItem('build-tools')] -public function fixBuildTools(array $missing): bool -{ - foreach ($missing as $cmd) { - try { - shell(true)->exec('brew install ' . escapeshellarg($cmd)); - } catch (RuntimeException) { - return false; - } - } - return true; -} -``` - -`#[AsFixItem()]` 属性传入的参数即修复项的名称,该方法必须返回 True 或 False。当返回 False 时,表明自动修复失败,需要手动处理。 - -此处的代码中 `shell()->exec()` 是项目的执行命令的方法,用于替代 `exec()`、`system()`,同时提供了 debug、获取执行状态、进入目录等特性。 + diff --git a/docs/zh/develop/index.md b/docs/zh/develop/index.md index 85c9ad5fe..eb2f68cdd 100644 --- a/docs/zh/develop/index.md +++ b/docs/zh/develop/index.md @@ -1,27 +1,4 @@ # 开发简介 -开发本项目需要安装部署 PHP 环境,以及一些 PHP 项目常用的扩展和 Composer。 - -项目的开发环境和运行环境几乎完全一致。你可以参照 **手动构建** 部分安装系统 PHP 或使用本项目预构建的静态 PHP 作为环境。这里不再赘述。 - -抛开用途,本项目本身其实就是一个 `php-cli` 程序,你可以将它当作一个正常的 PHP 项目进行编辑和开发,同时你需要了解不同系统的 Shell 命令行。 - -本项目目前的目的就是为了编译静态编译的独立 PHP,但主体部分也包含编译很多依赖库的静态版本,所以你可以复用这套编译逻辑,用于构建其他程序的独立二进制版本,例如 Nginx 等。 - -## 环境准备 - -开发本项目需要 PHP 环境。你可以使用系统自带的 PHP,也可以使用本项目构建的静态 PHP。 - -无论是使用哪种 PHP,在开发环境,你需要安装这些扩展: - -``` -curl,dom,filter,mbstring,openssl,pcntl,phar,posix,sodium,tokenizer,xml,xmlwriter -``` - -static-php-cli 项目本身不需要这么多扩展,但在开发过程中,你会用到 Composer 和 PHPUnit 等工具,它们需要这些扩展。 - -> 对于 static-php-cli 自身构建的 micro 自执行二进制,仅需要 `pcntl,posix,mbstring,tokenizer,phar`。 - -## 开始开发 - -继续向下查看项目结构文档,你可以学习 `static-php-cli` 是如何工作的。 + diff --git a/docs/zh/develop/package-model.md b/docs/zh/develop/package-model.md new file mode 100644 index 000000000..3960139e6 --- /dev/null +++ b/docs/zh/develop/package-model.md @@ -0,0 +1,5 @@ +# Package 模型 + + diff --git a/docs/zh/develop/php-src-changes.md b/docs/zh/develop/php-src-changes.md index 5fabd3018..e15cf1eca 100644 --- a/docs/zh/develop/php-src-changes.md +++ b/docs/zh/develop/php-src-changes.md @@ -1,51 +1,3 @@ # 对 PHP 源码的修改 -由于 static-php-cli 在静态编译过程中为了实现良好的兼容性、性能和安全性,对 PHP 源码进行了一些修改。下面是目前对 PHP 源码修改的说明。 - -## micro 相关补丁 - -基于 phpmicro 项目提供的补丁,static-php-cli 对 PHP 源码进行了一些修改,以适应静态编译的需求。[补丁列表](https://github.com/easysoft/phpmicro/tree/master/patches) 包含: - -目前 static-php-cli 在编译时用到的补丁有: - -- static_opcache -- static_extensions_win32 -- cli_checks -- disable_huge_page -- vcruntime140 -- win32 -- zend_stream -- cli_static -- macos_iconv -- phar - -## PHP <= 8.1 libxml 补丁 - -因为 PHP 官方仅对 8.1 进行安全更新,旧版本停止更新,所以 static-php-cli 对 PHP 8.1 及以下版本应用了在新版本 PHP 中已经应用的 libxml 编译补丁。 - -## gd 扩展 Windows 补丁 - -在 Windows 下编译 gd 扩展需要大幅改动 `config.w32` 文件,static-php-cli 对 gd 扩展进行了一些修改,使其在 Windows 下编译更加方便。 - -## yaml 扩展 Windows 补丁 - -yaml 扩展在 Windows 下编译需要修改 `config.w32` 文件,static-php-cli 对 yaml 扩展进行了一些修改,使其在 Windows 下编译更加方便。 - -## static-php-cli 版本信息插入 - -static-php-cli 在编译时会在 PHP 版本信息中插入 static-php-cli 的版本信息,以便于识别。 - -## 加入硬编码 INI 的选项 - -在使用 `-I` 参数硬编码 INI 到静态 PHP 的功能中,static-php-cli 会修改 PHP 源码以插入硬编码内容。 - -## Linux 系统修复补丁 - -部分编译环境可能缺少一些头文件或库,static-php-cli 会在编译时自动修复这些问题,如: - -- HAVE_STRLCAT missing problem -- HAVE_STRLCPY missing problem - -## Windows 系统下 Fiber 问题修复补丁 - -在 Windows 下编译 PHP 时,Fiber 扩展会出现一些问题,static-php-cli 会在编译时自动修复这些问题(修改 php-src 的 `config.w32`)。 + diff --git a/docs/zh/develop/registry.md b/docs/zh/develop/registry.md new file mode 100644 index 000000000..d617ebf0b --- /dev/null +++ b/docs/zh/develop/registry.md @@ -0,0 +1,5 @@ +# Registry 与插件系统 + + diff --git a/docs/zh/develop/source-module.md b/docs/zh/develop/source-module.md index 769ffa08b..c35555d86 100644 --- a/docs/zh/develop/source-module.md +++ b/docs/zh/develop/source-module.md @@ -1,350 +1,5 @@ # 资源模块 -static-php-cli 的下载资源模块是一个主要的功能,它包含了所依赖的库、外部扩展、PHP 源码的下载方式和资源解压方式。 -下载的配置文件主要涉及 `source.json` 和 `pkg.json` 文件,这个文件记录了所有可下载的资源的下载方式。 - -下载功能主要涉及的命令有 `bin/spc download` 和 `bin/spc extract`。其中 `download` 命令是一个下载器,它会根据配置文件下载资源; -`extract` 命令是一个解压器,它会根据配置文件解压资源。 - -一般来说,下载资源可能会比较慢,因为这些资源来源于各个官网、GitHub 等不同位置,同时它们也占用了较大空间,所以你可以在一次下载资源后,可重复使用。 - -下载器的配置文件是 `source.json`,它包含了所有资源的下载方式,你可以在其中添加你需要的资源下载方式,也可以修改已有的资源下载方式。 - -每个资源的下载配置结构如下,下面是 `libevent` 扩展对应的资源下载配置: - -```json -{ - "libevent": { - "type": "ghrel", - "repo": "libevent/libevent", - "match": "libevent.+\\.tar\\.gz", - "provide-pre-built": true, - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -这里最主要的字段是 `type`,目前它支持的类型有: - -- `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`。 -- `pie`: 使用 PIE(PHP Installer for Extensions)标准从 Packagist 下载 PHP 扩展。 -- `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。 -- `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 -- `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 -- `bitbuckettag`: 使用 BitBucket API 下载,基本和 `ghtagtar` 相同,只是这个适用于 BitBucket。 -- `git`: 直接从一个 Git 地址克隆项目来下载资源,适用于任何公开 Git 仓库。 -- `filelist`: 使用爬虫爬取提供文件索引的 Web 下载站点,并获取最新版本的文件名并下载。 -- `custom`: 如果以上下载方式都不能满足,你可以编写 `custom` 后,在 `src/SPC/store/source/` 下新建一个类,并继承 `CustomSourceBase`,自己编写下载脚本。 - -## source.json 通用参数 - -source.json 中每个源文件拥有以下字段: - -- `license`: 源代码的开源许可证,见下方 **开源许可证** 章节 -- `type`: 必须为上面提到的类型之一 -- `path`(可选): 释放源码到指定目录而非 `source/{name}` -- `provide-pre-built`(可选): 是否提供预编译的二进制文件,如果为 `true`,则会在 `bin/spc download` 时尝试自动下载预编译的二进制文件 - -::: tip -`source.json` 中的 `path` 参数可指定相对路径或绝对路径。当指定为相对路径时,路径基于 `source/`。 -::: - -## 下载类型 - url - -url 类型的资源指的是从 URL 直接下载文件。 - -包含的参数有: - -- `url`: 文件的下载地址,如 `https://example.com/file.tgz` -- `filename`(可选): 保存到本地的文件名,如不指定,则使用 url 的文件名 - -例子(下载 imagick 扩展,并解压缩到 php 源码的扩展存放路径): - -```json -{ - "ext-imagick": { - "type": "url", - "url": "https://pecl.php.net/get/imagick", - "path": "php-src/ext/imagick", - "filename": "imagick.tgz", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## 下载类型 - pie - -PIE(PHP Installer for Extensions)类型的资源是从 Packagist 下载遵循 PIE 标准的 PHP 扩展。 -该方法会自动从 Packagist 仓库获取扩展信息,并下载相应的分发文件。 - -包含的参数有: - -- `repo`: Packagist 的 vendor/package 名称,如 `vendor/package-name` - -例子(使用 PIE 从 Packagist 下载 PHP 扩展): - -```json -{ - "ext-example": { - "type": "pie", - "repo": "vendor/example-extension", - "path": "php-src/ext/example", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -::: tip -PIE 下载类型会自动从 Packagist 元数据中检测扩展信息,包括下载 URL、版本和分发类型。 -扩展必须在其 Packagist 包定义中标记为 `type: php-ext` 或包含 `php-ext` 元数据。 -::: - -## 下载类型 - ghrel - -ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。 - -包含的参数有: - -- `repo`: GitHub 仓库名称 -- `match`: 匹配 Assets 文件的正则表达式 -- `prefer-stable`: 是否优先下载稳定版本(默认为 `false`) - -例子(下载 libsodium 库,匹配 Release 中的 libsodium-x.y.tar.gz 文件): - -```json -{ - "libsodium": { - "type": "ghrel", - "repo": "jedisct1/libsodium", - "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## 下载类型 - ghtar - -ghtar 会从 GitHub Release Tag 下载文件,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 - -包含的参数有: - -- `repo`: GitHub 仓库名称 -- `prefer-stable`: 是否优先下载稳定版本(默认为 `false`) - -例子(brotli 库): - -```json -{ - "brotli": { - "type": "ghtar", - "repo": "google/brotli", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## 下载类型 - ghtagtar - -使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 - -包含的参数有: - -- `repo`: GitHub 仓库名称 -- `prefer-stable`: 是否优先下载稳定版本(默认为 `false`) - -例子(gmp 库): - -```json -{ - "gmp": { - "type": "ghtagtar", - "repo": "alisw/GMP", - "license": { - "type": "text", - "text": "EXAMPLE LICENSE" - } - } -} -``` - -## 下载类型 - bitbuckettag - -使用 BitBucket API 下载,基本和 `ghtagtar` 相同,只是这个适用于 BitBucket。 - -包含的参数有: - -- `repo`: BitBucket 仓库名称 - -## 下载类型 - git - -直接从一个 Git 地址克隆项目来下载资源,适用于任何公开 Git 仓库。 - -包含的参数有: - -- `url`: Git 链接(仅限 HTTPS) -- `rev`: 分支名称 - -```json -{ - "imap": { - "type": "git", - "url": "https://github.com/static-php/imap.git", - "rev": "master", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -## 下载类型 - filelist - -使用爬虫爬取提供文件索引的 Web 下载站点,并获取最新版本的文件名并下载。 - -注意,该方法仅限于镜像站、GNU 官网等具有页面 index 功能的静态站点使用。 - -包含的参数有: - -- `url`: 要爬取文件最新版本的页面 URL -- `regex`: 匹配文件名及下载链接的正则表达式 - -例子(从 GNU 官网下载 libiconv 库): - -```json -{ - "libiconv": { - "type": "filelist", - "url": "https://ftp.gnu.org/gnu/libiconv/", - "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/", - "license": { - "type": "file", - "path": "COPYING" - } - } -} -``` - -## 下载类型 - custom - -如果以上下载方式都不能满足,你可以编写 `custom` 后,在 `src/SPC/store/source/` 下新建一个类,并继承 `CustomSourceBase`,自己编写下载脚本。 - -这里不再赘述,你可以查看 `src/SPC/store/source/PhpSource.php` 或 `src/SPC/store/source/PostgreSQLSource.php` 作为例子。 - -## pkg.json 通用参数 - -pkg.json 存放的是非源码类型的文件资源,例如 musl-toolchain、UPX 等预编译的工具。它的使用包含: - -- `type`: 与 `source.json` 相同的类型及不同种类的参数。 -- `extract`(可选): 下载后解压缩的路径,默认为 `pkgroot/{pkg_name}`。 -- `extract-files`(可选): 下载后仅解压指定的文件到指定位置。 - -需要注意的是,`pkg.json` 不涉及源代码的编译和修改分发,所以没有 `license` 开源许可证字段。并且你不能同时使用 `extract` 和 `extract-files` 参数。 - -例子(下载 nasm 到本地,并只提取程序文件到 PHP SDK): - -```json -{ - "nasm-x86_64-win": { - "type": "url", - "url": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip", - "extract-files": { - "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" - } - } -} -``` - -`extract-files` 中的键名为源文件夹下的文件,键值为存放的路径。存放的路径可以使用以下变量: - -- `{php_sdk_path}`: (仅限 Windows)PHP SDK 路径 -- `{pkg_root_path}`: `pkgroot/` -- `{working_dir}`: 当前工作目录 -- `{download_path}`: 下载目录 -- `{source_path}`: 源码解压缩目录 - -当 `extract-files` 不使用变量且为相对路径时,相对路径的目录为 `{working_dir}`。 - -## 开源许可证 - -对于 `source.json` 而言,每个源文件都应包含开源许可证。`license` 字段存放了开源许可证的信息。 - -每个 `license` 包含的参数有: - -- `type`: `file` 或 `text` -- `path`: 源代码目录中的许可证文件(当 `type` 为 `file` 时,此项必填) -- `text`: 许可证文本(当 `type` 为 `text` 时,此项必填) - -例子(yaml 扩展的源代码中带有 LICENSE 文件): - -```json -{ - "yaml": { - "type": "git", - "path": "php-src/ext/yaml", - "rev": "php7", - "url": "https://github.com/php/pecl-file_formats-yaml", - "license": { - "type": "file", - "path": "LICENSE" - } - } -} -``` - -当开源项目拥有多个许可证时,可指定多个文件: - -```json -{ - "libuv": { - "type": "ghtar", - "repo": "libuv/libuv", - "license": [ - { - "type": "file", - "path": "LICENSE" - }, - { - "type": "file", - "path": "LICENSE-extra" - } - ] - } -} -``` - -当一个开源项目的许可证在不同版本间使用不同的文件,`path` 参数可以使用数组将可能的许可证文件列出: - -```json -{ - "redis": { - "type": "git", - "path": "php-src/ext/redis", - "rev": "release/6.0.2", - "url": "https://github.com/phpredis/phpredis", - "license": { - "type": "file", - "path": [ - "LICENSE", - "COPYING" - ] - } - } -} -``` + diff --git a/docs/zh/develop/structure.md b/docs/zh/develop/structure.md index 16c3a9e4c..dde6e68f4 100644 --- a/docs/zh/develop/structure.md +++ b/docs/zh/develop/structure.md @@ -1,163 +1,4 @@ -# 项目结构简介 +# 项目结构 -static-php-cli 主要包含三种逻辑组件:资源、依赖库、扩展。这三种组件四个配置文件:`source.json`、`lib.json`、`ext.json`、`pkg.json`。 - -一个完整的构建静态 PHP 流程是: - -1. 使用资源下载模块 `Downloader` 下载指定或所有资源,这些资源包含 PHP 源码、依赖库源码、扩展源码。 -2. 使用资源解压模块 `SourceExtractor` 解压下载的资源到编译目录。 -3. 使用依赖工具计算出当前加入的扩展的依赖扩展、依赖库,然后对每个需要编译的依赖库进行编译,按照依赖顺序。 -4. 使用对应操作系统下的 `Builder` 构建每个依赖库后,将其安装到 `buildroot` 目录。 -5. 如果包含外部扩展(源码没有包含在 PHP 内的扩展),将外部扩展拷贝到 `source/php-src/ext/` 目录。 -6. 使用 `Builder` 构建 PHP 源码,将其安装到 `buildroot` 目录。 - -项目主要分为几个文件夹: - -- `bin/`: 用于存放程序入口文件,包含 `bin/spc`、`bin/spc-alpine-docker`、`bin/setup-runtime`。 -- `config/`: 包含了所有项目支持的扩展、依赖库以及这些资源下载的地址、下载方式等,:`lib.json`、`ext.json`、`source.json`、`pkg.json`、`pre-built.json`。 -- `src/SPC/`: 项目的核心代码,包含了整个框架以及编译各种扩展和库的命令。 -- `src/globals/`: 项目的全局方法和常量、运行时需要的测试文件(例如:扩展的可用性检查代码)。 -- `vendor/`: Composer 依赖的目录,你无需对它做出任何修改。 - -其中运行原理就是启动一个 `symfony/console` 的 `ConsoleApplication`,然后解析用户在终端输入的命令。 - -## 基本命令行结构 - -`bin/spc` 是一个 PHP 代码入口文件,包含了 Unix 通用的 `#!/usr/bin/env php` 用来让系统自动以系统安装好的 PHP 解释器执行。 -在项目执行了 `new ConsoleApplication()` 后,框架会自动使用反射的方式,解析 `src/SPC/command` 目录下的所有类,并将其注册成为命令。 - -项目并没有直接使用 Symfony 推荐的 Command 注册方式和命令执行方式,这里做出了一点小变动: - -1. 每个命令都使用 `#[AsCommand()]` Attribute 来注册名称和简介。 -2. 将 `execute()` 抽象化,让所有命令基于 `BaseCommand`(它基于 `Symfony\Component\Console\Command\Command`),每个命令本身的执行代码写到了 `handle()` 方法中。 -3. `BaseCommand` 添加了变量 `$no_motd`,用于是否在该命令执行时显示 Figlet 欢迎词。 -4. `BaseCommand` 将 `InputInterface` 和 `OutputInterface` 保存为成员变量,你可以在命令的类内使用 `$this->input` 和 `$this->output`。 - -## 基本源码结构 - -项目的源码位于 `src/SPC` 目录,支持 PSR-4 标准的自动加载,包含以下子目录和类: - -- `src/SPC/builder/`: 用于不同操作系统下构建依赖库、PHP 及相关扩展的核心编译命令代码,还包含了一些编译的系统工具方法。 -- `src/SPC/command/`: 项目的所有命令都在这里。 -- `src/SPC/doctor/`: Doctor 模块,它是一个较为独立的用于检查系统环境的模块,可使用命令 `bin/spc doctor` 进入。 -- `src/SPC/exception/`: 异常类。 -- `src/SPC/store/`: 有关存储、文件和资源的类都在这里。 -- `src/SPC/util/`: 一些可以复用的工具方法都在这里。 -- `src/SPC/ConsoleApplication.php`: 命令行程序入口文件。 - -如果你阅读过源码,你可能会发现还有一个 `src/globals/` 目录,它是用于存放一些全局变量、全局方法、构建过程中依赖的非 PSR-4 标准的 PHP 源码,例如测试扩展代码等。 - -## Phar 应用目录问题 - -和其他 php-cli 项目一样,spc 自身对路径有额外的考虑。 -因为 spc 可以在 `php-cli directly`、`micro SAPI`、`php-cli with Phar`、`vendor with Phar` 等多种模式下运行,各类根目录存在歧义。这里会进行一个完整的说明。 -此问题一般常见于 PHP 项目中存取文件的基类路径选择问题,尤其是在配合 `micro.sfx` 使用时容易出现路径问题。 - -注意,此处仅对你在开发 Phar 项目或 PHP 框架时可能有用。 - -> 接下来我们都将 `static-php-cli`(也就是 spc)当作一个普通的 `php` 命令行程序来看,你可以将 spc 理解为你自己的任何 php-cli 应用以参考。 - -下面主要有三个基本的常量理论值,我们建议你在编写 php 项目时引入这三种常量: - -- `WORKING_DIR`:执行 PHP 脚本时的工作目录 -- `SOURCE_ROOT_DIR` 或 `ROOT_DIR`:项目文件夹的根目录,一般为 `composer.json` 所在目录 -- `FRAMEWORK_ROOT_DIR`:使用框架的根目录,自行开发的框架可能会用到,一般框架目录为只读 - -你可以在你的框架或者 cli 应用程序入口中定义这些常量,以方便在你的项目中使用路径。 - -下面是 PHP 内置的常量值,在 PHP 解释器内部已被定义: - -- `__DIR__`:当前执行脚本的文件所在目录 -- `__FILE__`:当前执行脚本的文件路径 - -### Git 项目模式(source) - -Git 项目模式指的是一个框架或程序本身在当前文件夹以纯文本形式存放,运行通过 `php path/to/entry.php` 方式。 - -假设你的项目存放在 `/home/example/static-php-cli/` 目录下,或你的项目就是框架本身,里面包含 `composer.json` 等项目文件: - -``` -composer.json -src/App/MyCommand.app -vendor/* -bin/entry.php -``` - -我们假设从 `src/App/MyCommand.php` 中获取以上常量: - -| Constant | Value | -|----------------------|------------------------------------------------------| -| `WORKING_DIR` | `/home/example/static-php-cli` | -| `SOURCE_ROOT_DIR` | `/home/example/static-php-cli` | -| `FRAMEWORK_ROOT_DIR` | `/home/example/static-php-cli` | -| `__DIR__` | `/home/example/static-php-cli/src/App` | -| `__FILE__` | `/home/example/static-php-cli/src/App/MyCommand.php` | - -这种情况下,`WORKING_DIR`、`SOURCE_ROOT_DIR`、`FRAMEWORK_ROOT_DIR` 的值是完全一致的:`/home/example/static-php-cli`。 -框架的源码和应用的源码都在当前路径下。 - -### Vendor 库模式(vendor) - -Vendor 库模式一般是指你的项目为框架类或者被其他应用作为 composer 依赖项安装到项目中,存放位置在 `vendor/author/XXX` 目录。 - -假设你的项目是 `crazywhalecc/static-php-cli`,你或其他人在另一个项目使用 `composer require` 安装了这个项目。 - -我们假设 static-php-cli 中包含同 `Git 模式` 的除 `vendor` 目录外的所有文件,并从 `src/App/MyCommand` 中获取常量值, -目录常量应该是: - -| Constant | Value | -|----------------------|--------------------------------------------------------------------------------------| -| `WORKING_DIR` | `/home/example/another-app` | -| `SOURCE_ROOT_DIR` | `/home/example/another-app` | -| `FRAMEWORK_ROOT_DIR` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli` | -| `__DIR__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App` | -| `__FILE__` | `/home/example/another-app/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | - - -这里的 `SOURCE_ROOT_DIR` 就指的是使用 `static-php-cli` 的项目的根目录。 - -### Git 项目 Phar 模式(source-phar) - -Git 项目 Phar 模式指的是将 Git 项目模式的项目目录打包为一个 `phar` 文件的模式。我们假设 `/home/example/static-php-cli` 将打包为一个 Phar 文件,目录有以下文件: - -``` -composer.json -src/App/MyCommand.app -vendor/* -bin/entry.php -``` - -打包为 `app.phar` 并存放到 `/home/example/static-php-cli` 目录下时,此时执行 `app.phar`,假设执行了 `src/App/MyCommand` 代码,常量在该文件内获取: - -| Constant | Value | -|----------------------|----------------------------------------------------------------------| -| `WORKING_DIR` | `/home/example/static-php-cli` | -| `SOURCE_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | -| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/static-php-cli/app.phar/` | -| `__DIR__` | `phar:///home/example/static-php-cli/app.phar/src/App` | -| `__FILE__` | `phar:///home/example/static-php-cli/app.phar/src/App/MyCommand.php` | - -因为在 phar 内读取自身 phar 的文件需要 `phar://` 协议进行,所以项目根目录和框架目录将会和 `WORKING_DIR` 不同。 - -### Vendor 库 Phar 模式(vendor-phar) - -Vendor 库 Phar 模式指的是你的项目作为框架安装在其他项目内,存储于 `vendor` 目录下。 - -我们假设你的项目目录结构如下: - -``` -composer.json # 当前项目的 Composer 配置文件 -box.json # 打包 Phar 的配置文件 -another-app.php # 另一个项目的入口文件 -vendor/crazywhalecc/static-php-cli/* # 你的项目被作为依赖库 -``` - -将该目录 `/home/example/another-app/` 下的这些文件打包为 `app.phar` 时,对于你的项目而言,下面常量的值应为: - -| Constant | Value | -|----------------------|------------------------------------------------------------------------------------------------------| -| `WORKING_DIR` | `/home/example/another-app` | -| `SOURCE_ROOT_DIR` | `phar:///home/example/another-app/app.phar/` | -| `FRAMEWORK_ROOT_DIR` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli` | -| `__DIR__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App` | -| `__FILE__` | `phar:///home/example/another-app/app.phar/vendor/crazywhalecc/static-php-cli/src/App/MyCommand.php` | + diff --git a/docs/zh/develop/system-build-tools.md b/docs/zh/develop/system-build-tools.md index a96d4dace..7b77f1b90 100644 --- a/docs/zh/develop/system-build-tools.md +++ b/docs/zh/develop/system-build-tools.md @@ -1,204 +1,4 @@ -# 系统编译工具 +# 编译工具 -static-php-cli 在构建静态 PHP 时使用了许多系统编译工具,这些工具主要包括: - -- `autoconf`: 用于生成 `configure` 脚本。 -- `make`: 用于执行 `Makefile`。 -- `cmake`: 用于执行 `CMakeLists.txt`。 -- `pkg-config`: 用于查找依赖库的安装路径。 -- `gcc`: 用于在 Linux 下编译 C/C++ 语言代码。 -- `clang`: 用于在 macOS 下编译 C/C++ 语言代码。 - -对于 Linux 和 macOS 操作系统,这些工具通常可以通过包管理安装,这部分在 doctor 模块中编写了。 -理论上我们也可以通过编译和手动下载这些工具,但这样会增加编译的复杂度,所以我们不推荐这样做。 - -## Linux 环境编译工具 - -对于 Linux 系统来说,不同发行版的编译工具安装方式不同。而且对于静态编译来说,某些发行版的包管理无法安装用于纯静态编译的库和工具, -所以对于 Linux 平台及其不同发行版,我们目前提供了多种编译环境的部署措施。 - -### glibc 环境 - -glibc 环境指的是系统底层的 `libc` 库(即所有 C 语言编写的程序动态链接的 C 标准库)使用的是 `glibc`,这是大多数发行版的默认环境。 -例如:Ubuntu、Debian、CentOS、RHEL、openSUSE、Arch Linux 等。 - -而 glibc 环境下,我们使用的包管理、编译器都是默认指向 glibc 的,glibc 不能被良好地静态链接。它不能被静态链接的原因之一是它的网络库 `nss` 无法静态编译。 - -对于 glibc 环境,在 2.0 RC8 及以后的 static-php-cli 及 spc 中,你可以选择两种方式来构建静态 PHP: - -1. 使用 Docker 构建,这是最简单的方式,你可以使用 `bin/spc-alpine-docker` 来构建,它会在 Alpine Linux 环境下构建。 -2. 使用 `bin/spc doctor` 安装 musl-wrapper 和 musl-cross-make 套件,然后直接正常构建。([相关源码](https://github.com/crazywhalecc/static-php-cli/blob/main/src/SPC/doctor/item/LinuxMuslCheck.php)) - -一般来说,这两种构建方式的构建结果是一致的,你可以根据实际需求选择。 - -在 doctor 模块中,static-php-cli 会先检测当前的 Linux 发行版。如果当前发行版是 glibc 环境,会提示需要安装 musl-wrapper 和 musl-cross-make 套件。 - -在 glibc 环境下安装 musl-wrapper 的过程如下: - -1. 从 musl 官网下载特定版本的 [musl-wrapper 源码](https://musl.libc.org/releases/)。 -2. 使用从包管理安装的 `gcc` 编译 musl-wrapper 源码,生成 `musl-libc` 等库:`./configure --disable-gcc-wrapper && make -j && sudo make install`。 -3. musl-wrapper 相关库将被安装在 `/usr/local/musl` 目录。 - -在 glibc 环境下安装 musl-cross-make 的过程如下: - -1. 从 dl.static-php.dev 下载预编译好的 [musl-cross-make](https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/) 压缩包。 -2. 解压到 `/usr/local/musl` 目录。 - -::: tip -在 glibc 环境下,静态编译可以通过直接安装 musl-wrapper 来实现,但是 musl-wrapper 仅包含了 `musl-gcc`,而没有 `musl-g++`,这也就意味着无法编译 C++ 代码。 -所以我们需要 musl-cross-make 来提供 `musl-g++`。 - -而 musl-cross-make 套件无法在本地直接编译的原因是它的编译环境要求比较高(需要 36GB 以上内存,Alpine Linux 下编译),所以我们提供了预编译好的二进制包,可用于所有 Linux 发行版。 - -同时,部分发行版的包管理提供了 musl-wrapper,但 musl-cross-make 需要匹配对应的 musl-wrapper 版本,所以我们不使用包管理安装 musl-wrapper。 - -对于如何编译 musl-cross-make,将在本章节内的 **编译 musl-cross-make** 小节中介绍。 -::: - -### musl 环境 - -musl 环境指的是系统底层的 `libc` 库使用的是 `musl`,这是一种轻量级的 C 标准库,它的特点是可以被良好地静态链接。 - -对于目前流行的 Linux 发行版,Alpine Linux 使用的就是 musl 环境,所以 static-php-cli 在 Alpine Linux 下可以直接构建静态 PHP,仅需直接从包管理安装基础编译工具(如 gcc、cmake 等)即可。 - -对于其他发行版,如果你的发行版使用的是 musl 环境,那么你也可以在安装必要的编译工具后直接使用 static-php-cli 构建静态 PHP。 - -::: tip -在 musl 环境下,static-php-cli 会自动跳过 musl-wrapper 和 musl-cross-make 的安装。 -::: - -### Docker 环境 - -Docker 环境指的是使用 Docker 容器来构建静态 PHP,你可以使用 `bin/spc-alpine-docker` 来构建。 -执行这个命令前需要先安装 Docker,然后在项目根目录执行 `bin/spc-alpine-docker` 即可。 - -在执行 `bin/spc-alpine-docker` 后,static-php-cli 会自动下载 Alpine Linux 镜像,然后构建一个 `cwcc-spc-x86_64` 或 `cwcc-spc-aarch64` 的镜像。 -然后一切的构建都在这个镜像内进行,相当于在 Alpine Linux 内编译。总的来说,Docker 环境就是 musl 环境。 - -## musl-cross-make 工具链编译 - -在 Linux 中,尽管你不需要手动编译 musl-cross-make 工具,但是如果你想了解它的编译过程,可以参考这里。 -还有一个重要的原因就是,这个可能无法使用 CI、Actions 等自动化工具编译,因为现有的 CI 服务编译环境不满足 musl-cross-make 的编译要求,满足要求的配置价格太高。 - -musl-cross-make 的编译过程如下: - -准备一个 Alpine Linux 环境(直接安装或使用 Docker 均可),编译的过程需要 36GB 以上内存,所以你需要在内存较大的机器上编译。如果没有这么多内存,可能会导致编译失败。 - -然后将以下内容写入 `config.mak` 文件内: - -```makefile -STAT = -static --static -FLAG = -g0 -Os -Wno-error - -ifneq ($(NATIVE),) -COMMON_CONFIG += CC="$(HOST)-gcc ${STAT}" CXX="$(HOST)-g++ ${STAT}" -else -COMMON_CONFIG += CC="gcc ${STAT}" CXX="g++ ${STAT}" -endif - -COMMON_CONFIG += CFLAGS="${FLAG}" CXXFLAGS="${FLAG}" LDFLAGS="${STAT}" - -BINUTILS_CONFIG += --enable-gold=yes --enable-gprofng=no -GCC_CONFIG += --enable-static-pie --disable-cet --enable-default-pie -#--enable-default-pie - -CONFIG_SUB_REV = 888c8e3d5f7b -GCC_VER = 13.2.0 -BINUTILS_VER = 2.40 -MUSL_VER = 1.2.4 -GMP_VER = 6.2.1 -MPC_VER = 1.2.1 -MPFR_VER = 4.2.0 -LINUX_VER = 6.1.36 -``` - -同时,你需要新建一个 `gcc-13.2.0.tar.xz.sha1` 文件,文件内容如下: - -``` -5f95b6d042fb37d45c6cbebfc91decfbc4fb493c gcc-13.2.0.tar.xz -``` - -如果你使用的是 Docker 构建,新建一个 `Dockerfile` 文件,写入以下内容: - -```dockerfile -FROM alpine:edge - -RUN apk add --no-cache \ -gcc g++ git make curl perl \ -rsync patch wget libtool \ -texinfo autoconf automake \ -bison tar xz bzip2 zlib \ -file binutils flex \ -linux-headers libintl \ -gettext gettext-dev icu-libs pkgconf \ -pkgconfig icu-dev bash \ -ccache libarchive-tools zip - -WORKDIR /opt - -RUN git clone https://git.zv.io/toolchains/musl-cross-make.git -WORKDIR /opt/musl-cross-make -COPY config.mak /opt/musl-cross-make -COPY gcc-13.2.0.tar.xz.sha1 /opt/musl-cross-make/hashes - -RUN make TARGET=x86_64-linux-musl -j || : -RUN sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h -RUN make TARGET=x86_64-linux-musl -j -RUN make TARGET=x86_64-linux-musl install -j -RUN tar cvzf x86_64-musl-toolchain.tgz output/* -``` - -如果你使用的是非 Docker 环境的 Alpine Linux,可以直接执行 Dockerfile 中的命令,例如: - -```bash -apk add --no-cache \ -gcc g++ git make curl perl \ -rsync patch wget libtool \ -texinfo autoconf automake \ -bison tar xz bzip2 zlib \ -file binutils flex \ -linux-headers libintl \ -gettext gettext-dev icu-libs pkgconf \ -pkgconfig icu-dev bash \ -ccache libarchive-tools zip - -git clone https://git.zv.io/toolchains/musl-cross-make.git -# 将 config.mak 拷贝到 musl-cross-make 的工作目录内,你需要将 /path/to/config.mak 替换为你的 config.mak 文件路径 -cp /path/to/config.mak musl-cross-make/ -cp /path/to/gcc-13.2.0.tar.xz.sha1 musl-cross-make/hashes - -make TARGET=x86_64-linux-musl -j || : -sed -i 's/poison calloc/poison/g' ./gcc-13.2.0/gcc/system.h -make TARGET=x86_64-linux-musl -j -make TARGET=x86_64-linux-musl install -j -tar cvzf x86_64-musl-toolchain.tgz output/* -``` - -::: tip -以上所有脚本都适用于 x86_64 架构的 Linux。如果你需要构建 ARM 环境的 musl-cross-make,只需要将上方所有 `x86_64` 替换为 `aarch64` 即可。 -::: - -这个编译过程可能会因为内存不足、网络问题等原因导致编译失败,你可以多尝试几次,或者使用更大内存的机器来编译。 -如果遇到了问题,或者你有更好的改进方案,可以在 [讨论](https://github.com/crazywhalecc/static-php-cli-hosted/issues/1) 中提出。 - -## macOS 环境编译工具 - -对于 macOS 系统来说,我们使用的编译工具主要是 `clang`,它是 macOS 系统默认的编译器,同时也是 Xcode 的编译器。 - -在 macOS 下编译,主要依赖于 Xcode 或 Xcode Command Line Tools,你可以在 App Store 下载 Xcode,或者在终端执行 `xcode-select --install` 来安装 Xcode Command Line Tools。 - -此外,在 `doctor` 环境检查模块中,static-php-cli 会检查 macOS 系统是否安装了 Homebrew、编译工具等,如果没有,会提示你安装,这里不再赘述。 - -## FreeBSD 环境编译工具 - -FreeBSD 也是 Unix 系统,它的编译工具和 macOS 类似,你可以直接使用包管理 `pkg` 安装 `clang` 等编译工具,通过 `doctor` 命令。 - -## pkg-config 编译 - -如果你在使用 static-php-cli 构建静态 PHP 时仔细观察编译的日志,你会发现无论编译什么,都会先编译 `pkg-config`,这是因为 `pkg-config` 是一个用于查找依赖库的工具。 -在早期的 static-php-cli 版本中,我们直接使用了包管理安装的 `pkg-config` 工具,但是这样会导致一些问题,例如: - -- 即使指定了 `PKG_CONFIG_PATH`,`pkg-config` 也会尝试从系统路径中查找依赖包。 -- 由于 `pkg-config` 会从系统路径中查找依赖包,所以如果系统中存在同名的依赖包,可能会导致编译失败。 - -为了避免以上问题,我们将 `pkg-config` 编译到用户态的 `buildroot/bin` 内并使用,使用了 `--without-sysroot` 等参数来避免从系统路径中查找依赖包。 + diff --git a/docs/zh/develop/vendor-mode/annotations.md b/docs/zh/develop/vendor-mode/annotations.md new file mode 100644 index 000000000..a33273959 --- /dev/null +++ b/docs/zh/develop/vendor-mode/annotations.md @@ -0,0 +1,6 @@ +# 注解参考 + + diff --git a/docs/zh/develop/vendor-mode/dependency-injection.md b/docs/zh/develop/vendor-mode/dependency-injection.md new file mode 100644 index 000000000..01c662c6b --- /dev/null +++ b/docs/zh/develop/vendor-mode/dependency-injection.md @@ -0,0 +1,5 @@ +# 依赖注入 + + diff --git a/docs/zh/develop/vendor-mode/index.md b/docs/zh/develop/vendor-mode/index.md new file mode 100644 index 000000000..b90618f61 --- /dev/null +++ b/docs/zh/develop/vendor-mode/index.md @@ -0,0 +1,6 @@ +# Vendor 模式 + + diff --git a/docs/zh/develop/vendor-mode/lifecycle-hooks.md b/docs/zh/develop/vendor-mode/lifecycle-hooks.md new file mode 100644 index 000000000..ecaf5f41c --- /dev/null +++ b/docs/zh/develop/vendor-mode/lifecycle-hooks.md @@ -0,0 +1,6 @@ +# 生命周期 Hook + + diff --git a/docs/zh/develop/vendor-mode/package-classes.md b/docs/zh/develop/vendor-mode/package-classes.md new file mode 100644 index 000000000..15145190e --- /dev/null +++ b/docs/zh/develop/vendor-mode/package-classes.md @@ -0,0 +1,6 @@ +# 编写 Package 类 + + diff --git a/docs/zh/faq/index.md b/docs/zh/faq/index.md index 142a1503f..3fe16593e 100644 --- a/docs/zh/faq/index.md +++ b/docs/zh/faq/index.md @@ -1,96 +1,10 @@ # 常见问题 -这里将会编写一些你容易遇到的问题。目前有很多,但是我需要花时间来整理一下。 - -## php.ini 的路径是什么? - -在 Linux、macOS 和 FreeBSD 上,`php.ini` 的路径是 `/usr/local/etc/php/php.ini`。 -在 Windows 中,路径是 `C:\windows\php.ini` 或 `php.exe` 所在的当前目录。 -可以在 *nix 系统中使用手动构建选项 `--with-config-file-path` 来更改查找 `php.ini` 的目录。 - -此外,在 Linux、macOS 和 FreeBSD 上,`/usr/local/etc/php/conf.d` 目录中的 `.ini` 文件也会被加载。 -在 Windows 中,该路径默认为空。 -可以使用手动构建选项 `--with-config-file-scan-dir` 更改该目录。 - -PHP 默认也会从 [其他标准位置](https://www.php.net/manual/zh/configuration.file.php) 中搜索 `php.ini`。 - -## 静态编译的 PHP 可以安装扩展吗? - -因为传统架构下的 PHP 安装扩展的原理是使用 `.so` 类型的动态链接的库方式安装新扩展,而使用本项目编译的静态链接的 PHP。但是静态链接在不同操作系统有不同的定义。 - -首先,对于 Linux 系统,静态链接的二进制文件不会链接系统的动态链接库。纯静态链接的二进制文件(`-all-static`)无法加载动态库,因此无法添加新扩展。 -同时,在纯静态模式下,你也不能使用 `ffi` 等扩展来加载外部 `.so` 模块。 - -你可以使用命令 `ldd buildroot/bin/php` 来检查你在 Linux 下构建的二进制文件是否为纯静态链接。 - -如果你 [构建基于 GNU libc 的 PHP](../guide/build-with-glibc),你可以使用 `ffi` 扩展来加载外部 `.so` 模块,并加载具有相同 ABI 的 `.so` 扩展。 - -例如,你可以使用以下命令构建一个与 glibc 动态链接的静态 PHP 二进制文件,支持 FFI 扩展并加载相同 PHP 版本和相同 TS 类型的 `xdebug.so` 扩展: - -```bash -bin/spc-gnu-docker download --for-extensions=ffi,xml --with-php=8.4 -bin/spc-gnu-docker build ffi,xml --build-cli --debug - -buildroot/bin/php -d "zend_extension=/path/to/php{PHP_VER}-{ts/nts}/xdebug.so" --ri xdebug -``` - -对于 macOS 平台,macOS 下的几乎所有二进制文件都无法真正纯静态链接,几乎所有二进制文件都会链接 macOS 系统库:`/usr/lib/libresolv.9.dylib` 和 `/usr/lib/libSystem.B.dylib`。 -因此,在 macOS 上,你可以**直接**使用 SPC 构建具有动态链接扩展的静态编译 PHP 二进制文件: - -1. 使用 `--build-shared=XXX` 选项构建共享扩展 `xxx.so`。例如:`bin/spc build bcmath,zlib --build-shared=xdebug --build-cli` -2. 你将获得 `buildroot/modules/xdebug.so` 和 `buildroot/bin/php`。 -3. `xdebug.so` 文件可用于版本和线程安全相同的 php。 - -对于 Windows 平台,由于官方构建的扩展(如 `php_yaml.dll`)强制使用了 `php8.dll` 动态库作为链接,静态构建的 PHP 不包含任何系统库以外的动态库, -所以 Windows 下无法加载官方构建的动态扩展。 由于 static-php-cli 还暂未支持构建动态扩展,所以目前还没有让 static-php 加载动态扩展的方法。 - -不过,Windows 可以正常使用 `FFI` 扩展加载其他的 dll 文件并调用。 - -## 可以支持 Oracle 数据库扩展吗? - -部分依赖库闭源的扩展,如 `oci8`、`sourceguardian` 等,它们没有提供纯静态编译的依赖库文件(`.a`),仅提供了动态依赖库文件(`.so`), -这些扩展无法使用源码的形式编译到 static-php-cli 中,所以本项目可能永远也不会支持这些扩展。不过,理论上你可以根据上面的问题在 macOS 和 Linux 下接入和使用这类扩展。 - -如果你对此类扩展有需求,或者大部分人都对这些闭源扩展使用有需求, -可以看看有关 [standalone-php-cli](https://github.com/crazywhalecc/static-php-cli/discussions/58) 的讨论。欢迎留言。 - -## 支持 Windows 吗? - -该项目目前支持 Windows,但支持的扩展数量较少。Windows 支持并不完美。主要有以下问题: - -1. Windows 的编译过程与 *nix 不同,使用的工具链也不同。用于编译每个扩展依赖库的编译工具也几乎完全不同。 -2. Windows 版本的需求也会根据所有使用本项目的人的需求推进。如果很多人需要,我会尽快支持相关扩展。 - -## 我可以使用 micro 保护我的源代码吗? - -不可以。micro.sfx 本质上是将 php 和 php 代码合并为一个文件,没有编译或加密 PHP 代码的过程。 - -首先,php-src 是 PHP 代码的官方解释器,市场上没有与主流分支兼容的 PHP 编译器。 -我在网上看到一个名为 BPC(Binary PHP Compiler?)的项目可以将 PHP 编译为二进制,但有很多限制。 - -加密和保护代码的方向与编译不同。编译后,也可以通过逆向工程等方法获得代码。真正的保护仍然通过打包和加密代码等手段进行。 - -因此,本项目(static-php-cli)和相关项目(lwmbs、swoole-cli)都提供了 php-src 源代码的便捷编译工具。 -本项目和相关项目引用的 phpmicro 只是 PHP 的 sapi 接口封装,而不是 PHP 代码的编译工具。 -PHP 代码的编译器是一个完全不同的项目,因此不考虑额外的情况。 -如果你对加密感兴趣,可以考虑使用现有的加密技术,如 Swoole Compiler、Source Guardian 等。 - -## 无法使用 ssl - -**更新:该问题已在最新版本的 static-php-cli 中修复,现在默认读取系统的证书文件。如果你仍然遇到问题,请尝试下面的解决方案。** - -使用 curl、pgsql 等请求 HTTPS 网站或建立 SSL 连接时,可能会出现 `error:80000002:system library::No such file or directory` 错误。 -此错误是由于静态编译的 PHP 未通过 `php.ini` 指定 `openssl.cafile` 导致的。 - -你可以通过在使用 PHP 前指定 `php.ini` 并在 INI 中添加 `openssl.cafile=/path/to/your-cert.pem` 来解决此问题。 - -对于 Linux 系统,你可以从 curl 官方网站下载 [cacert.pem](https://curl.se/docs/caextract.html) 文件,也可以使用系统自带的证书文件。 -有关不同发行版的证书位置,请参考 [Golang 文档](https://go.dev/src/crypto/x509/root_linux.go)。 - -> INI 配置 `openssl.cafile` 不能使用 `ini_set()` 函数动态设置,因为 `openssl.cafile` 是 `PHP_INI_SYSTEM` 类型的配置,只能在 `php.ini` 文件中设置。 - -## 为什么不支持旧版本的 PHP? - -因为旧版本的 PHP 有很多问题,如安全问题、性能问题和功能问题。此外,许多旧版本的 PHP 与最新的依赖库不兼容,这也是不支持旧版本 PHP 的原因之一。 - -你可以使用 static-php-cli 早期编译的旧版本,如 PHP 8.0,但不会明确支持早期版本。 + diff --git a/docs/zh/guide/cli-generator.md b/docs/zh/guide/cli-generator.md index c3936dea2..bbf65ed39 100644 --- a/docs/zh/guide/cli-generator.md +++ b/docs/zh/guide/cli-generator.md @@ -2,14 +2,6 @@ aside: false --- - +# 编译命令生成器 -# CLI 编译命令生成器 - -::: tip -下面选择扩展可能包含所选操作系统不支持的扩展,这可能导致编译失败。请先查阅 [支持的扩展](./extensions)。 -::: - - + diff --git a/docs/zh/guide/cli-reference.md b/docs/zh/guide/cli-reference.md new file mode 100644 index 000000000..155f82020 --- /dev/null +++ b/docs/zh/guide/cli-reference.md @@ -0,0 +1,6 @@ +# 命令行参考 + + diff --git a/docs/zh/guide/deps-map.md b/docs/zh/guide/deps-map.md index 91ff57fd8..5dc795bd5 100644 --- a/docs/zh/guide/deps-map.md +++ b/docs/zh/guide/deps-map.md @@ -1,22 +1,3 @@ ---- -outline: 'deep' ---- +# 依赖关系图 -# 依赖关系图表 - -在编译 PHP 时,每个扩展、库都有依赖关系,这些依赖关系可能是必需的,也可能是可选的。在编译 PHP 时,可以选择是否包含这些可选的依赖关系。 - -例如,在 Linux 下编译 `gd` 扩展时,会强制编译 `zlib,libpng` 库和 `zlib` 扩展,而 `libavif,libwebp,libjpeg,freetype` 库都是可选的库,默认不会编译,除非通过 `--with-libs=avif,webp,jpeg,freetype` 选项指定。 - -- 对于可选扩展(扩展的可选特性),需手动在编译时指定,例如启用 Redis 的 igbinary 支持:`bin/spc build redis,igbinary`。 -- 对于可选库,需通过 `--with-libs=XXX` 选项编译指定。 -- 如果想启用所有的可选扩展,可以使用 `bin/spc build redis --with-suggested-exts` 参数。 -- 如果想启用所有的可选库,可以使用 `--with-suggested-libs` 参数。 - -## 扩展的依赖图 - - - -## 库的依赖表 - - \ No newline at end of file + diff --git a/docs/zh/guide/env-vars.md b/docs/zh/guide/env-vars.md index 9a2f9deb9..f3b41deb5 100644 --- a/docs/zh/guide/env-vars.md +++ b/docs/zh/guide/env-vars.md @@ -1,112 +1,4 @@ # 环境变量 -本页面的环境变量列表中所提到的所有环境变量都具有默认值,除非另有说明。你可以通过设置这些环境变量来覆盖默认值。 - -## 环境变量列表 - -在 2.3.5 版本之后,我们将环境变量集中到了 `config/env.ini` 文件中,你可以通过修改这个文件来设置环境变量。 - -我们将 static-php-cli 支持的环境变量分为三种: - -- 全局内部环境变量:在 static-php-cli 启动后即声明,你可以在 static-php-cli 的内部使用 `getenv()` 来获取他们,也可以在启动 static-php-cli 前覆盖。 -- 固定环境变量:在 static-php-cli 启动后声明,你仅可使用 `getenv()` 获取,但无法通过 shell 脚本对其覆盖。 -- 配置文件环境变量:在 static-php-cli 构建前声明,你可以通过修改 `config/env.ini` 文件或通过 shell 脚本来设置这些环境变量。 - -你可以阅读 [config/env.ini](https://github.com/crazywhalecc/static-php-cli/blob/main/config/env.ini) 中每项参数的注释来了解其作用(仅限英文版)。 - -## 自定义环境变量 - -一般情况下,你不需要修改任何以下环境变量,因为它们已经被设置为最佳值。 -但是,如果你有特殊需求,你可以通过设置这些环境变量来满足你的需求(比如你需要调试不同编译参数下的 PHP 性能表现)。 - -如需使用自定义环境变量,你可以在终端中使用 `export` 命令或者在命令前直接设置环境变量,例如: - -```shell -# export 方式 -export SPC_CONCURRENCY=4 -bin/spc build mbstring,pcntl --build-cli - -# 直接设置方式 -SPC_CONCURRENCY=4 bin/spc build mbstring,pcntl --build-cli -``` - -或者,如果你需要长期修改某个环境变量,你可以通过修改 `config/env.ini` 文件来实现。 - -`config/env.ini` 分为三段,其中 `[global]` 全局有效,`[windows]`、`[macos]`、`[linux]` 仅对应的操作系统有效。 - -例如,你需要修改编译 PHP 的 `./configure` 命令,你可以在 `config/env.ini` 文件中找到 `SPC_CMD_PREFIX_PHP_CONFIGURE` 环境变量,然后修改其值即可。 - -但如果你的构建条件比较复杂,需要多种 env.ini 进行切换,我们推荐你使用 `config/env.custom.ini` 文件,这样你可以在不修改默认的 `config/env.ini` 文件的情况下, -通过写入额外的重载项目指定你的环境变量。 - -```ini -; This is an example of `config/env.custom.ini` file, -; we modify the `SPC_CONCURRENCY` and linux default CFLAGS passing to libs and PHP -[global] -SPC_CONCURRENCY=4 - -[linux] -SPC_DEFAULT_C_FLAGS="-O3" -``` - -## 编译依赖库的环境变量(仅限 Unix 系统) - -从 2.2.0 开始,static-php-cli 对所有 macOS、Linux、FreeBSD 等 Unix 系统的编译依赖库的命令均支持自定义环境变量。 - -这样你就可以随时通过环境变量来调整编译依赖库的行为。例如你可以通过 `xxx_CFLAGS=-O0` 来设置编译 xxx 库的优化参数。 - -当然,不是每个依赖库都支持注入环境变量,我们目前提供了三个通配的环境变量,后缀分别为: - -- `_CFLAGS`: C 编译器的参数 -- `_LDFLAGS`: 链接器的参数 -- `_LIBS`: 额外的链接库 - -前缀为依赖库的名称,具体依赖库的名称以 `lib.json` 为准。其中,带有 `-` 的依赖库名称需要将 `-` 替换为 `_`。 - -下面是一个替换 openssl 库编译的优化选项示例: - -```shell -openssl_CFLAGS="-O0" -``` - -库名称使用同 `lib.json` 中列举的名称,区分大小写。 - -::: tip -当未指定相关环境变量时,除以下变量外,其余值均默认为空: - -| var name | var default value | -|-----------------------|-------------------------------------------------------------------------------------------------| -| `pkg_config_CFLAGS` | macOS: `$SPC_DEFAULT_C_FLAGS -Wimplicit-function-declaration -Wno-int-conversion`, Other: empty | -| `pkg_config_LDFLAGS` | Linux: `--static`, Other: empty | -| `imagemagick_LDFLAGS` | Linux: `-static`, Other: empty | -| `imagemagick_LIBS` | macOS: `-liconv`, Other: empty | -| `ldap_LDFLAGS` | `-L$BUILD_LIB_PATH` | -| `openssl_CFLAGS` | Linux: `$SPC_DEFAULT_C_FLAGS`, Other: empty | -| others... | empty | - -::: - -下表是支持自定义以上三种变量的依赖库名称列表: - -| lib name | -|-------------| -| brotli | -| bzip | -| curl | -| freetype | -| gettext | -| gmp | -| imagemagick | -| ldap | -| libargon2 | -| libavif | -| libcares | -| libevent | -| openssl | - -::: tip -因为给每个库适配自定义环境变量是一项特别繁琐的工作,且大部分情况下你都不需要这些库的自定义环境变量,所以我们目前只支持了部分库的自定义环境变量。 - -如果你需要自定义环境变量的库不在上方列表,可以通过 [GitHub Issue](https://github.com/crazywhalecc/static-php-cli/issues) -来提出需求。 -::: + diff --git a/docs/zh/guide/extension-notes.md b/docs/zh/guide/extension-notes.md index 70d60d1c9..607eec2a5 100644 --- a/docs/zh/guide/extension-notes.md +++ b/docs/zh/guide/extension-notes.md @@ -1,158 +1,3 @@ # 扩展注意事项 -因为是静态编译,扩展不会 100% 完美编译,而且不同扩展对 PHP、环境都有不同的要求,这里将一一列举。 - -## curl - -HTTP3 支持默认未启用,需在编译时添加 `--with-libs="nghttp2,nghttp3,ngtcp2"` 以启用 PHP 8.4 及以上版本的 HTTP3 支持。 - -使用 curl 请求 HTTPS 时,可能存在 `error:80000002:system library::No such file or directory` 错误, -解决办法详见 [FAQ - 无法使用 ssl](../faq/#无法使用-ssl)。 - -## phpmicro - -1. phpmicro SAPI 仅支持 PHP >= 8.0 版本。 - -## swoole - -1. swoole >= 5.0 版本仅支持 PHP >= 8.0 版本。 -2. swoole 目前不支持 PHP 8.0 版本 curl 的 hook(后续有可能会修复)。 -3. 编译时只包含 `swoole` 扩展时不会完整开启支持的 Swoole 数据库协程 hook,如需使用请加入对应的 `swoole-hook-xxx` 扩展。 -4. swoole 在部分扩展组合下可能出现 `zend_mm_heap corrupted` 问题,暂未找到是什么原因导致的。 - -## swoole-hook-pgsql - -swoole-hook-pgsql 不是一个扩展,而是 Swoole 的 Hook 特性。 -如果你在编译时添加了 `swoole,swoole-hook-pgsql`,你将启用 Swoole 的 PostgreSQL 客户端和 `pdo_pgsql` 扩展的协程模式。 - -swoole-hook-pgsql 与 `pdo_pgsql` 扩展冲突。如需使用 Swoole 和 `pdo_pgsql`,请删除 pdo_pgsql 扩展,启用 `swoole` 和 `swoole-hook-pgsql` 即可。 -该扩展包含了 `pdo_pgsql` 的协程环境的实现。 - -在 macOS 系统,`pdo_pgsql` 可能无法正常连接到 postgresql 服务器,请谨慎使用。 - -## swoole-hook-mysql - -swoole-hook-mysql 不是一个扩展,而是 Swoole 的 Hook 特性。 -如果你在编译时添加了 `swoole,swoole-hook-mysql`,你将启用 Swoole 的 `mysqlnd` 和 `pdo_mysql` 的协程模式。 - -## swoole-hook-sqlite - -swoole-hook-sqlite 不是一个扩展,而是 Swoole 的 Hook 特性。 -如果你在编译时添加了 `swoole,swoole-hook-sqlite`,你将启用 Swoole 的 `pdo_sqlite` 的协程模式(Swoole 必须为 5.1 以上)。 - -swoole-hook-sqlite 与 `pdo_sqlite` 扩展冲突。如需使用 Swoole 和 `pdo_sqlite`,请删除 pdo_sqlite 扩展,启用 `swoole` 和 `swoole-hook-sqlite` 即可。 -该扩展包含了 `pdo_sqlite` 的协程环境的实现。 - -## swoole-hook-odbc - -swoole-hook-odbc 不是一个扩展,而是 Swoole 的 Hook 特性。 -如果你在编译时添加了 `swoole,swoole-hook-odbc`,你将启用 Swoole 的 `odbc` 扩展的协程模式。 - -swoole-hook-odbc 与 `pdo_odbc` 扩展冲突。如需使用 Swoole 和 `pdo_odbc`,请删除 `pdo_odbc` 扩展,启用 `swoole` 和 `swoole-hook-odbc` 即可。 -该扩展包含了 `pdo_odbc` 的协程环境的实现。 - -## swow - -1. swow 仅支持 PHP 8.0+ 版本。 - -## imagick - -1. OpenMP 支持已被禁用,这是维护者推荐的做法,系统软件包也是如此配置。 - -## imap - -1. 该扩展目前不支持 Kerberos。 -2. 由于底层的 c-client、ext-imap 不是线程安全的。 无法在 `--enable-zts` 构建中使用它。 -3. 该扩展已在 PHP 8.4 中被移除,因此我们建议您寻找替代实现,例如 [Webklex/php-imap](https://github.com/Webklex/php-imap)。 - -## gd - -1. gd 扩展依赖了较多的额外图形库,默认情况下,直接使用 `bin/spc build gd` 不会引入和支持部分图形库,例如 `libjpeg`、`libavif` 等, -需要使用 `--with-libs` 参数补全。目前支持 `freetype,libjpeg,libavif,libwebp` 四个库的支持,所以这里可以使用以下命令来让 gd 库引入它们: - -```bash -bin/spc build gd --with-libs=freetype,libjpeg,libavif,libwebp --build-cli -``` - -## mcrypt - -1. 目前未支持,未来也不计划支持此扩展。[#32](https://github.com/crazywhalecc/static-php-cli/issues/32) - -## oci8 - -1. oci8 是 Oracle 数据库的扩展,因为 Oracle 提供的扩展所依赖的库未提供静态编译版本(`.a`)或源代码,无法使用静态链接的方式将此扩展编译到 php 内,故无法支持。 - -## xdebug - -1. Xdebug 只能作为共享扩展进行构建。您需要使用除了 `musl-static` 外的其他 `SPC_TARGET` 构建目标。 -2. 使用 Linux/glibc 或 macOS 时,您可以使用 `--build-shared=xdebug` 将 Xdebug 编译为共享扩展。 - 编译后的 `./php` 二进制文件可以通过指定 INI 文件进行配置和运行,例如 `./php -d 'zend_extension=/path/to/xdebug.so' your-code.php`。 - -## xml - -1. xml包括 xmlreader、xmlwriter、dom、simplexml 等,添加 xml 扩展时最好同时启用这些扩展。 -2. libxml 包含在 xml 扩展中。 启用 xml 相当于启用 libxml。 - -## glfw - -1. glfw 扩展依赖 OpenGL,在 Linux 平台还依赖 X11 等环境,这些库都无法被轻易地动态链接。 -2. 在 macOS 系统下,我们可以动态链接系统的 OpenGL 和一些相关的库。 - -## rar - -1. rar 扩展目前在 macOS x86_64 环境下与 `common` 扩展集合编译 phpmicro 存在问题。 - -## pgsql - -~~pgsql ssl 连接与 openssl 3.2.0 不兼容。相关链接:~~ - -- ~~~~ -- ~~~~ -- ~~~~ - -pgsql 16.2 修复了这个 Bug,现在正常工作了。 - -在 pgsql 使用 SSL 连接时,可能存在 `error:80000002:system library::No such file or directory` 错误, -解决办法详见 [FAQ - 无法使用 ssl](../faq/#无法使用-ssl)。 - -## openssl - -使用基于 openssl 的扩展(如 curl、pgsql 等网络库)时,可能存在 `error:80000002:system library::No such file or directory` 错误, -解决办法详见 [FAQ - 无法使用 ssl](../faq/#无法使用-ssl)。 - -## password-argon2 - -1. password-argon2不是一个标准的扩展。`password_hash` 函数的 `PASSWORD_ARGON2ID` 算法需要 libsodium 或 libargon2 才能工作。 -2. 使用 password-argon2 可以为此启用多线程支持。 - -## ffi - -1. 由于 musl libc 静态链接的限制,无法加载动态库,因此无法使用 ffi。 - 如果您需要使用 ffi 扩展,请参阅 [使用 GNU libc 编译 PHP](./build-with-glibc)。 -2. macOS 支持 ffi 扩展,但某些内核不包含调试符号时会出现错误。 -3. Windows x64 支持 ffi 扩展。 - -## xhprof - -xhprof 扩展包含三部分:`xhprof_extension`、`xhprof_html`、`xhprof_libs`。编译的二进制中只包含 `xhprof_extension`。 -如果需要使用 xhprof,请到 [pecl.php.net/package/xhprof](http://pecl.php.net/package/xhprof) 下载源码,指定 `xhprof_libs` 和 `xhprof_html` 路径来使用。 - -## event - -event 扩展在 macOS 系统下编译后暂无法使用 `openpty` 特性。相关 Issue: - -- [static-php-cli#335](https://github.com/crazywhalecc/static-php-cli/issues/335) - -## parallel - -parallel 扩展只支持 PHP 8.0 及以上版本,并只支持 ZTS 构建(`--enable-zts`)。 - -## spx - -1. SPX 目前不支持 Windows,且官方仓库也不支持静态编译,static-php-cli 使用了 [修改版本](https://github.com/static-php/php-spx)。 - -## mimalloc - -1. 从技术上讲,这不是扩展,而是一个库。 -2. 在 Linux 或 macOS 上使用 `--with-libs="mimalloc"` 进行构建将覆盖默认分配器。 -3. 目前,这还处于实验阶段,但建议在线程环境中使用。 + diff --git a/docs/zh/guide/extensions.md b/docs/zh/guide/extensions.md index 43095a077..4d6aee802 100644 --- a/docs/zh/guide/extensions.md +++ b/docs/zh/guide/extensions.md @@ -1,22 +1,3 @@ - +# 支持的扩展列表 -# 扩展列表 - -> - `yes`: 已支持 -> - 空白: 目前还不支持,或正在支持中 -> - `no` with issue link: 确定不支持或无法支持 -> - `partial` with issue link: 已支持,但是无法完美工作 - - - - -::: tip -如果缺少您需要的扩展,您可以创建 [功能请求](https://github.com/crazywhalecc/static-php-cli/issues)。 - -有些扩展或扩展依赖的库会有一些可选的特性,例如 gd 库可选支持 libwebp、freetype 等。 -如果你只使用 `bin/spc build gd --build-cli` 是不会包含它们(static-php-cli 默认为最小依赖原则)。 - -有关编译可选库,请参考 [扩展、库的依赖关系图表](./deps-map)。对于可选的库,你也可以从 [编译命令生成器](./cli-generator) 中选择扩展后展开选择可选库。 -::: + diff --git a/docs/zh/guide/first-build.md b/docs/zh/guide/first-build.md new file mode 100644 index 000000000..a9d65c8d2 --- /dev/null +++ b/docs/zh/guide/first-build.md @@ -0,0 +1,187 @@ +# 第一次构建 + +本页通过完整的示例演示如何从零开始构建一个静态 PHP 二进制。 + +::: tip +如果你采用的是 spc 二进制方式安装,请将本章节中的所有 `spc` 替换为 `./spc` 或 `.\spc.exe`。 + +如果你采用的是源码安装,请将 `spc` 替换为 `bin/spc`。 +::: + +## 两种构建方式 + +StaticPHP 提供两种构建方式,根据使用场景选择: + +| 方式 | 适合场景 | +|--------------|--------------------------| +| `craft` 一键构建 | 日常使用、快速上手 | +| 分步构建 | CI/CD 流水线、需要拆分下载与编译阶段的场景 | + +## 方式一:`craft` 一键构建(推荐) + +`craft` 命令读取一个 `craft.yml` 配置文件,自动完成依赖下载、库编译、PHP 构建的全流程。 + +### 编写 craft.yml + +在当前目录创建 `craft.yml`,声明要编译的 PHP 版本、扩展和目标 SAPI: + +```yaml +php-version: 8.4 +extensions: bcmath,posix,phar,zlib,openssl,curl,fileinfo,tokenizer +sapi: + - cli + - micro +``` + +不想手动编写?试试[命令行生成器](./cli-generator)自动生成配置。 + +### 开始构建 + +```bash +spc craft +``` + +构建过程依次执行:下载依赖 → 编译依赖库 → 编译 PHP。全程无需人工干预。 + +如需查看详细日志,加上 `-v`、`-vv` 或 `-vvv` 参数: + +```bash +spc craft -v +``` + +### 查看产物 + +构建成功后,产物均位于 `buildroot/bin/`: + +| SAPI | 产物路径 | +|------------|------------------------------------------------------| +| cli | `buildroot/bin/php`(Windows:`buildroot/bin/php.exe`) | +| fpm | `buildroot/bin/php-fpm` | +| micro | `buildroot/bin/micro.sfx` | +| embed | `buildroot/lib/libphp.a` | +| frankenphp | `buildroot/bin/frankenphp` | + +验证一下 cli 是否可用: + +```bash +./buildroot/bin/php -v +./buildroot/bin/php -m +``` + +## 方式二:分步构建 + +分步方式适合需要将下载与编译拆分为独立阶段的场景,例如在 CI 中缓存下载内容以加速后续构建。 + +### 第一步:下载依赖 + +v3 版本中,你可以省略这一步骤,直接构建想要的内容,StaticPHP 会自动下载所需的依赖库和扩展源码。 + +但如果你想提前下载,或在网络环境较差的情况下分阶段构建,可以使用 `download` 命令: + +```bash +# 按扩展列表下载(推荐,只下载实际需要的内容) +spc download --for-extensions="bcmath,posix,phar,zlib,openssl,curl,fileinfo,tokenizer" --with-php=8.4 + +# 按依赖包列表下载 +spc download "curl,openssl" --with-php=8.4 +``` + +下载内容缓存在 `downloads/` 目录,重复构建时会直接复用。 + +```bash +# 网络较慢时,可增大并发数和重试次数 +spc download --for-extensions=bcmath,openssl,curl --parallel 10 --retry=3 + +# 优先使用预编译的二进制依赖,跳过源码编译(大幅加速构建) +spc download --for-extensions=bcmath,openssl,curl --prefer-binary +``` + +### 第二步:构建 PHP + +```bash +# 构建 cli SAPI +spc build:php bcmath,phar,zlib,openssl,curl,fileinfo,tokenizer --build-cli + +# 同时构建多个 SAPI +spc build:php bcmath,phar,zlib,openssl,curl --build-cli --build-micro +``` + + + +#### 常用构建选项 + +| 选项 | 说明 | +|----------------------|--------------------------------------| +| `--build-cli` | 构建 cli SAPI | +| `--build-fpm` | 构建 php-fpm(不支持 Windows) | +| `--build-micro` | 构建 micro.sfx | +| `--build-embed` | 构建嵌入式 SAPI | +| `--build-frankenphp` | 构建 FrankenPHP | +| `--enable-zts` | 启用线程安全(ZTS)版本 | +| `--no-strip` | 保留调试符号,不精简二进制 | +| `-I key=value` | 硬编译 INI 选项到 PHP 中 | +| `--with-upx-pack` | 用 UPX 压缩产物(需先 `spc install-pkg upx`) | + +硬编译 INI 的例子——预设更大的内存限制,并禁用 `system` 函数: + +```bash +spc build:php bcmath,pcntl,posix --build-cli -I "memory_limit=4G" -I "disable_functions=system" +``` + +## 打包 micro 应用 + +构建 `micro.sfx` 后,用 `micro:combine` 将你的 PHP 代码打包进去,生成一个完全独立的可执行文件: + +```bash +echo " hello.php +spc micro:combine hello.php --output=hello +./hello +``` + +也支持打包 `.phar` 文件,以及注入 INI 配置: + +```bash +# 打包 phar +spc micro:combine your-app.phar --output=your-app + +# 打包时注入 INI +spc micro:combine your-app.phar --output=your-app -I "memory_limit=512M" + +# 从 ini 文件注入配置 +spc micro:combine your-app.phar --output=your-app -N /path/to/custom.ini +``` + +## 调试与重新构建 + +构建失败,或想查看详细过程,使用 `-v` / `-vv` / `-vvv`: + +- `-v` 将显示 `INFO` 级别的日志,包含执行到的模块和执行的编译命令等。 +- `-vv` 将显示 `DEBUG` 级别的日志,包含所有 StaticPHP 中调试级别的日志。 +- `-vvv` 将显示 `DEBUG` 级别的日志,并将其他 shell 命令执行的 STDOUT 输出到终端。 + +```bash +spc build:php bcmath,openssl --build-cli -vv +``` + +或者,你也可以查看 `log/spc.shell.log` 和 `log/spc.output.log` 获取终端输出和 StaticPHP 日志。 + +如需清理编译中间产物、从头重新构建(不重新下载),使用 `reset`: + +```bash +spc reset +# 然后重新构建 +spc build:php bcmath,openssl --build-cli +``` + +::: tip +`reset` 只清理 `buildroot/` 和 `source/` 目录,不会删除 `downloads/` 缓存。 +如需同时清理下载缓存,加上 `--with-download` 参数。 +::: + +如果问题持续无法解决,欢迎提交 [Issue](https://github.com/crazywhalecc/static-php-cli/issues),并附上 `craft.yml`(如有)和 `log/` 目录的压缩包。 + +## 接下来 + +- [命令行参考](./cli-reference) — 所有命令与选项的完整说明 +- [扩展列表](./extensions) — 查看支持的扩展及其依赖关系 +- [常见问题](./troubleshooting) — 构建失败时的排查指南 diff --git a/docs/zh/guide/index.md b/docs/zh/guide/index.md index 8e5727c2a..41158498f 100644 --- a/docs/zh/guide/index.md +++ b/docs/zh/guide/index.md @@ -1,48 +1,53 @@ -# 指南 +# 构建指南 -static-php-cli 是一个用于构建静态编译的 PHP 二进制的工具,目前支持 Linux 和 macOS 系统。 +## StaticPHP 是什么 -在指南章节中,你将了解到如何使用 static-php-cli 构建独立的 php 程序。 +StaticPHP 是一个构建工具,能够将 PHP 解释器与你所需的扩展一起编译成一个独立的二进制文件,无需在目标系统上预先安装 PHP 或任何依赖库。 +构建产物可以直接分发和运行,适用于 Linux、macOS 和 Windows 平台。 -- [本地构建](./manual-build) -- [Action 构建](./action-build) -- [扩展列表](./extensions) +## 为什么要构建静态 PHP -## 编译环境 +普通 PHP 安装依赖系统环境:你需要先安装 PHP、再装扩展、再处理各个发行版之间的差异。 +将 PHP 构建为静态二进制之后,这些问题都不再存在——你得到的是一个单文件可执行程序,在任何相同架构的系统上开箱即用。 -下面是架构支持情况,:gear: 代表支持 GitHub Action 构建,:computer: 代表支持本地构建,空 代表暂不支持。 +典型使用场景: -| | x86_64 | aarch64 | -|---------|-------------------|-------------------| -| macOS | :gear: :computer: | :gear: :computer: | -| Linux | :gear: :computer: | :gear: :computer: | -| Windows | :gear: :computer: | | -| FreeBSD | :computer: | :computer: | +- **部署命令行工具**:把 PHP 工具(如 Composer、PHPStan、自研 CLI)打包后直接分发,用户无需安装 PHP。 +- **容器和嵌入式环境**:用最小体积的静态 PHP 替代臃肿的基础镜像。 +- **服务端应用**:构建包含 FPM 或 FrankenPHP SAPI 的静态二进制,部署更简单,不依赖宿主机环境。 -当前支持编译的 PHP 版本: +## phpmicro:把 PHP 和你的代码打包成一个文件 -> :warning: 部分支持,对于新的测试版和旧版本可能存在问题。 -> -> :heavy_check_mark: 支持 -> -> :x: 不支持 +[phpmicro](https://github.com/easysoft/phpmicro) 是一个第三方 PHP SAPI,StaticPHP 对其提供原生支持。 +它能将 PHP 解释器本身和你的 `.php` 源文件(或 `.phar` 打包文件)合并成单个自解压可执行文件(`sfx`)。 -| PHP Version | Status | Comment | -|-------------|--------------------|---------------------------------------------------------| -| 7.2 | :x: | | -| 7.3 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | -| 7.4 | :x: | phpmicro 和许多扩展不支持 7.3、7.4 版本 | -| 8.0 | :warning: | PHP 官方已停止 8.0 的维护,我们不再处理 8.0 相关的 backport 支持 | -| 8.1 | :warning: | PHP 官方仅对 8.1 提供安全更新,在 8.5 发布后我们不再处理 8.1 相关的 backport 支持 | -| 8.2 | :heavy_check_mark: | | -| 8.3 | :heavy_check_mark: | | -| 8.4 | :heavy_check_mark: | | -| 8.5 (beta) | :warning: | PHP 8.5 目前处于 beta 阶段 | +``` +micro.sfx + your-app.phar = your-app (可直接运行,无任何依赖) +``` -> 这个表格的支持状态是 static-php-cli 对构建对应版本的支持情况,不是 PHP 官方对该版本的支持情况。 +这特别适合分发 PHP 编写的命令行工具:用户拿到的只是一个普通的可执行文件,完全感知不到背后是 PHP。 -## PHP 支持版本 +## 改善你的项目分发与部署 -目前,static-php-cli 对 PHP 8.2 ~ 8.5 版本是支持的,对于 PHP 8.1 及更早版本理论上支持,只需下载时选择早期版本即可。 -但由于部分扩展和特殊组件已对早期版本的 PHP 停止了支持,所以 static-php-cli 不会明确支持早期版本。 -我们推荐你编译尽可能新的 PHP 版本,以获得更好的体验。 +**取代臃肿的 Docker 基础镜像** + +官方 `php:8.x` 镜像动辄数百 MB,大多数情况下只是为了提供一个 PHP 运行环境。 +改用静态 PHP 二进制配合极简基础镜像(甚至 `FROM scratch`),镜像体积可以压缩到个位数 MB,启动速度也更快。 + +**构建可分发的 PHP CLI 工具** + +用 [symfony/console](https://symfony.com/doc/current/components/console.html) 或 [Laravel Zero](https://laravel-zero.com) 写好你的 CLI 程序, +再用 [Box](https://github.com/box-project/box) 打包成 `.phar`,最后通过 phpmicro 合并为单文件可执行程序。 +最终产物可以直接分发,用户无需安装任何 PHP 环境,和 Go、Rust 工具的体验完全一致。 + +**基于 FrankenPHP 构建单文件 Web 应用** + +[FrankenPHP](https://frankenphp.dev) 是一个现代 PHP 应用服务器,内置 HTTP/2、HTTP/3 和 HTTPS 自动管理。 +StaticPHP 支持将 FrankenPHP 连同所需扩展一起静态编译, +最终产物是一个包含完整 Web 服务器的单一可执行文件,无需 Nginx、PHP-FPM,直接部署即可运行。 + +## 接下来 + +- [安装 SPC](./installation) — 安装 StaticPHP 构建工具 +- [第一次构建](./first-build) — 完整流程演示:从下载源码到得到可执行文件 +- [命令行参考](./cli-reference) — 所有命令与选项速查 diff --git a/docs/zh/guide/installation.md b/docs/zh/guide/installation.md new file mode 100644 index 000000000..1bfce62f9 --- /dev/null +++ b/docs/zh/guide/installation.md @@ -0,0 +1,108 @@ +# 安装 StaticPHP + +## 系统要求 + +| 平台 | 架构 | 说明 | +|---|---|---| +| Linux | x86_64、aarch64 | 支持主流发行版(Alpine、Debian/Ubuntu、RHEL/CentOS 等) | +| macOS | x86_64 (Intel)、arm64 (Apple Silicon) | 需要 macOS 12 或更高版本 | +| Windows | x86_64 | 需要 Windows 10 Build 17063 或更高版本 | + +::: tip +Linux 下,glibc 环境(Debian、Ubuntu、Arch 等)和 musl 环境(Alpine)均受支持。 +`doctor` 命令会自动检测当前环境并在必要时引导安装合适的工具链。 +::: + +StaticPHP 有多种安装方式,选择适合你的场景: + +| 方式 | 适合谁 | +|---|---| +| 预编译二进制 | 大多数用户,直接下载开箱即用 | +| 从源码安装 | 参与开发、或需要修改核心构建逻辑的开发者 | +| Vendor 模式 | 在已有 PHP 项目中集成 StaticPHP 能力 | + +## 预编译二进制 + +spc 无须任何依赖,下载即可运行,支持 Linux、macOS 和 Windows。 + +> spc 本身是由 StaticPHP 构建的静态 PHP 二进制,幽默地说:我们用 StaticPHP 构建了 StaticPHP 的构建工具。 + +```shell +# Linux x86_64 +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-x86_64 -o spc +# Linux arm64 +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-linux-aarch64 -o spc +# macOS x86_64 (Intel) +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-x86_64 -o spc +# macOS arm64 (Apple Silicon) +curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-macos-aarch64 -o spc +# Windows x86_64 (PowerShell) +curl.exe -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-windows-x86_64.exe -o spc.exe +``` + +*nix 系统下载完成后需要赋予可执行权限: + +```bash +chmod +x spc && ./spc --version +``` + +## 从源码安装 + +适合想参与开发、或需要修改核心注册表和构建脚本的开发者。需要系统已安装 PHP >= 8.4、Composer,以及 `mbstring,posix,pcntl,iconv,phar,zlib` 扩展。 + +```bash +git clone https://github.com/static-php/static-php.git --branch v3 +cd static-php-cli +composer install +``` + +如果系统还没有 PHP 和 Composer,可以用内置脚本一键安装运行环境: + +::: code-group +```bash [Linux / macOS] +bin/setup-runtime +``` +```powershell [Windows] +.\bin\setup-runtime.ps1 +.\bin\setup-runtime.ps1 add-path # 将 runtime/ 加入 PATH +``` +::: + +脚本执行完成后,会在项目目录下生成 `runtime/` 子目录,其中包含 `php` 和 `composer` 两个可执行文件。安装完成后有两种使用方式: + +1. **直接通过路径调用**(无需修改环境变量): + ```bash + runtime/php bin/spc --help + runtime/php runtime/composer install + ``` + +2. **将 `runtime/` 加入 PATH**(之后可直接使用 `php`、`composer`、`bin/spc`): + ```bash + export PATH="/path/to/static-php/runtime:$PATH" + # 建议写入 ~/.bashrc 或 ~/.zshrc 使其永久生效 + ``` + +## Vendor 模式 + +适合在已有 PHP 项目中直接集成 StaticPHP 能力,或通过自定义 registry 支持私有库和扩展的构建。 + +```bash +composer require crazywhalecc/static-php-cli +``` + +Vendor 模式的详细用法见 [Vendor 模式指南](../develop/vendor-mode/)。 + +## 验证构建环境 + +> **Vendor 模式用户可跳过此步骤。** + +安装完成后,运行 `doctor` 检查系统构建工具链是否就绪(cmake、make、编译器等): + +```bash +# 使用 spc 二进制 +./spc doctor --auto-fix +# 使用源码安装 +bin/spc doctor --auto-fix +``` + +检查通过后,继续阅读[第一次构建](./first-build)。 diff --git a/docs/zh/guide/troubleshooting.md b/docs/zh/guide/troubleshooting.md index c65236547..79c39bd89 100644 --- a/docs/zh/guide/troubleshooting.md +++ b/docs/zh/guide/troubleshooting.md @@ -1,31 +1,5 @@ # 故障排除 -使用 static-php-cli 过程中可能会碰到各种各样的故障,这里将讲述如何自行查看错误并反馈 Issue。 - -## 下载失败问题 - -下载资源问题是 spc 最常见的问题之一。主要是由于 spc 下载资源使用的地址一般均为对应项目的官方网站或 GitHub 等,而这些网站可能偶尔会宕机、屏蔽 IP 地址。 -在遇到下载失败后,可以多次尝试调用下载命令。 - -当下载资源时,你可能最终会看到类似 `curl: (56) The requested URL returned error: 403` 的错误,这通常是由于 GitHub 限制导致的。 -你可以通过在命令中添加 `--debug` 来验证,会看到类似 `[DEBU] Running command (no output) : curl -sfSL "https://api.github.com/repos/openssl/openssl/releases"` 的输出。 - -要解决这个问题,可以在 GitHub 上 [创建](https://github.com/settings/tokens) 一个个人访问令牌,并将其设置为环境变量 `GITHUB_TOKEN=`。 - -如果确认地址确实无法正常访问,可以提交 Issue 或 PR 更新地址或下载类型。 - -## Doctor 无法修复某些问题 - -在绝大部分情况下,doctor 模块都可以对缺失的系统环境进行自动修复和安装,但也存在特殊的环境无法正常使用自动修复功能。 - -由于系统限制(例如,Windows 下无法自动安装 Visual Studio 等软件),自动修复功能无法用于某些项目。 -在遇到无法自动修复功能时,如果遇到 `Some check items can not be fixed` 字样,则表明无法自动修复。 -请根据终端显示的方法提交 Issue 或自行修复环境。 - -## 编译错误 - -遇到编译错误时,如果没有开启 `--debug` 日志,请先开启调试日志,然后确定报错的命令。 -报错的终端输出对于修复编译错误非常重要。 -在提交 Issue 时,请上传终端日志的最后报错片段(或整个终端日志输出),并且包含使用的 `spc` 命令和参数。 - -如果你是重复构建,请参考 [本地构建 - 多次构建](./manual-build#多次构建) 章节。 + diff --git a/yarn.lock b/yarn.lock index 61bba08eb..234b5cd95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4,24 +4,24 @@ "@babel/helper-string-parser@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== "@babel/helper-validator-identifier@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== "@babel/parser@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz" integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== dependencies: "@babel/types" "^7.28.2" "@babel/types@^7.28.2": version "7.28.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz" integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== dependencies: "@babel/helper-string-parser" "^7.27.1" @@ -29,269 +29,49 @@ "@docsearch/css@^4.0.0-beta.7": version "4.0.0-beta.8" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-4.0.0-beta.8.tgz#836ac7c3eeecf87cfc9c518210f4dfd27e49b05f" + resolved "https://registry.npmjs.org/@docsearch/css/-/css-4.0.0-beta.8.tgz" integrity sha512-/ZlyvZCjIJM4aaOYoJpVNHPJckX7J5KIbt6IWjnZXvo0QAUI1aH976vKEJUC9olgUbE3LWafB8yuX4qoqahIQg== "@docsearch/js@^4.0.0-beta.7": version "4.0.0-beta.8" - resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-4.0.0-beta.8.tgz#eca030c793fad34487c0c1c9147112544edb02bc" + resolved "https://registry.npmjs.org/@docsearch/js/-/js-4.0.0-beta.8.tgz" integrity sha512-elgqPYpykRQr5MlfqoO8U2uC3BcPgjUQhzmHt/H4lSzP7khJ9Jpv/cCB4tiZreXb6GkdRgWr5csiItNq6jjnhg== -"@esbuild/aix-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" - integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== - -"@esbuild/android-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c" - integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== - -"@esbuild/android-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419" - integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== - -"@esbuild/android-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683" - integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== - -"@esbuild/darwin-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae" - integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== - -"@esbuild/darwin-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be" - integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== - -"@esbuild/freebsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca" - integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== - -"@esbuild/freebsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab" - integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== - -"@esbuild/linux-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b" - integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== - -"@esbuild/linux-arm@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37" - integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== - -"@esbuild/linux-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4" - integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== - -"@esbuild/linux-loong64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0" - integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== - -"@esbuild/linux-mips64el@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5" - integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== - -"@esbuild/linux-ppc64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db" - integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== - -"@esbuild/linux-riscv64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547" - integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== - -"@esbuild/linux-s390x@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830" - integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== - "@esbuild/linux-x64@0.25.9": version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f" + resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz" integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== -"@esbuild/netbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548" - integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== - -"@esbuild/netbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52" - integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== - -"@esbuild/openbsd-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935" - integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== - -"@esbuild/openbsd-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf" - integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== - -"@esbuild/openharmony-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314" - integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== - -"@esbuild/sunos-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e" - integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== - -"@esbuild/win32-arm64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b" - integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== - -"@esbuild/win32-ia32@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3" - integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== - -"@esbuild/win32-x64@0.25.9": - version "0.25.9" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" - integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== - "@iconify-json/simple-icons@^1.2.47": version "1.2.49" - resolved "https://registry.yarnpkg.com/@iconify-json/simple-icons/-/simple-icons-1.2.49.tgz#2465a3d7e24d32b57dd3ce873914417af6e6dbea" + resolved "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.49.tgz" integrity sha512-nRLwrHzz+cTAQYBNQrcr4eWOmQIcHObTj/QSi7nj0SFwVh5MvBsgx8OhoDC/R8iGklNmMpmoE/NKU0cPXMlOZw== dependencies: "@iconify/types" "*" "@iconify/types@*": version "2.0.0" - resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" + resolved "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz" integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@rolldown/pluginutils@1.0.0-beta.29": version "1.0.0-beta.29" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz#f8fc9a8788757dccba0d3b7fee93183621773d4c" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz" integrity sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q== -"@rollup/rollup-android-arm-eabi@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz#ba432433f5e7b419dba2be407d1d59fea6b8de48" - integrity sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA== - -"@rollup/rollup-android-arm64@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz#4e05c86e0fb9af6eaf52fc298dcdec577477e35c" - integrity sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w== - -"@rollup/rollup-darwin-arm64@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz#788fad425b4129875639e0c14b6441c5f3b69d46" - integrity sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw== - -"@rollup/rollup-darwin-x64@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz#d44e05bee55b781d7c2cf535d9f9169787c3599d" - integrity sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg== - -"@rollup/rollup-freebsd-arm64@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz#107786b4d604495224c3543bfd2cae33ddf76500" - integrity sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA== - -"@rollup/rollup-freebsd-x64@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz#54e105c3da27f31084ca6913fed603627755abde" - integrity sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w== - -"@rollup/rollup-linux-arm-gnueabihf@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz#725c23e0766b5d9368180bc2c427a51e31d0e147" - integrity sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w== - -"@rollup/rollup-linux-arm-musleabihf@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz#6946b0d2f132f2baf5657945b81565d8abd51cc0" - integrity sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA== - -"@rollup/rollup-linux-arm64-gnu@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz#83510a6d03e748619241a17f5a879418a963c5ed" - integrity sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg== - -"@rollup/rollup-linux-arm64-musl@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz#085b98d44c10908626dd40f26bf924433bbd8471" - integrity sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg== - -"@rollup/rollup-linux-loongarch64-gnu@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz#13e0a4808e9f7924f2cc8c133603f627c7a00543" - integrity sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ== - -"@rollup/rollup-linux-ppc64-gnu@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz#aeee4e47fc9ca5d6687e686fea4696202af6b2f4" - integrity sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g== - -"@rollup/rollup-linux-riscv64-gnu@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz#603e4591643f1d7851a96d096cf7fcd273f7b0e1" - integrity sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw== - -"@rollup/rollup-linux-riscv64-musl@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz#f8fd9b01f1888e1816d5a398789d430511286c00" - integrity sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw== - -"@rollup/rollup-linux-s390x-gnu@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz#37a1fd372d9b93d2b75b2f37c482ecf52f52849b" - integrity sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A== - "@rollup/rollup-linux-x64-gnu@4.49.0": version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz#131e66dbf7e71cb2a389acc45319bd4c990e093a" + resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz" integrity sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA== -"@rollup/rollup-linux-x64-musl@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz#b7245a5ea57db9679e8bf3032c25a5d2c5f54056" - integrity sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg== - -"@rollup/rollup-win32-arm64-msvc@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz#768a128bb5da3c5472c3c56aec77507d28bc7209" - integrity sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA== - -"@rollup/rollup-win32-ia32-msvc@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz#ce3f3b2eebe585340631498666718f00983a6a62" - integrity sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA== - -"@rollup/rollup-win32-x64-msvc@4.49.0": - version "4.49.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz#c2a0e3b81262a7e9dd12ce18b350a97558dd50bc" - integrity sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg== - -"@shikijs/core@3.12.0", "@shikijs/core@^3.9.2": +"@shikijs/core@^3.9.2", "@shikijs/core@3.12.0": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.12.0.tgz#b193f7460c22bfdc2f4cd9b28fb2a88cb2a3c1f2" + resolved "https://registry.npmjs.org/@shikijs/core/-/core-3.12.0.tgz" integrity sha512-rPfCBd6gHIKBPpf2hKKWn2ISPSrmRKAFi+bYDjvZHpzs3zlksWvEwaF3Z4jnvW+xHxSRef7qDooIJkY0RpA9EA== dependencies: "@shikijs/types" "3.12.0" @@ -301,7 +81,7 @@ "@shikijs/engine-javascript@3.12.0": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.12.0.tgz#a66b13dbde02cafe32dba95b0e0c4c758bd2779d" + resolved "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.12.0.tgz" integrity sha512-Ni3nm4lnKxyKaDoXQQJYEayX052BL7D0ikU5laHp+ynxPpIF1WIwyhzrMU6WDN7AoAfggVR4Xqx3WN+JTS+BvA== dependencies: "@shikijs/types" "3.12.0" @@ -310,7 +90,7 @@ "@shikijs/engine-oniguruma@3.12.0": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.0.tgz#060ca540c857fd7851a75d1280254392da96015b" + resolved "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.12.0.tgz" integrity sha512-IfDl3oXPbJ/Jr2K8mLeQVpnF+FxjAc7ZPDkgr38uEw/Bg3u638neSrpwqOTnTHXt1aU0Fk1/J+/RBdst1kVqLg== dependencies: "@shikijs/types" "3.12.0" @@ -318,29 +98,29 @@ "@shikijs/langs@3.12.0": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.12.0.tgz#8c3c2035ee15a05566691f28eeb91084edd9ed75" + resolved "https://registry.npmjs.org/@shikijs/langs/-/langs-3.12.0.tgz" integrity sha512-HIca0daEySJ8zuy9bdrtcBPhcYBo8wR1dyHk1vKrOuwDsITtZuQeGhEkcEfWc6IDyTcom7LRFCH6P7ljGSCEiQ== dependencies: "@shikijs/types" "3.12.0" "@shikijs/themes@3.12.0": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.12.0.tgz#704f9bec156b25b873e42474c6dc8472ad96f246" + resolved "https://registry.npmjs.org/@shikijs/themes/-/themes-3.12.0.tgz" integrity sha512-/lxvQxSI5s4qZLV/AuFaA4Wt61t/0Oka/P9Lmpr1UV+HydNCczO3DMHOC/CsXCCpbv4Zq8sMD0cDa7mvaVoj0Q== dependencies: "@shikijs/types" "3.12.0" "@shikijs/transformers@^3.9.2": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-3.12.0.tgz#1f91f3c504f3d5110996690d58be72ec377a46e6" + resolved "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.12.0.tgz" integrity sha512-HcJwlvMAyZzOY+ayEAGE891BdJ7Vtio+qdWUTF9ki4d0LIkDb6DBz8ynOWGAEglHv6eQs/WcAWf/h6ina6IgCw== dependencies: "@shikijs/core" "3.12.0" "@shikijs/types" "3.12.0" -"@shikijs/types@3.12.0", "@shikijs/types@^3.9.2": +"@shikijs/types@^3.9.2", "@shikijs/types@3.12.0": version "3.12.0" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.12.0.tgz#3d5f30acb6a3e6a72a1089fa8ca4764fc9f61336" + resolved "https://registry.npmjs.org/@shikijs/types/-/types-3.12.0.tgz" integrity sha512-jsFzm8hCeTINC3OCmTZdhR9DOl/foJWplH2Px0bTi4m8z59fnsueLsweX82oGcjRQ7mfQAluQYKGoH2VzsWY4A== dependencies: "@shikijs/vscode-textmate" "^10.0.2" @@ -348,29 +128,29 @@ "@shikijs/vscode-textmate@^10.0.2": version "10.0.2" - resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224" + resolved "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz" integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg== "@types/estree@1.0.8": version "1.0.8" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== "@types/hast@^3.0.0", "@types/hast@^3.0.4": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== dependencies: "@types/unist" "*" "@types/linkify-it@^5": version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz" integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== "@types/markdown-it@^14.1.2": version "14.1.2" - resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz" integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== dependencies: "@types/linkify-it" "^5" @@ -378,41 +158,41 @@ "@types/mdast@^4.0.0": version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz" integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== dependencies: "@types/unist" "*" "@types/mdurl@^2": version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz" integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/unist@*", "@types/unist@^3.0.0": version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== "@types/web-bluetooth@^0.0.21": version "0.0.21" - resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63" + resolved "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz" integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA== "@ungap/structured-clone@^1.0.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== "@vitejs/plugin-vue@^6.0.1": version "6.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz#4c7f559621af104a22255c6ace5626e6d8349689" + resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz" integrity sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw== dependencies: "@rolldown/pluginutils" "1.0.0-beta.29" "@vue/compiler-core@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.20.tgz#ea100646460703c98117b88900aab4aa7e6f797e" + resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.20.tgz" integrity sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg== dependencies: "@babel/parser" "^7.28.3" @@ -423,7 +203,7 @@ "@vue/compiler-dom@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz#7eb0d4b761a48b93723cf264d27c1385b90dae6d" + resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz" integrity sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ== dependencies: "@vue/compiler-core" "3.5.20" @@ -431,7 +211,7 @@ "@vue/compiler-sfc@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz#7bf92fc65951fd888076f1c71128dda4507a9328" + resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz" integrity sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw== dependencies: "@babel/parser" "^7.28.3" @@ -446,7 +226,7 @@ "@vue/compiler-ssr@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz#6cdae8662bf36974ffb4fe894ba08192f89d5660" + resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz" integrity sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA== dependencies: "@vue/compiler-dom" "3.5.20" @@ -454,14 +234,14 @@ "@vue/devtools-api@^8.0.0": version "8.0.1" - resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-8.0.1.tgz#142f94161ec80698f57ee8379bffad969868b251" + resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.1.tgz" integrity sha512-YBvjfpM7LEp5+b7ZDm4+mFrC+TgGjUmN8ff9lZcbHQ1MKhmftT/urCTZP0y1j26YQWr25l9TPaEbNLbILRiGoQ== dependencies: "@vue/devtools-kit" "^8.0.1" "@vue/devtools-kit@^8.0.1": version "8.0.1" - resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-8.0.1.tgz#84b8741bbd16fa7d6d5ce0baf3c2e8b3822237c2" + resolved "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.1.tgz" integrity sha512-7kiPhgTKNtNeXltEHnJJjIDlndlJP4P+UJvCw54uVHNDlI6JzwrSiRmW4cxKTug2wDbc/dkGaMnlZghcwV+aWA== dependencies: "@vue/devtools-shared" "^8.0.1" @@ -474,21 +254,21 @@ "@vue/devtools-shared@^8.0.1": version "8.0.1" - resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-8.0.1.tgz#ace79d39fec42f35e02d1053d5c83c4f492423a9" + resolved "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.1.tgz" integrity sha512-PqtWqPPRpMwZ9FjTzyugb5KeV9kmg2C3hjxZHwjl0lijT4QIJDd0z6AWcnbM9w2nayjDymyTt0+sbdTv3pVeNg== dependencies: rfdc "^1.4.1" "@vue/reactivity@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.20.tgz#95b959380af1f49780247686467e8858641209bc" + resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.20.tgz" integrity sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ== dependencies: "@vue/shared" "3.5.20" "@vue/runtime-core@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.20.tgz#298eb83fc08887e21adf098a8c6ffeaa9e24c867" + resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.20.tgz" integrity sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw== dependencies: "@vue/reactivity" "3.5.20" @@ -496,7 +276,7 @@ "@vue/runtime-dom@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz#bbeb73b51745bf4065d3d42c9a2f18266c3269b7" + resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz" integrity sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw== dependencies: "@vue/reactivity" "3.5.20" @@ -506,20 +286,20 @@ "@vue/server-renderer@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.20.tgz#6e075ef0386d099e8ec09c4662b6e8af1c1a086f" + resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.20.tgz" integrity sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg== dependencies: "@vue/compiler-ssr" "3.5.20" "@vue/shared" "3.5.20" -"@vue/shared@3.5.20", "@vue/shared@^3.5.18": +"@vue/shared@^3.5.18", "@vue/shared@3.5.20": version "3.5.20" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.20.tgz#8740b370738c8c7e29e02fa9051cfe6d20114cb4" + resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.5.20.tgz" integrity sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA== -"@vueuse/core@13.8.0", "@vueuse/core@^13.6.0": +"@vueuse/core@^13.6.0", "@vueuse/core@13.8.0": version "13.8.0" - resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-13.8.0.tgz#760800f866dd8a1152a0a28e412ed53336e9ad7e" + resolved "https://registry.npmjs.org/@vueuse/core/-/core-13.8.0.tgz" integrity sha512-rmBcgpEpxY0ZmyQQR94q1qkUcHREiLxQwNyWrtjMDipD0WTH/JBcAt0gdcn2PsH0SA76ec291cHFngmyaBhlxA== dependencies: "@types/web-bluetooth" "^0.0.21" @@ -528,7 +308,7 @@ "@vueuse/integrations@^13.6.0": version "13.8.0" - resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-13.8.0.tgz#4ed1e7ce5a4691f171e53f7430f51d67c4976fc6" + resolved "https://registry.npmjs.org/@vueuse/integrations/-/integrations-13.8.0.tgz" integrity sha512-64mD5Q7heVkr8JsqBFDh9xnQJrPLmWJghy8Qtj9UeLosQL9n+JYTcS7d+eNsEVwuvZvxfF7hUSi87jABm/eYpw== dependencies: "@vueuse/core" "13.8.0" @@ -536,71 +316,71 @@ "@vueuse/metadata@13.8.0": version "13.8.0" - resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-13.8.0.tgz#5bf97b8733346fc1abf0c20c31e01ea672279c0e" + resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.8.0.tgz" integrity sha512-BYMp3Gp1kBUPv7AfQnJYP96mkX7g7cKdTIgwv/Jgd+pfQhz678naoZOAcknRtPLP4cFblDDW7rF4e3KFa+PfIA== "@vueuse/shared@13.8.0": version "13.8.0" - resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-13.8.0.tgz#ed7baa8ad19e164a7626592531694bfe6c94d2e3" + resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-13.8.0.tgz" integrity sha512-x4nfM0ykW+RmNJ4/1IzZsuLuWWrNTxlTWUiehTGI54wnOxIgI9EDdu/O5S77ac6hvQ3hk2KpOVFHaM0M796Kbw== birpc@^2.5.0: version "2.5.0" - resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.5.0.tgz#3a014e54c17eceba0ce15738d484ea371dbf6527" + resolved "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz" integrity sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ== ccount@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== character-entities-html4@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== character-entities-legacy@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz" integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== comma-separated-tokens@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== copy-anything@^3.0.2: version "3.0.5" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" + resolved "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz" integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== dependencies: is-what "^4.1.8" csstype@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== dequal@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== devlop@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz" integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== dependencies: dequal "^2.0.0" entities@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== esbuild@^0.25.0: version "0.25.9" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.9.tgz#15ab8e39ae6cdc64c24ff8a2c0aef5b3fd9fa976" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz" integrity sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g== optionalDependencies: "@esbuild/aix-ppc64" "0.25.9" @@ -632,29 +412,24 @@ esbuild@^0.25.0: estree-walker@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== fdir@^6.4.4, fdir@^6.5.0: version "6.5.0" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== -focus-trap@^7.6.5: +focus-trap@^7, focus-trap@^7.6.5: version "7.6.5" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.6.5.tgz#56f0814286d43c1a2688e9bc4f31f17ae047fb76" + resolved "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz" integrity sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg== dependencies: tabbable "^6.2.0" -fsevents@~2.3.2, fsevents@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - hast-util-to-html@^9.0.5: version "9.0.5" - resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz#ccc673a55bb8e85775b08ac28380f72d47167005" + resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz" integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw== dependencies: "@types/hast" "^3.0.0" @@ -671,41 +446,41 @@ hast-util-to-html@^9.0.5: hast-util-whitespace@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== dependencies: "@types/hast" "^3.0.0" hookable@^5.5.3: version "5.5.3" - resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" + resolved "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz" integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== html-void-elements@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== is-what@^4.1.8: version "4.1.16" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + resolved "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz" integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== magic-string@^0.30.17: version "0.30.18" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.18.tgz#905bfbbc6aa5692703a93db26a9edcaa0007d2bb" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz" integrity sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ== dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" mark.js@8.11.1: version "8.11.1" - resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" + resolved "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz" integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== mdast-util-to-hast@^13.0.0: version "13.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz" integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== dependencies: "@types/hast" "^3.0.0" @@ -720,7 +495,7 @@ mdast-util-to-hast@^13.0.0: micromark-util-character@^2.0.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz" integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== dependencies: micromark-util-symbol "^2.0.0" @@ -728,12 +503,12 @@ micromark-util-character@^2.0.0: micromark-util-encode@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz" integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== micromark-util-sanitize-uri@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz" integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== dependencies: micromark-util-character "^2.0.0" @@ -742,37 +517,37 @@ micromark-util-sanitize-uri@^2.0.0: micromark-util-symbol@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz" integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== micromark-util-types@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz" integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== minisearch@^7.1.2: version "7.1.2" - resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.1.2.tgz#296ee8d1906cc378f7e57a3a71f07e5205a75df5" + resolved "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz" integrity sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA== mitt@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== nanoid@^3.3.11: version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== oniguruma-parser@^0.12.1: version "0.12.1" - resolved "https://registry.yarnpkg.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz#82ba2208d7a2b69ee344b7efe0ae930c627dcc4a" + resolved "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz" integrity sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w== oniguruma-to-es@^4.3.3: version "4.3.3" - resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz#50db2c1e28ec365e102c1863dfd3d1d1ad18613e" + resolved "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz" integrity sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg== dependencies: oniguruma-parser "^0.12.1" @@ -781,22 +556,22 @@ oniguruma-to-es@^4.3.3: perfect-debounce@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + resolved "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz" integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^4.0.2, picomatch@^4.0.3: +"picomatch@^3 || ^4", picomatch@^4.0.2, picomatch@^4.0.3: version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -postcss@^8.5.6: +postcss@^8, postcss@^8.5.6: version "8.5.6" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" @@ -805,36 +580,36 @@ postcss@^8.5.6: property-information@^7.0.0: version "7.1.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz" integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== regex-recursion@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/regex-recursion/-/regex-recursion-6.0.2.tgz#a0b1977a74c87f073377b938dbedfab2ea582b33" + resolved "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz" integrity sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg== dependencies: regex-utilities "^2.3.0" regex-utilities@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/regex-utilities/-/regex-utilities-2.3.0.tgz#87163512a15dce2908cf079c8960d5158ff43280" + resolved "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz" integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== regex@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/regex/-/regex-6.0.1.tgz#282fa4435d0c700b09c0eb0982b602e05ab6a34f" + resolved "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz" integrity sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA== dependencies: regex-utilities "^2.3.0" rfdc@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rollup@^4.43.0: version "4.49.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.49.0.tgz#9751ad9d06a47a4496c3c5c238b27b1422c8b0eb" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz" integrity sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA== dependencies: "@types/estree" "1.0.8" @@ -863,7 +638,7 @@ rollup@^4.43.0: shiki@^3.9.2: version "3.12.0" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-3.12.0.tgz#a0668393d04c02402522abcd221ad3a0774b2d44" + resolved "https://registry.npmjs.org/shiki/-/shiki-3.12.0.tgz" integrity sha512-E+ke51tciraTHpaXYXfqnPZFSViKHhSQ3fiugThlfs/om/EonlQ0hSldcqgzOWWqX6PcjkKKzFgrjIaiPAXoaA== dependencies: "@shikijs/core" "3.12.0" @@ -877,22 +652,22 @@ shiki@^3.9.2: source-map-js@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== space-separated-tokens@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== speakingurl@^14.0.1: version "14.0.1" - resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + resolved "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz" integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== stringify-entities@^4.0.0: version "4.0.4" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== dependencies: character-entities-html4 "^2.0.0" @@ -900,19 +675,19 @@ stringify-entities@^4.0.0: superjson@^2.2.2: version "2.2.2" - resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173" + resolved "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz" integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q== dependencies: copy-anything "^3.0.2" tabbable@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== tinyglobby@^0.2.14: version "0.2.14" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz" integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== dependencies: fdir "^6.4.4" @@ -920,33 +695,33 @@ tinyglobby@^0.2.14: trim-lines@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz" integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== unist-util-is@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== dependencies: "@types/unist" "^3.0.0" unist-util-position@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== dependencies: "@types/unist" "^3.0.0" unist-util-stringify-position@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== dependencies: "@types/unist" "^3.0.0" unist-util-visit-parents@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== dependencies: "@types/unist" "^3.0.0" @@ -954,7 +729,7 @@ unist-util-visit-parents@^6.0.0: unist-util-visit@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz" integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== dependencies: "@types/unist" "^3.0.0" @@ -963,7 +738,7 @@ unist-util-visit@^5.0.0: vfile-message@^4.0.0: version "4.0.3" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz" integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== dependencies: "@types/unist" "^3.0.0" @@ -971,15 +746,15 @@ vfile-message@^4.0.0: vfile@^6.0.0: version "6.0.3" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz" integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== dependencies: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -vite@^7.1.2: +"vite@^5.0.0 || ^6.0.0 || ^7.0.0", vite@^7.1.2: version "7.1.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.3.tgz#8d70cb02fd6346b4bf1329a6760800538ef0faea" + resolved "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz" integrity sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw== dependencies: esbuild "^0.25.0" @@ -993,7 +768,7 @@ vite@^7.1.2: vitepress@^2.0.0-alpha.5: version "2.0.0-alpha.12" - resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-2.0.0-alpha.12.tgz#e75648eec6c43bff1d669f9a7f81f777acc6e4fd" + resolved "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.12.tgz" integrity sha512-yZwCwRRepcpN5QeAhwSnEJxS3I6zJcVixqL1dnm6km4cnriLpQyy2sXQDsE5Ti3pxGPbhU51nTMwI+XC1KNnJg== dependencies: "@docsearch/css" "^4.0.0-beta.7" @@ -1015,9 +790,9 @@ vitepress@^2.0.0-alpha.5: vite "^7.1.2" vue "^3.5.18" -vue@^3.2.47, vue@^3.5.18: +vue@^3.2.25, vue@^3.2.47, vue@^3.5.0, vue@^3.5.18, vue@3.5.20: version "3.5.20" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.20.tgz#855c3f4c0a1260abc894f729c3ffb6cb687d0d34" + resolved "https://registry.npmjs.org/vue/-/vue-3.5.20.tgz" integrity sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw== dependencies: "@vue/compiler-dom" "3.5.20" @@ -1028,5 +803,5 @@ vue@^3.2.47, vue@^3.5.18: zwitch@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz" integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== From 2045055591cdd7e19618a0b9e81b7d1c2e42e944 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 19 Apr 2026 18:01:56 +0800 Subject: [PATCH 653/682] Add GUIDE section for v3 docs --- .gitignore | 1 + docs/.vitepress/components/CliGenerator.vue | 583 ++++++------------ docs/.vitepress/components/SearchTable.vue | 116 ++-- docs/.vitepress/config.ts | 1 + docs/.vitepress/extensions.data.js | 40 ++ docs/.vitepress/sidebar.en.ts | 1 + docs/.vitepress/sidebar.zh.ts | 1 + docs/en/faq/index.md | 117 +++- docs/en/guide/cli-generator.md | 6 +- docs/en/guide/cli-reference.md | 213 ++++++- docs/en/guide/env-vars.md | 56 +- docs/en/guide/extension-notes.md | 168 ++++- docs/en/guide/extensions.md | 16 +- docs/en/guide/first-build.md | 37 +- docs/en/guide/index.md | 4 +- docs/en/guide/sapi-reference.md | 277 +++++++++ docs/en/guide/troubleshooting.md | 44 +- docs/zh/faq/index.md | 101 ++- docs/zh/guide/cli-generator.md | 6 +- docs/zh/guide/cli-reference.md | 214 ++++++- docs/zh/guide/env-vars.md | 51 +- docs/zh/guide/extension-notes.md | 158 ++++- docs/zh/guide/extensions.md | 16 +- docs/zh/guide/first-build.md | 17 +- docs/zh/guide/index.md | 6 +- docs/zh/guide/sapi-reference.md | 277 +++++++++ docs/zh/guide/troubleshooting.md | 33 +- src/Package/Target/php.php | 25 +- .../Command/Dev/GenExtDocsCommand.php | 87 +++ src/StaticPHP/Command/SPCConfigCommand.php | 2 +- src/StaticPHP/ConsoleApplication.php | 2 + 31 files changed, 2157 insertions(+), 519 deletions(-) create mode 100644 docs/.vitepress/extensions.data.js create mode 100644 docs/en/guide/sapi-reference.md create mode 100644 docs/zh/guide/sapi-reference.md create mode 100644 src/StaticPHP/Command/Dev/GenExtDocsCommand.php diff --git a/.gitignore b/.gitignore index 810af82ce..f804c264a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ packlib_files.txt /node_modules/ /docs/.vitepress/dist/ /docs/.vitepress/cache/ +/docs/.vitepress/ext-data.json package-lock.json pnpm-lock.yaml diff --git a/docs/.vitepress/components/CliGenerator.vue b/docs/.vitepress/components/CliGenerator.vue index 378f6eaed..14590d64c 100644 --- a/docs/.vitepress/components/CliGenerator.vue +++ b/docs/.vitepress/components/CliGenerator.vue @@ -1,9 +1,14 @@