From 90846ab1498186ef76596ea48048d685e1df0d5c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 15:08:43 +0800 Subject: [PATCH 01/10] refactor: restructure tool target configs into tool package system - Move tool configs from config/pkg/target/ to config/pkg/tool/ - Add ToolVersionRegistry for tracking installed tool versions - Delete old Target/pkgconfig.php, move to tool config - Rename: 7za-win, go-win, go-xcaddy, jom, nasm, protoc, strawberry-perl, upx, vswhere, zig, pkg-config --- config/pkg/target/pkg-config.yml | 9 --- config/pkg/target/zig.yml | 4 - config/pkg/{target => tool}/7za-win.yml | 6 +- config/pkg/{target => tool}/go-win.yml | 7 +- config/pkg/{target => tool}/go-xcaddy.yml | 9 ++- config/pkg/{target => tool}/jom.yml | 6 +- config/pkg/{target => tool}/nasm.yml | 7 +- config/pkg/{target => tool}/protoc.yml | 6 +- .../pkg/{target => tool}/strawberry-perl.yml | 6 +- config/pkg/{target => tool}/upx.yml | 8 +- config/pkg/{target => tool}/vswhere.yml | 6 +- config/pkg/tool/zig.yml | 14 ++++ src/Package/Target/pkgconfig.php | 45 ----------- src/StaticPHP/Package/ToolVersionRegistry.php | 79 +++++++++++++++++++ 14 files changed, 143 insertions(+), 69 deletions(-) delete mode 100644 config/pkg/target/pkg-config.yml delete mode 100644 config/pkg/target/zig.yml rename config/pkg/{target => tool}/7za-win.yml (69%) rename config/pkg/{target => tool}/go-win.yml (68%) rename config/pkg/{target => tool}/go-xcaddy.yml (70%) rename config/pkg/{target => tool}/jom.yml (73%) rename config/pkg/{target => tool}/nasm.yml (72%) rename config/pkg/{target => tool}/protoc.yml (82%) rename config/pkg/{target => tool}/strawberry-perl.yml (72%) rename config/pkg/{target => tool}/upx.yml (79%) rename config/pkg/{target => tool}/vswhere.yml (71%) create mode 100644 config/pkg/tool/zig.yml delete mode 100644 src/Package/Target/pkgconfig.php create mode 100644 src/StaticPHP/Package/ToolVersionRegistry.php diff --git a/config/pkg/target/pkg-config.yml b/config/pkg/target/pkg-config.yml deleted file mode 100644 index 9b6d1603..00000000 --- a/config/pkg/target/pkg-config.yml +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index dda72b55..00000000 --- a/config/pkg/target/zig.yml +++ /dev/null @@ -1,4 +0,0 @@ -zig: - type: target - artifact: - binary: custom diff --git a/config/pkg/target/7za-win.yml b/config/pkg/tool/7za-win.yml similarity index 69% rename from config/pkg/target/7za-win.yml rename to config/pkg/tool/7za-win.yml index 351fad8c..c8d35bfe 100644 --- a/config/pkg/target/7za-win.yml +++ b/config/pkg/tool/7za-win.yml @@ -1,5 +1,9 @@ 7za-win: - type: target + type: tool artifact: binary: windows-x86_64: { type: url, url: 'https://dl.static-php.dev/v3/tools/7zip/7za.exe', extract: '{pkg_root_path}/bin/7za.exe' } + tool: + provides: + - 7za.exe + binary-subdir: bin diff --git a/config/pkg/target/go-win.yml b/config/pkg/tool/go-win.yml similarity index 68% rename from config/pkg/target/go-win.yml rename to config/pkg/tool/go-win.yml index 46c13f5f..a65b6f5a 100644 --- a/config/pkg/target/go-win.yml +++ b/config/pkg/tool/go-win.yml @@ -1,5 +1,5 @@ go-win: - type: target + type: tool artifact: binary: custom env: @@ -8,3 +8,8 @@ go-win: GOPATH: '{pkg_root_path}/go-win/go' path@windows: - '{pkg_root_path}/go-win/bin' + tool: + provides: + - go.exe + - gofmt.exe + binary-subdir: go-win/bin diff --git a/config/pkg/target/go-xcaddy.yml b/config/pkg/tool/go-xcaddy.yml similarity index 70% rename from config/pkg/target/go-xcaddy.yml rename to config/pkg/tool/go-xcaddy.yml index deafb37d..19c723a9 100644 --- a/config/pkg/target/go-xcaddy.yml +++ b/config/pkg/tool/go-xcaddy.yml @@ -1,5 +1,5 @@ go-xcaddy: - type: target + type: tool artifact: binary: custom env: @@ -8,5 +8,8 @@ go-xcaddy: GOPATH: '{pkg_root_path}/go-xcaddy/go' path@unix: - '{pkg_root_path}/go-xcaddy/bin' - static-bins: - - xcaddy + tool: + provides: + - go + - gofmt + binary-subdir: go-xcaddy/bin diff --git a/config/pkg/target/jom.yml b/config/pkg/tool/jom.yml similarity index 73% rename from config/pkg/target/jom.yml rename to config/pkg/tool/jom.yml index 70916bfd..fa98fcca 100644 --- a/config/pkg/target/jom.yml +++ b/config/pkg/tool/jom.yml @@ -1,7 +1,11 @@ jom: - type: target + type: tool 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' + tool: + provides: + - jom.exe + binary-subdir: jom diff --git a/config/pkg/target/nasm.yml b/config/pkg/tool/nasm.yml similarity index 72% rename from config/pkg/target/nasm.yml rename to config/pkg/tool/nasm.yml index 3e6d9e43..928adc2b 100644 --- a/config/pkg/target/nasm.yml +++ b/config/pkg/tool/nasm.yml @@ -1,5 +1,10 @@ nasm: - type: target + type: tool 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: '{pkg_root_path}/bin/nasm.exe', ndisasm.exe: '{pkg_root_path}/bin/ndisasm.exe' } } + tool: + provides: + - nasm.exe + - ndisasm.exe + binary-subdir: bin diff --git a/config/pkg/target/protoc.yml b/config/pkg/tool/protoc.yml similarity index 82% rename from config/pkg/target/protoc.yml rename to config/pkg/tool/protoc.yml index b45fb335..596dad19 100644 --- a/config/pkg/target/protoc.yml +++ b/config/pkg/tool/protoc.yml @@ -1,8 +1,12 @@ protoc: - type: target + type: tool 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' + tool: + provides: + - protoc + binary-subdir: protoc/bin diff --git a/config/pkg/target/strawberry-perl.yml b/config/pkg/tool/strawberry-perl.yml similarity index 72% rename from config/pkg/target/strawberry-perl.yml rename to config/pkg/tool/strawberry-perl.yml index 9e2e3187..d3587b4e 100644 --- a/config/pkg/target/strawberry-perl.yml +++ b/config/pkg/tool/strawberry-perl.yml @@ -1,5 +1,9 @@ strawberry-perl: - type: target + type: tool 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' } + tool: + provides: + - perl.exe + binary-subdir: strawberry-perl/perl/bin diff --git a/config/pkg/target/upx.yml b/config/pkg/tool/upx.yml similarity index 79% rename from config/pkg/target/upx.yml rename to config/pkg/tool/upx.yml index 29506c39..9530b1eb 100644 --- a/config/pkg/target/upx.yml +++ b/config/pkg/tool/upx.yml @@ -1,7 +1,13 @@ upx: - type: target + type: tool 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' } } + tool: + provides: + - upx + provides@windows: + - upx.exe + binary-subdir: bin diff --git a/config/pkg/target/vswhere.yml b/config/pkg/tool/vswhere.yml similarity index 71% rename from config/pkg/target/vswhere.yml rename to config/pkg/tool/vswhere.yml index d7f8670c..f4253376 100644 --- a/config/pkg/target/vswhere.yml +++ b/config/pkg/tool/vswhere.yml @@ -1,5 +1,9 @@ vswhere: - type: target + type: tool 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' } + tool: + provides: + - vswhere.exe + binary-subdir: bin diff --git a/config/pkg/tool/zig.yml b/config/pkg/tool/zig.yml new file mode 100644 index 00000000..0f243ebf --- /dev/null +++ b/config/pkg/tool/zig.yml @@ -0,0 +1,14 @@ +zig: + type: tool + artifact: + binary: custom + tool: + provides: + - zig + - zig-cc + - zig-c++ + - zig-ar + - zig-ld.lld + - zig-ranlib + - zig-objcopy + binary-subdir: zig diff --git a/src/Package/Target/pkgconfig.php b/src/Package/Target/pkgconfig.php deleted file mode 100644 index aa75fc88..00000000 --- a/src/Package/Target/pkgconfig.php +++ /dev/null @@ -1,45 +0,0 @@ -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 {$package->getBinDir()}/pkg-config"); - } -} diff --git a/src/StaticPHP/Package/ToolVersionRegistry.php b/src/StaticPHP/Package/ToolVersionRegistry.php new file mode 100644 index 00000000..98564db5 --- /dev/null +++ b/src/StaticPHP/Package/ToolVersionRegistry.php @@ -0,0 +1,79 @@ + */ + private static ?array $data = null; + + /** + * Get the recorded installed version for a tool, or null if never recorded (not a tool + * package, not installed yet, or the artifact doesn't expose a version). + */ + public static function get(string $tool_name): ?string + { + self::load(); + return self::$data[$tool_name]['version'] ?? null; + } + + /** + * Record the version currently installed for a tool. Called after a tool package's binary + * has been (re-)installed, regardless of whether extraction actually ran (keeps the registry + * self-healing if it was deleted separately from the installed files). + */ + public static function record(string $tool_name, ?string $version): void + { + self::load(); + self::$data[$tool_name] = [ + 'version' => $version, + 'platform' => SystemTarget::getCurrentPlatformString(), + 'installed_at' => date('c'), + ]; + self::save(); + } + + private static function getPath(): string + { + return PKG_ROOT_PATH . '/.spc-tool-versions.json'; + } + + private static function load(): void + { + if (self::$data !== null) { + return; + } + $path = self::getPath(); + if (!file_exists($path)) { + self::$data = []; + return; + } + $content = file_get_contents($path); + self::$data = is_string($content) ? (json_decode($content, true) ?: []) : []; + } + + private static function save(): void + { + $path = self::getPath(); + $dir = dirname($path); + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + } + file_put_contents($path, json_encode(self::$data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } +} From 1bdbdfc08c6d891a47fe46315baef6287e8c3e11 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 15:10:08 +0800 Subject: [PATCH 02/10] refactor: move getLibExtra* to Package base, add ToolPackage support to executors and builders - Move getLibExtraCFlags/getLibExtraCxxFlags/getLibExtraLdFlags/getLibExtraLibs from LibraryPackage to Package base class so ToolPackage can also use them - Full ToolPackage implementation: getBuildRootPath, getIncludeDir, getLibDir, getBinDir, getToolField with platform suffix support - Update PackageBuilder to support building ToolPackage (not just LibraryPackage) - Update PackageInstaller: support ToolPackage in download/extract/install pipeline, auto-install build-time tools, record installed versions - Allow LibraryPackage|ToolPackage union type in all Executor classes and UnixShell::initializeEnv --- src/StaticPHP/Package/LibraryPackage.php | 65 ------------ src/StaticPHP/Package/Package.php | 65 ++++++++++++ src/StaticPHP/Package/PackageBuilder.php | 13 ++- src/StaticPHP/Package/PackageInstaller.php | 68 ++++++++++--- src/StaticPHP/Package/ToolPackage.php | 98 +++++++++++++++++-- src/StaticPHP/Runtime/Executor/Executor.php | 5 +- .../Runtime/Executor/UnixAutoconfExecutor.php | 3 +- .../Runtime/Executor/UnixCMakeExecutor.php | 3 +- .../Runtime/Executor/WindowsCMakeExecutor.php | 3 +- src/StaticPHP/Runtime/Shell/UnixShell.php | 5 +- 10 files changed, 235 insertions(+), 93 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 42f60d79..cd8c626e 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -113,60 +113,6 @@ class LibraryPackage extends Package } } - /** - * 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_CFLAGS') ?: ''; - if (!empty(getenv('SPC_DEFAULT_CFLAGS')) && !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_CXXFLAGS') ?: ''; - if (!empty(getenv('SPC_DEFAULT_CXXFLAGS')) && !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_LDFLAGS') ?: ''; - if (!empty(getenv('SPC_DEFAULT_LDFLAGS')) && !str_contains($env, $arch_ld_flags)) { - $env .= ' ' . $arch_ld_flags; - } - return trim($env); - } - /** * Patch pkgconfig file prefix, exec_prefix, libdir, includedir for correct build. * @@ -364,17 +310,6 @@ class LibraryPackage extends Package return $res['libs']; } - /** - * 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 tar compress options from suffix * diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index b3f8ccb6..ba53f81d 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -284,6 +284,71 @@ abstract class Package return $this->getArtifact()?->hasPlatformBinary() ?? false; } + /** + * Get extra CFLAGS for current package. + * You need to define the environment variable in the format of {PACKAGE_NAME}_CFLAGS + * where {PACKAGE_NAME} is the snake_case name of the package. + * 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_CFLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_CFLAGS')) && !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 {PACKAGE_NAME}_CXXFLAGS + * where {PACKAGE_NAME} is the snake_case name of the package. + * 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_CXXFLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_CXXFLAGS')) && !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 {PACKAGE_NAME}_LDFLAGS + * where {PACKAGE_NAME} is the snake_case name of the package. + * 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_LDFLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_LDFLAGS')) && !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 {PACKAGE_NAME}_LIBS + * where {PACKAGE_NAME} is the snake_case name of the package. + * For example, for libjpeg, the environment variable should be libjpeg_LIBS. + */ + public function getLibExtraLibs(): string + { + return getenv($this->getSnakeCaseName() . '_LIBS') ?: ''; + } + /** * Get the snake_case name of the package. */ diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 582fa474..943b3ce5 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace StaticPHP\Package; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; @@ -39,8 +40,8 @@ class PackageBuilder 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.'); + if (!$package instanceof LibraryPackage && !$package instanceof ToolPackage) { + throw new SPCInternalException('Please, never try to build non-library, non-tool packages directly.'); } FileSystem::createDir($package->getBuildRootPath()); FileSystem::createDir($package->getIncludeDir()); @@ -78,6 +79,14 @@ class PackageBuilder $this->installLicense($package, $license); } } + + // Record the installed version for tool packages built from source, so + // `check-update --installed` reflects what's actually on disk (mirrors the + // binary-install recording in PackageInstaller::installBinary()). + if ($package instanceof ToolPackage) { + $source_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($package->getName()); + ToolVersionRegistry::record($package->getName(), $source_info['version'] ?? null); + } } catch (SPCException $e) { // Ensure package information is bound if not already if ($e->getPackageInfo() === null) { diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index a5481583..cf93368d 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -168,9 +168,6 @@ class PackageInstaller // Early validation: check if packages can be built or installed before downloading $this->validatePackagesBeforeBuild(); - // Check that all required tools are installed before proceeding - $this->ensureRequiredTools(); - // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); @@ -178,7 +175,7 @@ class PackageInstaller // 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 + fn ($p) => ($p instanceof LibraryPackage || $p instanceof ToolPackage) && !$this->isBuildPackage($p) && !$p->hasStage('build') && ($p->getArtifact()?->hasPlatformBinary() ?? false) @@ -216,8 +213,8 @@ class PackageInstaller $builder = ApplicationContext::get(PackageBuilder::class); foreach ($this->packages as $package) { $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_build_stage = ($package instanceof LibraryPackage || $package instanceof ToolPackage) && $package->hasStage('build'); + $should_use_binary = ($package instanceof LibraryPackage || $package instanceof ToolPackage) && ($package->getArtifact()?->shouldUseBinary() ?? false); $has_source = $package->hasSource(); if (!$is_to_build && $should_use_binary) { // install binary @@ -280,6 +277,11 @@ class PackageInstaller // perform after-install actions and emit post-install events $this->emitPostInstallEvents(); + + // Final verification: declared build-time tools should have been auto-installed by the + // pipeline above (they were merged into $this->packages in resolvePackages()). This only + // throws if a tool genuinely failed to become available (e.g. no artifact for this platform). + $this->ensureRequiredTools(); } public function isBuildPackage(Package|string $package): bool @@ -357,12 +359,16 @@ class PackageInstaller } // 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. - // Note: TargetPackage extends LibraryPackage, but target packages (e.g. zig) have no + // Note: TargetPackage extends LibraryPackage, but target packages have no // static-libs/headers configured, so isInstalled() would trivially return true for them. // Only apply this fallback to pure library packages. if ($package instanceof LibraryPackage && !($package instanceof TargetPackage)) { return $package->isInstalled(); } + // Tool packages: fall back to checking their provided binaries directly on disk. + if ($package instanceof ToolPackage) { + return $package->isInstalled(); + } return false; } @@ -511,6 +517,16 @@ class PackageInstaller } $status = $extractor->extract($artifact); + + // Record the installed version for tool packages, so `check-update --installed` can + // compare against what's actually on disk instead of only the download cache (which + // only reflects the last download and may be stale or cleared). Recorded regardless of + // $status so the registry self-heals if it was deleted separately from the install dir. + if ($package instanceof ToolPackage) { + $cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($artifact->getName(), SystemTarget::getCurrentPlatformString()); + ToolVersionRegistry::record($artifact->getName(), $cache_info['version'] ?? null); + } + if ($status === SPC_STATUS_ALREADY_EXTRACTED) { return SPC_STATUS_ALREADY_INSTALLED; } @@ -661,8 +677,8 @@ class PackageInstaller */ 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()) { + // target, library and tool packages must have at least source or platform binary + if (in_array($package->getType(), ['library', 'target', 'tool']) && !$package->getArtifact()?->hasSource() && !$package->getArtifact()?->hasPlatformBinary()) { throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or current platform (" . SystemTarget::getCurrentPlatformString() . ') binary defined.'); } } @@ -695,6 +711,32 @@ class PackageInstaller $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name); } + // Merge in build-time tool packages declared via 'tools'/'tools@platform' fields on the + // packages resolved above. Tools are intentionally NOT part of the library dependency graph + // (DependencyResolver), but still need to ride the normal download/extract/install pipeline + // so they get auto-installed like any other package. + // + // Tools are prepended so they install BEFORE any package that declares them — otherwise a + // library that invokes e.g. jom.exe during its build stage would fail because the tool + // hasn't been extracted yet (tools were appended after the dependency graph in insertion + // order, which is also the build-loop iteration order). + $tool_packages = []; + foreach ($this->collectRequiredTools() as $tool_name) { + if (isset($this->packages[$tool_name])) { + continue; + } + try { + $tool = PackageLoader::getPackage($tool_name); + } catch (WrongUsageException) { + continue; // will be reported as missing by ensureRequiredTools() + } + if ($tool instanceof ToolPackage) { + $tool_packages[$tool_name] = $tool; + } + } + // Prepend: tools first, then the dependency-resolved packages. + $this->packages = [...$tool_packages, ...$this->packages]; + foreach ($this->packages as $package) { $this->injectPackageEnvs($package); } @@ -703,8 +745,12 @@ class PackageInstaller /** * Ensure all required tools are installed, throwing if any are missing. * - * Called early in the build pipeline (before download/extract). - * When tools are missing, lists them with install hints. + * Called at the end of run(), after the normal download/extract/build/install pipeline + * has had a chance to auto-install any tool package merged in resolvePackages(). This is + * a final safety net: it only throws if a declared tool still isn't available afterward + * (e.g. no artifact defined for the current platform, or the tool name doesn't resolve to + * a registered ToolPackage). "General" tools not declared via any package's 'tools' field + * (e.g. zig, musl-toolchain) are not covered here and remain purely Doctor-driven. */ private function ensureRequiredTools(): void { diff --git a/src/StaticPHP/Package/ToolPackage.php b/src/StaticPHP/Package/ToolPackage.php index b78afa91..7e0e6ba3 100644 --- a/src/StaticPHP/Package/ToolPackage.php +++ b/src/StaticPHP/Package/ToolPackage.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace StaticPHP\Package; use StaticPHP\Config\PackageConfig; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; -use StaticPHP\Util\GlobalPathTrait; /** * Represents a build-time tool package. @@ -26,6 +26,11 @@ use StaticPHP\Util\GlobalPathTrait; * provides: [nasm.exe, ndisasm.exe] # executables this tool installs * binary-subdir: '' # subdirectory under install root (default: '') * min-version: '2.16' # minimum required version (optional) + * + * Fields nested under 'tool' support the same '@windows'/'@unix'/'@macos'/'@linux' suffix + * overrides as top-level package fields (e.g. 'provides@windows' overrides 'provides' when + * building on Windows), useful when a tool provides differently-named binaries per OS + * (e.g. upx vs upx.exe). * artifact: * binary: * windows-x86_64: @@ -36,7 +41,44 @@ use StaticPHP\Util\GlobalPathTrait; */ class ToolPackage extends Package { - use GlobalPathTrait; + /** + * Get the build root ('--prefix') for a tool that builds from source. + * + * Unlike LibraryPackage (which installs into BUILD_ROOT_PATH), tool packages that build + * from source (e.g. pkg-config, via UnixAutoconfExecutor/UnixCMakeExecutor) install into + * their own install root (PKG_ROOT_PATH by default), consistent with pre-built tool binaries. + */ + public function getBuildRootPath(): string + { + return $this->getInstallRoot(); + } + + /** + * Tool packages don't produce headers for other packages to consume. Kept self-contained + * under the tool's own install root so a from-source build never accidentally picks up + * unrelated headers from BUILD_ROOT_PATH. + */ + public function getIncludeDir(): string + { + return $this->getInstallRoot() . DIRECTORY_SEPARATOR . 'include'; + } + + /** + * Tool packages don't produce libraries for other packages to consume. Kept self-contained + * under the tool's own install root (see getIncludeDir()). + */ + public function getLibDir(): string + { + return $this->getInstallRoot() . DIRECTORY_SEPARATOR . 'lib'; + } + + /** + * Where this tool's own executables live, i.e. {install-root}/{binary-subdir}. + */ + public function getBinDir(): string + { + return $this->getBinaryDir(); + } /** * Get the install root directory for this tool. @@ -50,7 +92,7 @@ class ToolPackage extends Package if ($root = getenv($env_var)) { return $root; } - $config_root = $this->getToolConfig()['install-root'] ?? null; + $config_root = $this->getToolField('install-root'); if ($config_root !== null) { return FileSystem::replacePathVariable((string) $config_root); } @@ -65,7 +107,7 @@ class ToolPackage extends Package */ public function getBinaryDir(): string { - $subdir = $this->getToolConfig()['binary-subdir'] ?? ''; + $subdir = $this->getToolField('binary-subdir') ?? ''; if ($subdir === '') { return $this->getInstallRoot(); } @@ -75,14 +117,15 @@ class ToolPackage extends Package /** * Get the list of executables this tool provides. * - * Reads from YAML 'tool.provides' field. Each entry is a bare filename - * (e.g. 'nasm.exe'), resolved relative to getBinaryDir(). + * Reads from YAML 'tool.provides' field (with '@windows'/'@unix'/'@macos'/'@linux' + * suffix override support). Each entry is a bare filename (e.g. 'nasm.exe'), resolved + * relative to getBinaryDir(). * * @return string[] Bare executable names (not full paths) */ public function getProvides(): array { - return $this->getToolConfig()['provides'] ?? []; + return $this->getToolField('provides') ?? []; } /** @@ -112,6 +155,20 @@ class ToolPackage extends Package return array_all($this->getProvides(), fn ($binary) => file_exists($this->getBinary($binary))); } + /** + * Get the version currently installed on disk, as recorded by ToolVersionRegistry when this + * tool's binary was last (re-)installed via PackageInstaller::installBinary(). + * + * Returns null if the tool was never installed through the package installer (e.g. installed + * manually, or not installed at all), or if its artifact doesn't expose a version string. + * This reflects what's actually on disk, unlike the download cache which only reflects the + * last download and may be stale or cleared. + */ + public function getInstalledVersion(): ?string + { + return ToolVersionRegistry::get($this->name); + } + /** * Get the minimum required version for this tool, if specified. * @@ -119,7 +176,7 @@ class ToolPackage extends Package */ public function getMinVersion(): ?string { - $version = $this->getToolConfig()['min-version'] ?? null; + $version = $this->getToolField('min-version'); return $version !== null ? (string) $version : null; } @@ -148,4 +205,29 @@ class ToolPackage extends Package } return $config['tool']; } + + /** + * Get a field from the nested 'tool' config block, honoring the same + * '@windows'/'@unix'/'@macos'/'@linux'/'@bsd'/'@freebsd' suffix override priority + * that PackageConfig::get() applies to top-level fields. This lets a tool declare a + * per-OS override, e.g. 'provides' + 'provides@windows', without needing platform- + * specific package config files. + */ + private function getToolField(string $field): mixed + { + $tool = $this->getToolConfig(); + $suffixes = match (SystemTarget::getTargetOS()) { + 'Windows' => ['@windows', ''], + 'Darwin' => ['@macos', '@unix', ''], + 'Linux' => ['@linux', '@unix', ''], + 'BSD' => ['@freebsd', '@bsd', '@unix', ''], + }; + foreach ($suffixes as $suffix) { + $key = "{$field}{$suffix}"; + if (isset($tool[$key])) { + return $tool[$key]; + } + } + return null; + } } diff --git a/src/StaticPHP/Runtime/Executor/Executor.php b/src/StaticPHP/Runtime/Executor/Executor.php index a57e11e0..804b680f 100644 --- a/src/StaticPHP/Runtime/Executor/Executor.php +++ b/src/StaticPHP/Runtime/Executor/Executor.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace StaticPHP\Runtime\Executor; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\ToolPackage; abstract class Executor { - public function __construct(protected LibraryPackage $package) {} + public function __construct(protected LibraryPackage|ToolPackage $package) {} - public static function create(LibraryPackage $package): static + public static function create(LibraryPackage|ToolPackage $package): static { return new static($package); } diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index 2cce15c5..84d3ca07 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -10,6 +10,7 @@ use StaticPHP\Exception\SPCInternalException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\ToolPackage; use StaticPHP\Runtime\Shell\UnixShell; use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; @@ -22,7 +23,7 @@ class UnixAutoconfExecutor extends Executor protected PackageInstaller $installer; - public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null) + public function __construct(protected LibraryPackage|ToolPackage $package, ?PackageInstaller $installer = null) { parent::__construct($package); if ($installer !== null) { diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index d8ff5148..6d5e6343 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -11,6 +11,7 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\TargetPackage; +use StaticPHP\Package\ToolPackage; use StaticPHP\Runtime\Shell\UnixShell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; @@ -40,7 +41,7 @@ class UnixCMakeExecutor extends Executor protected PackageInstaller $installer; - public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null) + public function __construct(protected LibraryPackage|ToolPackage $package, ?PackageInstaller $installer = null) { parent::__construct($package); if ($installer !== null) { diff --git a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php index f79afb41..6ad51272 100644 --- a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php @@ -9,6 +9,7 @@ use StaticPHP\Exception\SPCInternalException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\ToolPackage; use StaticPHP\Runtime\Shell\WindowsCmd; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; @@ -35,7 +36,7 @@ class WindowsCMakeExecutor extends Executor protected PackageInstaller $installer; - public function __construct(protected LibraryPackage $package) + public function __construct(protected LibraryPackage|ToolPackage $package) { parent::__construct($this->package); $this->builder = ApplicationContext::get(PackageBuilder::class); diff --git a/src/StaticPHP/Runtime/Shell/UnixShell.php b/src/StaticPHP/Runtime/Shell/UnixShell.php index 7d74f65f..8ecad042 100644 --- a/src/StaticPHP/Runtime/Shell/UnixShell.php +++ b/src/StaticPHP/Runtime/Shell/UnixShell.php @@ -6,6 +6,7 @@ namespace StaticPHP\Runtime\Shell; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\ToolPackage; use StaticPHP\Runtime\SystemTarget; use ZM\Logger\ConsoleColor; @@ -40,9 +41,9 @@ class UnixShell extends Shell /** * Init the environment variable that common build will be used. * - * @param LibraryPackage $library Library package + * @param LibraryPackage|ToolPackage $library Library or tool package */ - public function initializeEnv(LibraryPackage $library): UnixShell + public function initializeEnv(LibraryPackage|ToolPackage $library): UnixShell { $this->setEnv([ 'CFLAGS' => $library->getLibExtraCFlags(), From dd1e70a394abc7356b71db50d28a03fc155e5a7b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 15:10:22 +0800 Subject: [PATCH 03/10] feat: declare tool dependencies in package configs via tools field - Add 'tools' field to ConfigValidator for build-time tool declarations - Move jom and strawberry-perl from depends@windows to tools@windows in openssl.yml - Move go-xcaddy/go-win from depends to tools in frankenphp.yml - Register config/pkg/tool/ path in spc.registry.yml - Use addInstallPackage for upx tool in php.php (ToolPackage instead of library dep) - Include 'tool' type in DumpCapabilitiesCommand and InstallPackageCommand package lists --- config/pkg/lib/openssl.yml | 4 +++- config/pkg/target/frankenphp.yml | 6 ++++-- spc.registry.yml | 1 + src/Package/Target/php.php | 6 ++++-- src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php | 4 ++-- src/StaticPHP/Command/InstallPackageCommand.php | 2 +- src/StaticPHP/Config/ConfigValidator.php | 7 +++++++ 7 files changed, 22 insertions(+), 8 deletions(-) diff --git a/config/pkg/lib/openssl.yml b/config/pkg/lib/openssl.yml index cbbd0984..42672183 100644 --- a/config/pkg/lib/openssl.yml +++ b/config/pkg/lib/openssl.yml @@ -17,7 +17,6 @@ openssl: - zlib depends@windows: - zlib - - jom headers: - openssl static-libs@unix: @@ -26,3 +25,6 @@ openssl: static-libs@windows: - libssl.lib - libcrypto.lib + tools@windows: + - jom + - strawberry-perl diff --git a/config/pkg/target/frankenphp.yml b/config/pkg/target/frankenphp.yml index 21d47448..dc3f9fa9 100644 --- a/config/pkg/target/frankenphp.yml +++ b/config/pkg/target/frankenphp.yml @@ -10,10 +10,8 @@ frankenphp: license: MIT depends: - php-embed - - go-xcaddy depends@windows: - php-embed - - go-win - pthreads4w suggests@unix: - brotli @@ -24,3 +22,7 @@ frankenphp: - frankenphp static-bins@windows: - frankenphp.exe + tools: + - go-xcaddy + tools@windows: + - go-win diff --git a/spc.registry.yml b/spc.registry.yml index 98e4bb42..dbaa6ee0 100644 --- a/spc.registry.yml +++ b/spc.registry.yml @@ -10,6 +10,7 @@ package: - config/pkg/lib/ - config/pkg/target/ - config/pkg/ext/ + - config/pkg/tool/ artifact: config: - config/artifact/ diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index d1f5a54e..9efd5867 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -255,9 +255,11 @@ class php extends TargetPackage $installer->addBuildPackage('php-embed'); } - // UPX compression: ensure the upx binary package is installed when requested + // UPX compression: ensure the upx tool package is installed when requested. + // upx is a ToolPackage now, so it's installed directly rather than added as a + // library dependency (tool packages bypass the DependencyResolver graph). if ($package->getBuildOption('with-upx-pack')) { - $additional_packages[] = 'upx'; + $installer->addInstallPackage('upx'); } return [...$extensions_pkg, ...$additional_packages]; diff --git a/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php index e2f3dba9..f4923b33 100644 --- a/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php +++ b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php @@ -54,8 +54,8 @@ class DumpCapabilitiesCommand extends BaseCommand { $result = []; - // library / target / virtual-target - foreach (PackageLoader::getPackages(['library', 'target', 'virtual-target']) as $name => $pkg) { + // library / target / virtual-target / tool + foreach (PackageLoader::getPackages(['library', 'target', 'virtual-target', 'tool']) as $name => $pkg) { $installable = []; $artifact = $pkg->getArtifact(); if ($artifact !== null) { diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 1185e9f9..2dc668c8 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -23,7 +23,7 @@ class InstallPackageCommand extends BaseCommand 'The package to install (name or path)', suggestedValues: function (CompletionInput $input) { $packages = []; - foreach (PackageLoader::getPackages(['target', 'virtual-target']) as $name => $_) { + foreach (PackageLoader::getPackages(['target', 'virtual-target', 'tool']) as $name => $_) { $packages[] = $name; } $val = $input->getCompletionValue(); diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 7aac35ac..6bb4e11b 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -24,6 +24,10 @@ class ConfigValidator 'lang' => ConfigType::STRING, 'frameworks' => ConfigType::LIST_ARRAY, // @ + // build-time tool dependency declaration (resolved independently of the library + // dependency graph, see PackageInstaller::collectRequiredTools()) + 'tools' => ConfigType::LIST_ARRAY, // @ + // php-extension type fields 'php-extension' => ConfigType::ASSOC_ARRAY, 'zend-extension' => ConfigType::BOOL, @@ -63,6 +67,9 @@ class ConfigValidator 'lang' => false, 'frameworks' => false, // @ + // build-time tool dependency declaration + 'tools' => false, // @ + // php-extension type fields 'php-extension' => false, From d9c41a73da6b4fd2dd90ff48b430a31d7396d354 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 15:10:52 +0800 Subject: [PATCH 04/10] feat: add --installed flag to check-update, improve CLI and InteractiveTerm - Add --installed option to check-update command to compare against on-disk version (tool packages) - Support use_installed parameter in ArtifactDownloader::checkUpdate/checkUpdates - Add -y shorthand for --auto-fix in doctor command - Hook InteractiveTerm::init() in ConsoleApplication::doRun() to properly inject Symfony input/output --- src/StaticPHP/Artifact/ArtifactDownloader.php | 48 ++++++++--- src/StaticPHP/Command/CheckUpdateCommand.php | 6 +- src/StaticPHP/Command/DoctorCommand.php | 2 +- src/StaticPHP/ConsoleApplication.php | 14 +++ src/StaticPHP/Util/InteractiveTerm.php | 86 +++++++++++++++---- 5 files changed, 122 insertions(+), 34 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index f52036c6..bfe7a21f 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -26,6 +26,7 @@ use StaticPHP\Exception\DownloaderException; use StaticPHP\Exception\ExecutionException; use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\ToolVersionRegistry; use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; @@ -346,12 +347,32 @@ class ArtifactDownloader } } - public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false): CheckUpdateResult + public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false, bool $use_installed = false): CheckUpdateResult { $artifact = ArtifactLoader::getArtifactInstance($artifact_name); if ($artifact === null) { throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); } + + // --installed: prefer the version actually installed on disk (tool packages only) as the + // baseline, instead of the download cache. Falls back to the normal cache/bare logic below + // if this artifact has no recorded installed version (not a tool package, or not installed + // via the package installer yet). + if ($use_installed) { + $installed_version = ToolVersionRegistry::get($artifact_name); + if ($installed_version !== null) { + [$first, $second] = $prefer_source + ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name, $installed_version), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name, $installed_version)] + : [fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name, $installed_version), fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name, $installed_version)]; + $result = $first() ?? $second(); + if ($result !== null) { + return $result; + } + return new CheckUpdateResult(old: $installed_version, new: null, needUpdate: false, unsupported: true); + } + logger()->warning("Artifact '{$artifact_name}' has no recorded installed version (not a tool package, or not installed yet); falling back to download-cache based check."); + } + if ($bare) { [$first, $second] = $prefer_source ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name)] @@ -403,16 +424,17 @@ class ArtifactDownloader * @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 + * @param bool $use_installed Prefer the installed (on-disk) version over the download cache (tool packages only) * @return array Results keyed by artifact name */ - public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null): array + public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null, bool $use_installed = false): array { if ($this->parallel > 1 && count($artifact_names) > 1) { - return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult); + return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult, $use_installed); } $results = []; foreach ($artifact_names as $name) { - $result = $this->checkUpdate($name, $prefer_source, $bare); + $result = $this->checkUpdate($name, $prefer_source, $bare, $use_installed); $results[$name] = $result; if ($onResult !== null) { ($onResult)($name, $result); @@ -436,7 +458,7 @@ class ArtifactDownloader return $this->options[$name] ?? $default; } - private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult): array + private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult, bool $use_installed = false): array { $results = []; $fiber_pool = []; @@ -451,8 +473,8 @@ class ArtifactDownloader // 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 = new \Fiber(function () use ($name, $prefer_source, $bare, $use_installed) { + return [$name, $this->checkUpdate($name, $prefer_source, $bare, $use_installed)]; }); $fiber->start(); $fiber_pool[$name] = $fiber; @@ -491,12 +513,12 @@ class ArtifactDownloader return $results; } - private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name, ?string $old_version = null): ?CheckUpdateResult { if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, - 'old_version' => null, + 'old_version' => $old_version, ]); } $config = $artifact->getDownloadConfig('source'); @@ -509,16 +531,16 @@ class ArtifactDownloader } /** @var CheckUpdateInterface $dl */ $dl = new $cls(); - return $dl->checkUpdate($artifact_name, $config, null, $this); + return $dl->checkUpdate($artifact_name, $config, $old_version, $this); } - private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name, ?string $old_version = null): ?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, + 'old_version' => $old_version, ]); } $binary_config = $artifact->getDownloadConfig('binary'); @@ -532,7 +554,7 @@ class ArtifactDownloader } /** @var CheckUpdateInterface $dl */ $dl = new $cls(); - return $dl->checkUpdate($artifact_name, $platform_config, null, $this); + return $dl->checkUpdate($artifact_name, $platform_config, $old_version, $this); } private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 077da4f4..193aac7b 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -23,6 +23,7 @@ class CheckUpdateCommand extends BaseCommand $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('installed', null, null, 'Compare against the version actually installed on disk (tool packages only) instead of the download cache; falls back to normal behavior for artifacts with no recorded installed version'); $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 @@ -45,8 +46,9 @@ class CheckUpdateCommand extends BaseCommand try { $downloader = new ArtifactDownloader($this->input->getOptions()); $bare = (bool) $this->getOption('bare'); + $use_installed = (bool) $this->getOption('installed'); if ($this->getOption('json')) { - $results = $downloader->checkUpdates($artifacts, bare: $bare); + $results = $downloader->checkUpdates($artifacts, bare: $bare, use_installed: $use_installed); $outputs = []; foreach ($results as $artifact => $result) { $outputs[$artifact] = [ @@ -59,7 +61,7 @@ class CheckUpdateCommand extends BaseCommand $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); return static::OK; } - $downloader->checkUpdates($artifacts, bare: $bare, onResult: function (string $artifact, CheckUpdateResult $result) { + $downloader->checkUpdates($artifacts, bare: $bare, use_installed: $use_installed, onResult: function (string $artifact, CheckUpdateResult $result) { if ($result->unsupported) { $this->output->writeln("Artifact {$artifact} does not support update checking, skipped"); } elseif (!$result->needUpdate) { diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index 40303d14..ef4f0826 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -13,7 +13,7 @@ class DoctorCommand extends BaseCommand { public function configure(): void { - $this->addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false); + $this->addOption('auto-fix', 'y', InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false); } public function handle(): int diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index cf305e05..a82cbd68 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -32,7 +32,10 @@ use StaticPHP\Command\SPCConfigCommand; use StaticPHP\Package\TargetPackage; use StaticPHP\Registry\PackageLoader; use StaticPHP\Registry\Registry; +use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; class ConsoleApplication extends Application { @@ -104,4 +107,15 @@ class ConsoleApplication extends Application { self::$additional_commands = array_merge(self::$additional_commands, $additional_commands); } + + /** + * Hook Symfony's doRun() to inject the real input/output into InteractiveTerm before + * any command executes. From this point forward, InteractiveTerm uses the configured + * Console (respecting --no-ansi, --verbose, etc.) instead of the lazy STDERR fallback. + */ + public function doRun(InputInterface $input, OutputInterface $output): int + { + InteractiveTerm::init($input, $output); + return parent::doRun($input, $output); + } } diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index d84933cd..083d5a3e 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace StaticPHP\Util; -use StaticPHP\DI\ApplicationContext; use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; @@ -15,10 +14,31 @@ class InteractiveTerm { private static ?ProgressIndicator $indicator = null; + private static ?OutputInterface $output = null; + + private static ?bool $noAnsi = null; + + /** + * Initialize with a real Symfony Console input/output (called from ConsoleApplication::doRun()). + * After this call, all output goes through the configured Console, and noAnsi reflects + * the user's --no-ansi flag. + */ + public static function init(InputInterface $input, OutputInterface $output): void + { + self::$output = $output; + try { + self::$noAnsi = (bool) $input->getOption('no-ansi'); + } catch (\InvalidArgumentException) { + // Symfony hasn't bound the application-level input definition yet + // (e.g. ArgvInput passed directly to doRun() on some versions). + self::$noAnsi = false; + } + } + public static function notice(string $message, bool $indent = false): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = self::noAnsi(); + $output = self::output(); if ($output->isVerbose()) { logger()->notice(strip_ansi_colors($message)); } else { @@ -29,8 +49,8 @@ class InteractiveTerm public static function success(string $message, bool $indent = false): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = self::noAnsi(); + $output = self::output(); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { @@ -41,8 +61,8 @@ class InteractiveTerm public static function plain(string $message, string $level = 'info'): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = self::noAnsi(); + $output = self::output(); if ($output->isVerbose()) { match ($level) { 'debug' => logger()->debug(strip_ansi_colors($message)), @@ -59,8 +79,8 @@ class InteractiveTerm public static function info(string $message): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = self::noAnsi(); + $output = self::output(); if (!$output->isVerbose()) { $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green('▶ ') . $message)); } @@ -69,8 +89,8 @@ class InteractiveTerm public static function error(string $message, bool $indent = true): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = self::noAnsi(); + $output = self::output(); if ($output->isVerbose()) { logger()->error(strip_ansi_colors($message)); } else { @@ -86,16 +106,16 @@ class InteractiveTerm public static function setMessage(string $message): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); + $no_ansi = self::noAnsi(); 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 { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); + $no_ansi = self::noAnsi(); $message = $no_ansi ? strip_ansi_colors($message) : $message; - $output = ApplicationContext::get(OutputInterface::class); + $output = self::output(); if ($output->isVerbose()) { if ($status) { logger()->info($message); @@ -116,8 +136,8 @@ class InteractiveTerm public static function indicateProgress(string $message): void { - $no_ansi = (bool) ApplicationContext::get(InputInterface::class)->getOption('no-ansi'); - $output = ApplicationContext::get(OutputInterface::class); + $no_ansi = self::noAnsi(); + $output = self::output(); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); return; @@ -131,11 +151,41 @@ class InteractiveTerm 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, [' •', ' •']); + self::$indicator = new ProgressIndicator(self::output(), 'verbose', 100, [' •', ' •']); self::$indicator->start(strip_ansi_colors($message)); return; } - self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' ⠏', ' ⠛', ' ⠹', ' ⢸', ' ⣰', ' ⣤', ' ⣆', ' ⡇']); + self::$indicator = new ProgressIndicator(self::output(), 'verbose', 100, [' ⠏', ' ⠛', ' ⠹', ' ⢸', ' ⣰', ' ⣤', ' ⣆', ' ⡇']); self::$indicator->start($message); } + + /** + * Lazy default initialization used when init() was never called (early-boot errors, + * tests, programmatic usage). Creates a plain STDERR output so error messages are + * visible without depending on the Symfony Console lifecycle. + */ + private static function initDefault(): void + { + if (self::$output !== null) { + return; + } + self::$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, false); + self::$noAnsi = false; + } + + private static function noAnsi(): bool + { + if (self::$output === null) { + self::initDefault(); + } + return self::$noAnsi ?? false; + } + + private static function output(): OutputInterface + { + if (self::$output === null) { + self::initDefault(); + } + return self::$output; + } } From 924fac137c1fb4992f0bf2452a5cae63df58aeb4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 15:11:07 +0800 Subject: [PATCH 05/10] docs: fix hosted repo URL in READMEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update static-php-cli-hosted → static-php/hosted in both README.md and README-zh.md --- README-zh.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README-zh.md b/README-zh.md index 7b01ce3d..cb5700fd 100755 --- a/README-zh.md +++ b/README-zh.md @@ -104,7 +104,7 @@ echo ' a.php ## 直接下载 -如果你暂时不想构建,或只想先测试,可以从 [Actions](https://github.com/static-php/static-php-cli-hosted/actions/workflows/build-php-bulk.yml) 下载示例预编译产物,或从自托管服务器下载。 +如果你暂时不想构建,或只想先测试,可以从 [Actions](https://github.com/static-php/hosted/actions/workflows/build-php-bulk.yml) 下载示例预编译产物,或从自托管服务器下载。 我们为每个 PHP 版本提供 2 种扩展集合: diff --git a/README.md b/README.md index e0df9998..2c4df622 100755 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ see . ## Direct Download -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. +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/hosted/actions/workflows/build-php-bulk.yml) or from a self-hosted server. We offer 2 types of extension sets for each PHP version: From 0b02f62b7e29a106deb65c1ab55008e9a71a35bc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 15:36:36 +0800 Subject: [PATCH 06/10] feat: add pkg-config tool package config --- config/pkg/tool/pkg-config.yml | 13 ++++++++++ src/Package/Tool/pkgconfig.php | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 config/pkg/tool/pkg-config.yml create mode 100644 src/Package/Tool/pkgconfig.php diff --git a/config/pkg/tool/pkg-config.yml b/config/pkg/tool/pkg-config.yml new file mode 100644 index 00000000..4c93c2f5 --- /dev/null +++ b/config/pkg/tool/pkg-config.yml @@ -0,0 +1,13 @@ +pkg-config: + type: tool + 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/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/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/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/hosted, match: pkg-config-aarch64-darwin.txz, extract: { bin/pkg-config: '{pkg_root_path}/bin/pkg-config' } } + tool: + provides: + - pkg-config + binary-subdir: bin diff --git a/src/Package/Tool/pkgconfig.php b/src/Package/Tool/pkgconfig.php new file mode 100644 index 00000000..1cf84ef2 --- /dev/null +++ b/src/Package/Tool/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 {$package->getBinDir()}/pkg-config"); + } +} From ab37b34fb5e551d40ded29ddc04bb00bf1d4c4f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 16:46:29 +0800 Subject: [PATCH 07/10] fix: improve Windows patching logic and update file paths for xlsxwriter --- src/Package/Extension/xlswriter.php | 9 ++++++--- src/StaticPHP/Util/SourcePatcher.php | 18 ++++++++++++------ .../patch/spc_fix_xlswriter_win32.patch | 12 ++++++------ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Package/Extension/xlswriter.php b/src/Package/Extension/xlswriter.php index f4202376..9af2d6db 100644 --- a/src/Package/Extension/xlswriter.php +++ b/src/Package/Extension/xlswriter.php @@ -32,12 +32,15 @@ class xlswriter extends PhpExtensionPackage #[PatchDescription('Fix Windows build: apply win32 patch and add UTF-8 BOM to theme.c')] public function patchBeforeMakeForWindows(): void { + $source_dir = $this->getSourceDir(); + $theme_file = "{$source_dir}/library/libxlsx/src/theme.c"; + // 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'); + SourcePatcher::patchFile('spc_fix_xlswriter_win32.patch', $source_dir); + $content = file_get_contents($theme_file); $bom = pack('CCC', 0xEF, 0xBB, 0xBF); if (!str_starts_with($content, $bom)) { - file_put_contents($this->getSourceDir() . '/library/libxlsxwriter/src/theme.c', $bom . $content); + file_put_contents($theme_file, $bom . $content); } } } diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 70525e67..de0f1d36 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -45,12 +45,18 @@ class SourcePatcher file_put_contents(SOURCE_PATH . "/{$filename}", file_get_contents($patch_file)); $patch_str = FileSystem::convertPath(SOURCE_PATH . "/{$filename}"); } + $patch_cwd = FileSystem::convertPath($cwd); + $patch_arg = $patch_str; + if (PHP_OS_FAMILY === 'Windows') { + $patch_cwd = FileSystem::convertWinPathToMinGW($patch_cwd); + $patch_arg = FileSystem::convertWinPathToMinGW($patch_arg); + } // 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' : '') + $detect_cmd = 'patch --dry-run -p1 -s -f -d ' . escapeshellarg($patch_cwd) + . ' -i ' . escapeshellarg($patch_arg) + . ($detect_reverse ? ' -R' : '') . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; exec($detect_cmd, $output, $detect_status); @@ -60,9 +66,9 @@ class SourcePatcher } // Apply patch - $apply_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' - . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) - . ' | patch -p1 ' . ($reverse ? '-R' : ''); + $apply_cmd = 'patch -p1 -d ' . escapeshellarg($patch_cwd) + . ' -i ' . escapeshellarg($patch_arg) + . ($reverse ? ' -R' : ''); exec($apply_cmd, $apply_output, $apply_status); if ($apply_status !== 0) { diff --git a/src/globals/patch/spc_fix_xlswriter_win32.patch b/src/globals/patch/spc_fix_xlswriter_win32.patch index 23b62014..4fc30c77 100644 --- a/src/globals/patch/spc_fix_xlswriter_win32.patch +++ b/src/globals/patch/spc_fix_xlswriter_win32.patch @@ -1,7 +1,7 @@ -diff --git a/library/libxlsxwriter/third_party/md5/md5.c b/library/libxlsxwriter/third_party/md5/md5.c +diff --git a/library/libxlsx/third_party/md5/md5.c b/library/libxlsx/third_party/md5/md5.c index b235e17..ce98e18 100644 ---- a/library/libxlsxwriter/third_party/md5/md5.c -+++ b/library/libxlsxwriter/third_party/md5/md5.c +--- a/library/libxlsx/third_party/md5/md5.c ++++ b/library/libxlsx/third_party/md5/md5.c @@ -35,7 +35,11 @@ * compile-time configuration. */ @@ -15,10 +15,10 @@ index b235e17..ce98e18 100644 #include -diff --git a/library/libxlsxwriter/third_party/md5/md5.h b/library/libxlsxwriter/third_party/md5/md5.h +diff --git a/library/libxlsx/third_party/md5/md5.h b/library/libxlsx/third_party/md5/md5.h index 2da44bf..3cb0a98 100644 ---- a/library/libxlsxwriter/third_party/md5/md5.h -+++ b/library/libxlsxwriter/third_party/md5/md5.h +--- a/library/libxlsx/third_party/md5/md5.h ++++ b/library/libxlsx/third_party/md5/md5.h @@ -23,7 +23,7 @@ * See md5.c for more information. */ From fa040bc3466b4f577fd7d5257200ce59a4e87565 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 19:10:10 +0800 Subject: [PATCH 08/10] Fix mingw patch paths --- src/StaticPHP/Util/FileSystem.php | 2 +- src/StaticPHP/Util/SourcePatcher.php | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index ad41425c..3bf12f99 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -174,7 +174,7 @@ class FileSystem public static function convertWinPathToMinGW(string $path): string { if (preg_match('/^[A-Za-z]:/', $path)) { - $path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2)); + $path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', ltrim(substr($path, 2), '\/')); } return $path; } diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index de0f1d36..c12717c2 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -48,13 +48,14 @@ class SourcePatcher $patch_cwd = FileSystem::convertPath($cwd); $patch_arg = $patch_str; if (PHP_OS_FAMILY === 'Windows') { - $patch_cwd = FileSystem::convertWinPathToMinGW($patch_cwd); $patch_arg = FileSystem::convertWinPathToMinGW($patch_arg); } // Detect if patch is already applied (reverse detection) $detect_reverse = !$reverse; - $detect_cmd = 'patch --dry-run -p1 -s -f -d ' . escapeshellarg($patch_cwd) + $cd_cmd = (PHP_OS_FAMILY === 'Windows' ? 'cd /d ' : 'cd ') . escapeshellarg($patch_cwd); + $detect_cmd = $cd_cmd + . ' && patch --dry-run -p1 -s -f' . ' -i ' . escapeshellarg($patch_arg) . ($detect_reverse ? ' -R' : '') . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; @@ -66,7 +67,8 @@ class SourcePatcher } // Apply patch - $apply_cmd = 'patch -p1 -d ' . escapeshellarg($patch_cwd) + $apply_cmd = $cd_cmd + . ' && patch -p1' . ' -i ' . escapeshellarg($patch_arg) . ($reverse ? ' -R' : ''); From c9401a0b7a5802a9a5a9ec48cba4404a18f470a3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 19:35:48 +0800 Subject: [PATCH 09/10] Use redirect instead --- src/StaticPHP/Util/SourcePatcher.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index c12717c2..7b96a105 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -47,17 +47,14 @@ class SourcePatcher } $patch_cwd = FileSystem::convertPath($cwd); $patch_arg = $patch_str; - if (PHP_OS_FAMILY === 'Windows') { - $patch_arg = FileSystem::convertWinPathToMinGW($patch_arg); - } // Detect if patch is already applied (reverse detection) $detect_reverse = !$reverse; $cd_cmd = (PHP_OS_FAMILY === 'Windows' ? 'cd /d ' : 'cd ') . escapeshellarg($patch_cwd); $detect_cmd = $cd_cmd . ' && patch --dry-run -p1 -s -f' - . ' -i ' . escapeshellarg($patch_arg) . ($detect_reverse ? ' -R' : '') + . ' < ' . escapeshellarg($patch_arg) . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; exec($detect_cmd, $output, $detect_status); @@ -69,8 +66,8 @@ class SourcePatcher // Apply patch $apply_cmd = $cd_cmd . ' && patch -p1' - . ' -i ' . escapeshellarg($patch_arg) - . ($reverse ? ' -R' : ''); + . ($reverse ? ' -R' : '') + . ' < ' . escapeshellarg($patch_arg); exec($apply_cmd, $apply_output, $apply_status); if ($apply_status !== 0) { From dc33b55fd01b74e2d9a3e12159dd9fd28ffe9e78 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 6 Jul 2026 20:12:10 +0800 Subject: [PATCH 10/10] Use binary --- src/StaticPHP/Util/SourcePatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 7b96a105..98fee7ec 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -52,7 +52,7 @@ class SourcePatcher $detect_reverse = !$reverse; $cd_cmd = (PHP_OS_FAMILY === 'Windows' ? 'cd /d ' : 'cd ') . escapeshellarg($patch_cwd); $detect_cmd = $cd_cmd - . ' && patch --dry-run -p1 -s -f' + . ' && patch --binary --dry-run -p1 -s -f' . ($detect_reverse ? ' -R' : '') . ' < ' . escapeshellarg($patch_arg) . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; @@ -65,7 +65,7 @@ class SourcePatcher // Apply patch $apply_cmd = $cd_cmd - . ' && patch -p1' + . ' && patch --binary -p1' . ($reverse ? ' -R' : '') . ' < ' . escapeshellarg($patch_arg);