diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6e2f8d90 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,24 @@ +# Force LF line endings for all text files. +# Prevents git from auto-converting LF -> CRLF on Windows checkouts. +* text=auto eol=lf + +# Explicit binary files — git won't touch line endings for these +*.phar binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.zip binary +*.gz binary +*.tgz binary +*.txz binary +*.xz binary +*.bz2 binary +*.7z binary +*.exe binary +*.dll binary +*.sfx binary +*.so binary +*.a binary +*.o binary diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..cfe03ed6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,23 @@ +# StaticPHP v3 Copilot Instructions + +> The canonical agent instructions live in [AGENTS.md](./AGENTS.md). This file provides the essential summary for GitHub Copilot; read `AGENTS.md` for the complete project map, working rules, and validation steps. + +StaticPHP is a PHP CLI application for building static PHP binaries, PHP SAPIs, extensions, libraries, and helper tools. Prefer the v3 architecture under `src/StaticPHP`, `src/Package`, `config/pkg`, and `config/artifact`. + +## Skills + +Use the repository skills when the task matches them: + +- `.github/skills/staticphp-package-maintenance`: add, modify, review, or validate package, artifact, extension, library, target, or tool definitions. +- `.github/skills/staticphp-build-troubleshooting`: diagnose build, download, doctor, shell, terminal, CI, smoke-test, or log failures. + +## Quick Reference + +- **Entrypoint**: `php bin/spc` +- **Config lint**: `php bin/spc dev:lint-config` +- **CS fix**: `composer cs-fix` +- **Tests**: `composer test` +- **Static analysis**: `composer analyse` +- **Extension packages**: `config/pkg/ext/ext-*.yml`, prefix with `ext-` in dependencies +- **Platform suffixes**: `@unix`, `@linux`, `@macos`, `@windows` — prefer these over runtime conditionals +- **Do NOT edit generated dirs**: `buildroot/`, `source/`, `downloads/`, `pkgroot/` diff --git a/.github/skills/staticphp-build-troubleshooting/SKILL.md b/.github/skills/staticphp-build-troubleshooting/SKILL.md new file mode 100644 index 00000000..b93f4c7e --- /dev/null +++ b/.github/skills/staticphp-build-troubleshooting/SKILL.md @@ -0,0 +1,56 @@ +--- +name: staticphp-build-troubleshooting +description: Diagnose StaticPHP v3 failures. Use when investigating build, compile, linker, download, doctor, environment, CI, smoke-test, terminal output, spc.output.log, spc.shell.log, config.log, CMake logs, or user-provided error snippets from StaticPHP commands. +--- + +# StaticPHP Build Troubleshooting + +## Overview + +Use this skill to triage StaticPHP failures from the outside in: command and environment first, SPC module/stage metadata next, shell/config logs last. The goal is to isolate the failing package, stage, command, and root cause before editing code. + +## First Pass + +1. Capture the exact command, OS, architecture, PHP version, extensions/libs/targets, and whether the run used `-v`, `-vv`, or `-vvv`. +2. Read the final terminal error first. StaticPHP prints module error info, failed package, failed stage, failed command, log paths, and extra log files when available. +3. If log files exist, inspect them in this order: + - `log/spc.output.log`: user-facing SPC messages and exception summary. + - `log/spc.shell.log`: executed commands, working directories, env, stdout/stderr. + - Extra logs named in the exception output: `php-src.config.log`, `lib..console.log`, `lib..cmake-error.log`, `lib..cmake-configure.log`, `lib..cmake-output.log`. +4. Read `references/log-triage.md` for pattern matching, likely causes, and next checks. + +## Diagnosis Rules + +- Do not start by changing shared core code. Most failures are package metadata, environment, upstream source, dependency order, or platform flags. +- Prefer evidence from the last failing command over earlier warnings. +- When logs are long, search backward for `Command exited`, `error:`, `undefined reference`, `not found`, `No package`, `CMake Error`, `configure: error`, `fatal error`, `Failed module`, and `Failed stage`. +- Use `config.log` and CMake logs for configure detection failures; use `spc.shell.log` for the actual command and env. +- Be careful with rebuild suggestions. `spc reset --with-download --yes` is destructive to caches; ask before clearing caches unless the user explicitly asked. + +## Repro Commands + +Use focused commands when reproducing: + +```bash +php bin/spc doctor -vvv +php bin/spc download --for-extensions="curl,openssl" --with-php=8.5 --parallel=4 --retry=3 -vvv +php bin/spc build:libs "openssl" -vvv +php bin/spc build:php "bcmath,openssl,curl" --build-cli -vvv +php bin/spc dev:lint-config +``` + +Choose the smallest command that still reaches the failing package. + +## Fix Direction + +After finding the failing package/stage: + +- Package YAML issue: use `$staticphp-package-maintenance`, then edit `config/pkg/*` or `config/artifact/*`. +- Build command or patch issue: inspect the package class under `src/Package/*`. +- Environment issue: check `doctor`, toolchain classes, and `config/pkg/tool/*`. +- Upstream download issue: check artifact type, regex, GitHub rate limiting, mirror behavior, and `GITHUB_TOKEN`. +- Core exception/logging issue: inspect `src/StaticPHP/Exception`, `src/StaticPHP/Runtime/Shell`, or executor classes only after package-level causes are ruled out. + +## Resources + +- `references/log-triage.md`: log files, failure categories, search patterns, and likely fixes. diff --git a/.github/skills/staticphp-build-troubleshooting/agents/openai.yaml b/.github/skills/staticphp-build-troubleshooting/agents/openai.yaml new file mode 100644 index 00000000..2afaef5c --- /dev/null +++ b/.github/skills/staticphp-build-troubleshooting/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "StaticPHP Build Troubleshooting" + short_description: "Diagnose StaticPHP build and log failures." + default_prompt: "Use $staticphp-build-troubleshooting to investigate a StaticPHP build, download, doctor, or log failure." diff --git a/.github/skills/staticphp-build-troubleshooting/references/log-triage.md b/.github/skills/staticphp-build-troubleshooting/references/log-triage.md new file mode 100644 index 00000000..96f5d6a5 --- /dev/null +++ b/.github/skills/staticphp-build-troubleshooting/references/log-triage.md @@ -0,0 +1,209 @@ +# StaticPHP Log Triage + +## Contents + +- Log files +- Terminal summary +- Search strategy +- Failure categories +- Rebuild hygiene +- Reporting checklist + +## Log Files + +Default log directory is `log/`, controlled by `SPC_LOGS_DIR`. The important files are: + +- `spc.output.log`: StaticPHP console/logger output and exception summaries. +- `spc.shell.log`: command execution log with command, caller, inline env, working directory, stdout, stderr, and exit code. +- `php-src.config.log`: copied when PHP source configure fails. +- `lib..console.log`: copied Autoconf `config.log` for a library when available. +- `lib..cmake-configure.log`: copied CMake 3.26+ configure log. +- `lib..cmake-error.log`: copied CMake error log. +- `lib..cmake-output.log`: copied CMake output log. + +Logs are created when `SPC_ENABLE_LOG_FILE` is true. Old `*.log` files are cleaned unless `SPC_PRESERVE_LOG` is set to a truthy value. The code in `src/bootstrap.php` reads `SPC_PRESERVE_LOG` (no trailing S). + +## Terminal Summary + +StaticPHP exceptions often print: + +- Exception category: build, download, environment, execution, file system, patch, validation, wrong usage, registry, internal. +- Failed module: package name, type, and sometimes class/file/line. +- Failed stage: call chain such as `build` or `build -> configure`. +- Failed command: the command that returned non-zero. +- Command working directory and inline env. +- Paths to `spc.output.log`, `spc.shell.log`, and extra logs. + +Use this summary to choose the first files to read. If a package and stage are present, start with that package's YAML and class after reading the relevant log tail. + +## Search Strategy + +For long logs, search from the end first. + +Useful patterns: + +```text +Failed module: +Failed stage: +Failed command: +Command exited with non-zero code +configure: error +CMake Error +undefined reference +cannot find -l +fatal error: +No package +Package .* not found +Could NOT find +is not a valid +permission denied +curl: ( +HTTP/2 403 +rate limit +``` + +In `spc.shell.log`, each command block begins with `>>>>>>>>>>>>>>>>>>>>>>>>>>`, then prints the command, caller, env, and working dir. The last failed block is usually the highest-value evidence. + +## Failure Categories + +### Download Failure + +Signs: + +- `Download failed` +- `curl: (56)`, `curl: (22)`, HTTP 403/404/429 +- GitHub API calls in verbose logs +- Asset regex did not match a release asset + +Likely checks: + +- If GitHub rate limited, ask user to set `GITHUB_TOKEN`. +- Verify artifact source type and regex in `config/pkg/*` or `config/artifact/*`. +- Use `--dl-retry`, `--dl-parallel`, `--ignore-cache`, `--no-alt`, or custom source options only as repro aids. +- Prefer fixing stale URLs/regexes over adding workarounds. + +### Doctor or Environment Failure + +Signs: + +- `Environment check failed` +- missing `make`, `cmake`, `autoconf`, `pkg-config`, compiler, Perl, Visual Studio, MSYS2, Zig, etc. +- `Some check items can not be fixed` + +Likely checks: + +- Run `php bin/spc doctor -vvv`. +- Inspect `src/StaticPHP/Doctor/Item/*` for the check and fix behavior. +- Inspect `config/pkg/tool/*` for installable helper tools. +- On Windows, some dependencies cannot be auto-installed. + +### Configure Failure + +Signs: + +- `configure: error` +- Autoconf failure before compilation +- extra `config.log` listed + +Likely checks: + +- Read copied `php-src.config.log` or `lib..console.log`. +- Look for the first compiler/linker probe that failed. +- Check `headers`, `static-libs`, `pkg-configs`, dependency order, and `initializeEnv($pkg)` behavior. +- For extensions, check `php-extension.arg-type` and `#[CustomPhpConfigureArg]`. + +### CMake Failure + +Signs: + +- `CMake Error` +- `Could NOT find` +- missing target, package config, or static library + +Likely checks: + +- Read `lib..cmake-error.log` and `lib..cmake-configure.log`. +- Inspect `UnixCMakeExecutor`/`WindowsCMakeExecutor` usage in the package class. +- Check CMake options, toolchain root path, `pkg-configs`, and transitive dependency libraries. + +### Compile Failure + +Signs: + +- compiler `fatal error: header.h: No such file or directory` +- syntax/type errors from upstream source +- incompatible PHP API or platform macros + +Likely checks: + +- Identify the source file and include path from `spc.shell.log`. +- Verify dependency headers are installed in `buildroot/include`. +- Check PHP version compatibility and existing patches in `src/globals/patch`. +- Prefer targeted patches or compile flags in the package class over broad toolchain changes. + +### Link Failure + +Signs: + +- `undefined reference` +- `cannot find -lfoo` +- duplicate symbols +- Windows unresolved external symbol or missing `.lib` + +Likely checks: + +- Verify `static-libs`, dependency order, and `getStaticLibFiles()` usage. +- Inspect `SPC_EXTRA_LIBS`, `Libs.private` in `.pc` files, and package-specific pkg-config patching. +- On Windows, confirm `.lib` names and required system libraries. +- For OpenSSL/curl-like packages, search existing linker flag hooks before adding a new one. + +### Smoke Test or Validation Failure + +Signs: + +- `Validation failed` +- `php --ri` failure +- extension not loaded after build +- micro/embed runtime smoke test fails + +Likely checks: + +- Check extension `display-name`; empty string skips `php --ri`. +- Confirm static vs shared build settings. +- Confirm the extension is included in the final SAPI target and not only built as a dependency. +- Inspect `src/globals/ext-tests/*` and common tests for expected runtime behavior. + +### Registry or Config Failure + +Signs: + +- `Registry error` +- unknown package, invalid field, invalid platform suffix, type mismatch +- package referenced by attribute but missing config + +Likely checks: + +- Run `php bin/spc dev:lint-config`. +- Inspect `ConfigValidator` tests for accepted fields and error expectations. +- Confirm attribute type matches YAML `type`. +- Confirm hooks reference existing package names and stages for the current platform. + +## Rebuild Hygiene + +Use the smallest cleanup that can invalidate the suspected stale state: + +- Redownload one artifact: `--ignore-cache="artifact-name"`. +- Skip downloads to test local source/build logic: `--no-download`. +- Prefer source when debugging build logic: `--dl-prefer-source`. +- Clear build products only when necessary. `spc reset --with-download --yes` removes caches and should not be suggested casually. + +## Reporting Checklist + +When preparing an issue or PR explanation, include: + +- Exact `php bin/spc ...` command. +- OS, architecture, PHP version option, and relevant env vars. +- Failed package and stage. +- Last failed command and exit code. +- Tail of `spc.output.log`, relevant command block from `spc.shell.log`, and any extra config/CMake log. +- Whether the failure reproduces after a focused clean or cache refresh. diff --git a/.github/skills/staticphp-package-maintenance/SKILL.md b/.github/skills/staticphp-package-maintenance/SKILL.md new file mode 100644 index 00000000..cc9b2ca4 --- /dev/null +++ b/.github/skills/staticphp-package-maintenance/SKILL.md @@ -0,0 +1,55 @@ +--- +name: staticphp-package-maintenance +description: Maintain StaticPHP v3 packages and artifacts. Use when adding, modifying, reviewing, or validating config under config/pkg or config/artifact; package classes under src/Package; extension/library/target/tool dependencies; PHP configure args; build hooks; package metadata; or related tests and docs. +--- + +# StaticPHP Package Maintenance + +## Overview + +Use this skill to make focused package changes without re-reading the entire repository. StaticPHP v3 separates declarative package/artifact YAML from package-specific PHP build logic; prefer config-only edits unless current patterns require a class. + +## Quick Workflow + +1. Identify the package kind and exact name. + - Extension: `config/pkg/ext/ext-name.yml`, class `src/Package/Extension/name.php`. + - Library: `config/pkg/lib/name.yml`, class `src/Package/Library/name.php`. + - Target or virtual target: `config/pkg/target/name.yml`, class `src/Package/Target/name.php`. + - Tool: `config/pkg/tool/name.yml`, class `src/Package/Tool/name.php`. + - Shared/custom artifact: `config/artifact/name.yml`, class `src/Package/Artifact/name.php`. + +2. Search for the closest existing package before designing anything new. + - Similar build system: `rg "#\\[BuildFor|UnixCMakeExecutor|UnixAutoconfExecutor|WindowsCMakeExecutor" src/Package`. + - Similar config fields: `rg "static-libs|pkg-configs|arg-type|depends@" config/pkg`. + - Similar download type: `rg "type: ghrel|type: pecl|type: pie|type: git|binary: hosted" config`. + +3. Read `references/package-reference.md` when changing YAML fields, artifact definitions, package naming, dependencies, platform suffixes, or validation expectations. + +4. Read `references/build-class-patterns.md` when PHP build logic, attributes, lifecycle hooks, custom configure args, source patching, or executor usage is needed. + +5. Validate narrowly, then broadly if risk warrants it. + - Config lint: `php bin/spc dev:lint-config` + - Focused tests: `vendor/bin/phpunit tests/StaticPHP/Config tests/StaticPHP/Registry tests/StaticPHP/Util/DependencyResolverTest.php --no-coverage` + - Full project checks: `composer test`, `composer analyse` + +## Editing Rules + +- Treat `config/pkg/*` and `config/artifact/*` as the source of package truth; package classes augment behavior. +- Use platform suffix fields for declarative OS differences: `@unix`, `@linux`, `@macos`, `@windows`. +- Add a PHP class only for custom build stages, validation, hook behavior, patches, or custom configure arguments. +- Keep extension dependencies prefixed with `ext-`; libraries, tools, and targets use their package names. +- Prefer existing helpers such as `UnixAutoconfExecutor`, `UnixCMakeExecutor`, `WindowsCMakeExecutor`, `shell()`, `cmd()`, and `FileSystem`. +- Do not modify build outputs (`buildroot/`, `source/`, `downloads/`, `pkgroot/`) to fix package definitions. +- If upstream metadata changes, update license metadata and smoke-test/display names when relevant. + +## Common Task Paths + +- Add a new PECL extension: define `config/pkg/ext/ext-name.yml` with `type: php-extension`, `artifact.source.type: pecl`, dependencies, and `php-extension.arg-type`; add a class only if configure args, patches, or hooks are non-standard. +- Add a library: define `config/pkg/lib/name.yml` with artifact, dependencies, and install verification fields (`headers`, `static-libs`, `pkg-configs`, `static-bins`); add build class methods by OS. +- Fix an existing package build: start from the failing package config/class, then inspect dependencies and hooks targeting that package before changing shared core code. +- Update a version source: prefer artifact fields that support update checking (`ghrel`, `ghtar`, `ghtagtar`, `git` with regex, `filelist`, `pecl`, `pie`) over hard-coded URLs when upstream supports it. + +## Resources + +- `references/package-reference.md`: YAML package/artifact model, naming, fields, validation, and commands. +- `references/build-class-patterns.md`: PHP package attributes, stages, hooks, executors, and class patterns. diff --git a/.github/skills/staticphp-package-maintenance/agents/openai.yaml b/.github/skills/staticphp-package-maintenance/agents/openai.yaml new file mode 100644 index 00000000..40923512 --- /dev/null +++ b/.github/skills/staticphp-package-maintenance/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "StaticPHP Package Maintenance" + short_description: "Add, update, and validate StaticPHP v3 package definitions." + default_prompt: "Use $staticphp-package-maintenance to add, modify, or review StaticPHP package and artifact definitions." diff --git a/.github/skills/staticphp-package-maintenance/references/build-class-patterns.md b/.github/skills/staticphp-package-maintenance/references/build-class-patterns.md new file mode 100644 index 00000000..0604e12d --- /dev/null +++ b/.github/skills/staticphp-package-maintenance/references/build-class-patterns.md @@ -0,0 +1,143 @@ +# StaticPHP Build Class Patterns + +## Contents + +- When a class is needed +- Package attributes +- Stage and hook behavior +- Executor choices +- Common patterns +- Failure-safe edits + +## When a Class Is Needed + +Do not add a PHP package class for simple metadata. Use YAML alone when StaticPHP can infer the configure args, dependencies, and verification. + +Add or update a class when the package needs: + +- OS-specific build commands. +- Custom validation before building. +- Custom PHP configure arguments. +- Patches or file edits before build. +- Hooks into another package's stage. +- Custom source/binary download or extraction logic. +- Package info shown by dev tooling. + +Package classes live in `src/Package/*` and are discovered through PSR-4 plus attributes. Config must exist first; `PackageLoader` throws if an attribute references an undefined package. + +## Package Attributes + +Class-level package attributes: + +- `#[Extension('curl')]` maps to package `ext-curl` if the prefix is omitted. +- `#[Library('openssl')]` +- `#[Target('php')]` +- `#[Tool('zig')]` + +Method-level attributes: + +- `#[BuildFor('Linux'|'Darwin'|'Windows')]`: registers the `build` stage for that OS. +- `#[Stage('name')]`: registers a named stage; defaults to the method name when omitted. +- `#[BeforeStage('package', 'stage', 'only-when-package-resolved')]`: hook before a target package stage. +- `#[AfterStage('package', 'stage', 'only-when-package-resolved')]`: hook after a target package stage. +- `#[PatchBeforeBuild]`: runs once before package build unless `.spc-patched` exists; return `true` to write that marker. +- `#[CustomPhpConfigureArg('Linux')]`: supplies custom extension configure args. +- `#[Validate]`: validates environment/source assumptions. +- `#[Info]`: returns package information for tooling. +- `#[InitPackage]`: runs while loading a package class. +- `#[ResolveBuild]`: target package callback for resolving build dependencies. +- `#[ConditionalOn(SomeClass::class)]`: conditionally enables before/after hooks only when DI has the class. + +## Stage and Hook Behavior + +`PackageBuilder::buildPackage()` does: + +1. Ensure build directories exist. +2. Skip if already installed unless forced. +3. Ensure source exists for non-virtual packages. +4. Emit `PatchBeforeBuild` callbacks. +5. Run the `build` stage. +6. Install license data. +7. Record tool package versions where relevant. + +`Package::runStage()` wraps a stage with: + +1. `BeforeStage` callbacks. +2. The stage method itself. +3. `AfterStage` callbacks. + +SPC exceptions bind package and stage metadata. Preserve that behavior by throwing existing `SPCException` subclasses instead of raw exceptions when adding new failure paths. + +## Executor Choices + +Use existing executors where possible: + +- `UnixAutoconfExecutor`: for Unix packages using `./configure && make && make install`. It injects default static flags and copies `config.log` into SPC logs on failure. +- `UnixCMakeExecutor`: for Unix CMake packages. It writes toolchain settings and copies CMake configure/error/output logs on failure. +- `WindowsCMakeExecutor`: for Windows CMake packages. +- `shell()->cd(...)->initializeEnv($pkg)`: for custom Unix command chains. +- `cmd()->cd(...)`: for Windows command chains. + +Prefer `FileSystem` helpers for file edits and copies so behavior stays consistent. + +## Common Patterns + +Minimal library class: + +```php +#[Library('example')] +class example +{ + #[BuildFor('Linux')] + public function build(LibraryPackage $pkg): void + { + (new UnixAutoconfExecutor($pkg)) + ->configure() + ->make(); + } +} +``` + +Extension hook into PHP build: + +```php +#[Extension('curl')] +class curl +{ + #[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-curl')] + public function patchBeforePhpBuild(): void + { + // Adjust env, generated build files, or linker flags. + } +} +``` + +Validation: + +```php +#[Validate] +public function validate(): void +{ + if (SystemTarget::getTargetOS() === 'Windows' && WindowsUtil::findCommand('perl.exe') === null) { + throw new EnvironmentException('You need to install perl first!'); + } +} +``` + +Custom configure args should be used when YAML `arg-type` is `custom` or when args need package/install context: + +```php +#[CustomPhpConfigureArg('Linux')] +public function configureArg(PhpExtensionPackage $ext): string +{ + return '--with-example=' . $ext->getBuildRootPath(); +} +``` + +## Failure-Safe Edits + +- Make hooks conditional with the third `BeforeStage`/`AfterStage` argument when a dependency must be resolved before the hook should run. +- Keep platform-specific code under matching `#[BuildFor]` methods rather than branching heavily inside one method. +- If a patch changes upstream source, place patch files in `src/globals/patch/` and describe them with `#[PatchDescription]` where appropriate. +- If a class only mutates environment variables for another package, search for existing hooks first to avoid duplicate linker flags. +- After adding attributes, run registry/config tests because invalid stages and unknown packages are caught during loading. diff --git a/.github/skills/staticphp-package-maintenance/references/package-reference.md b/.github/skills/staticphp-package-maintenance/references/package-reference.md new file mode 100644 index 00000000..a51d7ed4 --- /dev/null +++ b/.github/skills/staticphp-package-maintenance/references/package-reference.md @@ -0,0 +1,157 @@ +# StaticPHP Package Reference + +## Contents + +- Package locations +- Package types +- Artifact model +- Common YAML fields +- PHP extension fields +- Platform suffixes +- Validation and tests + +## Package Locations + +StaticPHP v3 keeps package metadata in YAML and behavior in PHP classes. + +| Kind | YAML | PHP class namespace | Typical attribute | +|---|---|---|---| +| PHP extension | `config/pkg/ext/ext-name.yml` | `Package\Extension` | `#[Extension('name')]` | +| Library | `config/pkg/lib/name.yml` | `Package\Library` | `#[Library('name')]` | +| Target | `config/pkg/target/name.yml` | `Package\Target` | `#[Target('name')]` | +| Virtual target | `config/pkg/target/name.yml` | `Package\Target` | `#[Target('name')]` | +| Tool | `config/pkg/tool/name.yml` | `Package\Tool` | `#[Tool('name')]` | +| Custom artifact | `config/artifact/name.yml` | `Package\Artifact` | artifact attributes | + +Prefer one package per YAML file unless an existing file groups related definitions. Match nearby naming and ordering. + +## Package Types + +`php-extension` packages describe PHP extensions. Config package names must use the `ext-` prefix. Dependencies on extensions also use `ext-`. + +`library` packages describe buildable dependency libraries. They usually define source artifacts plus verification fields such as headers, static libraries, pkg-config files, or binaries. + +`target` packages represent final build outputs and inherit the library-style fields. StaticPHP automatically exposes build commands for targets. + +`virtual-target` packages are abstract dependency/build scheduling nodes. They may omit `artifact`. + +`tool` packages are helper programs or toolchains installed into `pkgroot/` or used by builds. + +## Artifact Model + +Artifacts define downloadable source archives or prebuilt binaries. Use inline artifacts for simple one-package sources and `config/artifact/*.yml` for shared, complex, or custom artifacts. + +Top-level artifact sections: + +- `source`: source archive, git checkout, PECL/PIE package, local directory, or custom source. +- `binary`: prebuilt binary by platform, or aliases such as `hosted`/`custom`. +- `metadata`: `license`, `license-files`, and `source-root`. + +Common source types: + +- `url`: fixed URL; supports `filename`, `version`, and `extract`. +- `git`: repository source; use `rev` for a fixed branch/tag/commit or `regex` with named `version` capture for update checks. +- `ghrel`: GitHub release asset by regex filename match. +- `ghtar`: generated GitHub release tarball. +- `ghtagtar`: generated GitHub tag tarball. +- `filelist`: scrape a download index and extract version/filename with regex. +- `pecl`: PECL extension by name. +- `pie`: Packagist PIE extension by `vendor/package`. +- `php-release`: official PHP source selected by build PHP version. +- `local`: local source directory for development/offline use. +- `custom`: PHP artifact class handles source or binary logic. + +Use `metadata.source-root` when the actual build directory is inside an extracted subdirectory. + +Use `metadata.license-files` for source licenses. `@/file.txt` points to bundled license files in `src/globals/licenses/`. + +## Common YAML Fields + +Shared fields: + +- `type`: required package type. +- `description`: optional human-readable package description. +- `license`: package-level license annotation where applicable. +- `lang`: implementation language such as `c` or `c++`. +- `frameworks`: macOS framework tags. +- `artifact`: string reference or inline artifact object. Required for `library` and `target`; optional for built-in extensions and virtual targets. +- `depends`: hard dependencies. +- `suggests`: optional dependencies. + +Library, target, and tool verification/installation fields: + +- `headers`: expected files/directories under `buildroot/include`. +- `static-libs`: expected static libraries under `buildroot/lib`. +- `pkg-configs`: expected `.pc` files under `buildroot/lib/pkgconfig`. +- `static-bins`: expected executables under `buildroot/bin`. +- `path`: directories appended to PATH after install. +- `env`: environment variables set after install. +- `append-env`: values appended to existing environment variables. + +Path placeholders in `path`, `env`, and `append-env`: + +- `{build_root_path}`: `buildroot/` +- `{pkg_root_path}`: `pkgroot/` +- `{working_dir}`: repository or working directory +- `{download_path}`: `downloads/` +- `{source_path}`: `source/` +- `{spc_msys2_path}`: MSYS2 root on Windows + +## PHP Extension Fields + +Extension-specific fields live under `php-extension`. + +- `arg-type`: configure argument behavior. Built-ins include `enable`, `enable-path`, `with`, `with-path`, `custom`, and `none`. Full strings are allowed. +- `zend-extension`: true for Zend extensions such as opcache/xdebug. +- `build-shared`: whether shared builds are allowed. +- `build-static`: whether static builds are allowed. +- `build-with-php`: true when built inside the PHP source tree. +- `display-name`: value used for `php --ri` smoke tests and license display; empty string skips the `--ri` check. +- `os`: allowed OS families: `Linux`, `Darwin`, `Windows`. + +Configure string placeholders: + +- `@build_root_path@`: absolute buildroot path. +- `@shared_suffix@`: `=shared` in shared builds, empty in static builds. +- `@shared_path_suffix@`: `=shared,{buildroot}` in shared builds, `={buildroot}` in static builds. + +## Platform Suffixes + +Many fields support platform suffixes: + +- `@unix`: Linux and macOS. +- `@linux`: Linux only. +- `@macos`: macOS only. +- `@windows`: Windows only. + +For Linux, specific fields override generic fields in this order: `field@linux`, then `field@unix`, then `field`. + +Use suffixes for declarative differences in dependencies, headers, libraries, pkg-configs, extension args, and suggestions. + +## Validation and Tests + +Run config validation for most package edits: + +```bash +php bin/spc dev:lint-config +``` + +Run focused tests after config or registry changes: + +```bash +vendor/bin/phpunit tests/StaticPHP/Config tests/StaticPHP/Registry tests/StaticPHP/Util/DependencyResolverTest.php --no-coverage +``` + +Run broader checks for framework or class changes: + +```bash +composer test +composer analyse +``` + +Build commands are expensive. Use them for user-provided repros, package-specific fixes, or when validation cannot prove the behavior: + +```bash +php bin/spc build:libs "openssl,curl" -vvv +php bin/spc build:php "bcmath,openssl,curl" --build-cli -vvv +``` diff --git a/.github/workflows/post-test-bot-comment.yml b/.github/workflows/post-test-bot-comment.yml new file mode 100644 index 00000000..2da0878f --- /dev/null +++ b/.github/workflows/post-test-bot-comment.yml @@ -0,0 +1,48 @@ +name: Post Test Bot Comment + +on: + workflow_run: + workflows: ["v3 Tests"] + types: [completed] + +permissions: + pull-requests: write + actions: read + +jobs: + post-comment: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Download bot output + id: download + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: bot-output + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Post/Update comment + if: steps.download.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + COMMENT_BODY=$(jq -r '.comment_body' bot-output.json) + PR_NUMBER=$(jq -r '.pr_number' bot-output.json) + MARKER="" + + # Find existing bot comment id + EXISTING_ID=$(gh api \ + "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + --jq "[.[] | select(.body | startswith(\"${MARKER}\")) | .id] | first // empty") + + if [ -n "$EXISTING_ID" ]; then + gh api --method PATCH \ + "repos/${{ github.repository }}/issues/comments/${EXISTING_ID}" \ + -f body="$COMMENT_BODY" + else + gh pr comment "$PR_NUMBER" \ + --repo "${{ github.repository }}" \ + --body "$COMMENT_BODY" + fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a62fcc0a..34dbf959 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -131,7 +131,6 @@ jobs: if: needs.check-gate.outputs.enabled == 'true' runs-on: ubuntu-latest permissions: - pull-requests: write contents: read outputs: need_test: ${{ steps.bot.outputs.need_test }} @@ -168,23 +167,17 @@ jobs: echo "php_versions=$(echo "$BOT_JSON" | jq -c '.php_versions')" >> "$GITHUB_OUTPUT" echo "tier2=$(echo "$BOT_JSON" | jq -r '.tier2')" >> "$GITHUB_OUTPUT" - COMMENT_BODY=$(echo "$BOT_JSON" | jq -r '.comment_body') - MARKER="" + # Save JSON for the comment-posting workflow + jq -n --argjson bot "$BOT_JSON" \ + --arg pr '${{ github.event.pull_request.number }}' \ + '$bot + {pr_number: $pr}' > bot-output.json - # Find existing bot comment id - EXISTING_ID=$(gh api \ - repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ - --jq "[.[] | select(.body | startswith(\"$MARKER\")) | .id] | first // empty") - - if [ -n "$EXISTING_ID" ]; then - gh api --method PATCH \ - repos/${{ github.repository }}/issues/comments/"$EXISTING_ID" \ - -f body="$COMMENT_BODY" - else - gh pr comment ${{ github.event.pull_request.number }} \ - --repo ${{ github.repository }} \ - --body "$COMMENT_BODY" - fi + - name: Upload bot output + uses: actions/upload-artifact@v4 + with: + name: bot-output + path: bot-output.json + retention-days: 1 gen-matrix: name: "Generate test matrix" diff --git a/.github/workflows/vitepress-deploy.yml b/.github/workflows/vitepress-deploy.yml index 515cf1d6..86b633b4 100644 --- a/.github/workflows/vitepress-deploy.yml +++ b/.github/workflows/vitepress-deploy.yml @@ -1,68 +1,68 @@ -name: Docs build test and auto deploy -on: - pull_request: - branches: [ "v3" ] - types: [ opened, synchronize, reopened ] - paths: - - 'config/**.yml' - - 'docs/**' - - 'package.json' - - 'yarn.lock' - - '.github/workflows/vitepress-deploy.yml' - 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 - - - run: npm install - - - 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: Build - run: npm run docs:build - - # Deploy to GitHub Pages only when the workflow is triggered by a push to the v3 branch - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - if: github.event_name == 'push' && github.ref == 'refs/heads/v3' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: docs/.vitepress/dist +name: Docs build test and auto deploy +on: + pull_request: + branches: [ "v3" ] + types: [ opened, synchronize, reopened ] + paths: + - 'config/**.yml' + - 'docs/**' + - 'package.json' + - 'yarn.lock' + - '.github/workflows/vitepress-deploy.yml' + 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 + + - run: npm install + + - 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: Build + run: npm run docs:build + + # Deploy to GitHub Pages only when the workflow is triggered by a push to the v3 branch + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + if: github.event_name == 'push' && github.ref == 'refs/heads/v3' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/.vitepress/dist diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2292f0a4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,59 @@ +# StaticPHP v3 Agent Guide + +StaticPHP is a PHP CLI application for building static PHP binaries, PHP SAPIs, extensions, libraries, helper tools, and project bundles. Prefer the v3 architecture under `src/StaticPHP`, `src/Package`, `config/pkg`, and `config/artifact`. + +This file is the shared entry point for coding agents such as Codex, OpenCode, Claude-compatible agents, and GitHub Copilot agents. GitHub Copilot also has `.github/copilot-instructions.md`. + +## Skills + +Detailed task workflows live in `.github/skills`. Use them when the task matches: + +- `.github/skills/staticphp-package-maintenance`: add, modify, review, or validate package, artifact, extension, library, target, or tool definitions. +- `.github/skills/staticphp-build-troubleshooting`: diagnose build, download, doctor, shell, terminal, CI, smoke-test, or log failures. + +If your agent does not automatically discover skills from `.github/skills`, read the matching `SKILL.md` manually before working on that task. + +## Project Map + +- `bin/spc`: primary CLI entrypoint. +- `composer.json`: PHP 8.4+, Symfony Console, DI, YAML, logger; useful scripts are `composer test`, `composer analyse`, `composer lint-config`, and `composer cs-fix`. +- `spc.registry.yml`: built-in `core` registry. +- `config/pkg/ext`: PHP extension package YAML, usually named `ext-*.yml`. +- `config/pkg/lib`: library package YAML. +- `config/pkg/target`: final build target and virtual target YAML. +- `config/pkg/tool`: helper tool package YAML. +- `config/artifact`: standalone artifact YAML for shared or complex sources/binaries. +- `log/`: build and shell execution logs (`spc.output.log`, `spc.shell.log`, and per-package CMake/config logs); controlled by `SPC_LOGS_DIR` and `SPC_ENABLE_LOG_FILE`. +- `src/StaticPHP`: framework core: registry loading, config validation, dependency resolution, package install/build pipeline, doctor, toolchains, runtime shell/executors, exceptions. +- `src/Package`: package-specific build logic registered by PHP attributes. +- `src/globals`: constants, helper functions, patches, smoke tests, bundled license text. +- `tests`: PHPUnit tests for config, registry, artifacts, dependency resolver, DI, commands, and utilities. +- `docs/en/develop` and `docs/zh/develop`: developer documentation. Some pages may be TODO; verify against source when behavior matters. + +## Working Rules + +- Keep changes scoped to the requested package, artifact, command, docs, or tests. +- Prefer existing package patterns over new abstractions. Search for a similar package first. +- Use structured YAML config; do not encode dependency logic in PHP when config fields are enough. +- Match package names exactly: extension packages use `ext-` in config and dependencies; `#[Extension('curl')]` expands to `ext-curl`. +- Use platform suffixes such as `@unix`, `@linux`, `@macos`, and `@windows` instead of runtime conditionals when the difference is declarative. +- Add or update PHP package classes only when build commands, validation, custom configure args, hooks, or source patching are required. +- Do not edit generated build directories (`buildroot/`, `source/`, `downloads/`, `pkgroot/`) as a fix. + +## Validation + +For config or package changes, run CS checks first: + +```bash +bin/spc dev:lint-config +composer cs-fix +``` + +For broader PHP code changes, also run: + +```bash +composer test +composer analyse +``` + +Use build commands only when needed for the task or when the user provided a repro, because full static builds can be slow and platform-sensitive. diff --git a/README-zh.md b/README-zh.md index 3abe0a69..cb5700fd 100755 --- a/README-zh.md +++ b/README-zh.md @@ -1,156 +1,156 @@ -# 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) -[![Discord](https://img.shields.io/discord/nrSRbpMJ?label=Discord&logo=discord&style=flat-square)](https://discord.gg/xf6Rd4pEAk) - -**StaticPHP** 是一个强大的工具,用于构建可移植的可执行文件,包括 PHP、扩展等。 - -> [!IMPORTANT] -> 我们正准备发布 **v3** 版本,届时项目名称将从 **static-php-cli** 更改为 **StaticPHP**。 -> 此分支对应 v3 版本。v2 版本请查看 [v2 分支](https://github.com/crazywhalecc/static-php-cli/tree/main)。 -> 请更新您的参考资料,并关注正式版发布。 - -## 特性 - -- :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-bin/nightly/spc-linux-x86_64 -# For Linux aarch64 -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-linux-aarch64 -# macOS x86_64 (Intel) -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-macos-x86_64 -# macOS aarch64 (Apple) -curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/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-bin/nightly/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。 +# 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) +[![Discord](https://img.shields.io/discord/nrSRbpMJ?label=Discord&logo=discord&style=flat-square)](https://discord.gg/xf6Rd4pEAk) + +**StaticPHP** 是一个强大的工具,用于构建可移植的可执行文件,包括 PHP、扩展等。 + +> [!IMPORTANT] +> 我们正准备发布 **v3** 版本,届时项目名称将从 **static-php-cli** 更改为 **StaticPHP**。 +> 此分支对应 v3 版本。v2 版本请查看 [v2 分支](https://github.com/crazywhalecc/static-php-cli/tree/main)。 +> 请更新您的参考资料,并关注正式版发布。 + +## 特性 + +- :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-bin/nightly/spc-linux-x86_64 +# For Linux aarch64 +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-linux-aarch64 +# macOS x86_64 (Intel) +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-macos-x86_64 +# macOS aarch64 (Apple) +curl -fsSL -o spc https://dl.static-php.dev/v3/spc-bin/nightly/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-bin/nightly/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/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 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: diff --git a/bin/setup-runtime.ps1 b/bin/setup-runtime.ps1 index 4b3a25b7..44e0bb8f 100644 --- a/bin/setup-runtime.ps1 +++ b/bin/setup-runtime.ps1 @@ -1,116 +1,116 @@ -param ( - [string] ${action} -) - -function AddToPath { - param ( - [string]$pathToAdd - ) - - $currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') - - if ($currentPath -notlike "*$pathToAdd*") { - $newPath = $currentPath + ";$pathToAdd" - [System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User') - Write-Host "Added '$pathToAdd' to Path." - Write-Host "To remove path, use: " -NoNewline - Write-Host "bin/setup-runtime remove-path" -ForegroundColor Cyan - } else { - Write-Host "Path already exists." - } -} - -function RemoveFromPath { - param ( - [string]$pathToRemove - ) - - $currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') - - if ($currentPath -like "*$pathToRemove*") { - $newPath = $currentPath -replace [regex]::Escape(';' + $pathToRemove), '' - [System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User') - Write-Host "Removed Path '$pathToRemove'" - } else { - Write-Host "Path '$pathToRemove' not in Path" - } -} - -# working dir -$WorkingDir = (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Definition)) - -if ($action -eq 'add-path') { - AddToPath ($WorkingDir + '\runtime') - exit 0 -} elseif ($action -eq 'remove-path') { - RemoveFromPath ($WorkingDir + '\runtime') - exit 0 -} elseif (-not($action -eq '')) { - Write-Host ("Invalid action: " + $action) -ForegroundColor Red - exit 1 -} - -# get php 8.1 specific version - -# php windows download -$PHPRuntimeUrl = "https://windows.php.net/downloads/releases/archives/php-8.4.4-nts-Win32-vs17-x64.zip" -Write-Host "Downloading PHP from: " -NoNewline -Write-Host $PHPRuntimeUrl -ForegroundColor Cyan -$ComposerUrl = "https://getcomposer.org/download/latest-stable/composer.phar" - -# create dir -New-Item -Path "downloads" -ItemType Directory -Force | Out-Null - -# download php -if (-not(Test-Path "downloads\php.zip")) -{ - Write-Host "Downloading PHP ..." - Invoke-WebRequest $PHPRuntimeUrl -OutFile "downloads\php.zip" -} - -# extract php -New-Item -Path "runtime" -ItemType Directory -Force | Out-Null -Write-Host "Extracting php.zip ..." -Expand-Archive -Path "downloads/php.zip" -DestinationPath "runtime" -Force -# make php.ini -Move-Item -Path "runtime\php.ini-production" -Destination "runtime\php.ini" -Force -$OriginINI = Get-Content -Path "runtime\php.ini" -Raw -$OriginINI = $OriginINI -replace ';extension=openssl', 'extension=openssl' -$OriginINI = $OriginINI -replace ';extension=curl', 'extension=curl' -$OriginINI = $OriginINI -replace ';extension=mbstring', 'extension=mbstring' -$OriginINI = $OriginINI -replace ';extension=sodium', 'extension=sodium' -$OriginINI = $OriginINI -replace ';extension_dir = "./"', ('extension_dir = "' + (Split-Path -Parent $MyInvocation.MyCommand.Definition) + '\..\runtime\ext"') -$OriginINI | Set-Content -Path "runtime\php.ini" - -# download composer -if (-not(Test-Path "runtime\composer.phar")) -{ - Write-Host "Downloading composer ..." - Invoke-WebRequest $ComposerUrl -OutFile "downloads\composer.phar" - Move-Item -Path "downloads\composer.phar" -Destination "runtime\composer.phar" -Force -} - -# create runtime\composer.ps1 -$ComposerContent = ' -$WorkingDir = (Split-Path -Parent $MyInvocation.MyCommand.Definition) -& ($WorkingDir + "\php.exe") (Join-Path $WorkingDir "\composer.phar") @args -' -$ComposerContent | Set-Content -Path 'runtime\composer.ps1' -Encoding UTF8 - -Write-Host "Successfully downloaded PHP and Composer !" -ForegroundColor Green -Write-Host "Use static-php-cli: " -NoNewline -Write-Host "bin/spc" -ForegroundColor Cyan -Write-Host "Use php: " -NoNewline -Write-Host "runtime/php" -ForegroundColor Cyan -Write-Host "Use composer: " -NoNewline -Write-Host "runtime/composer" -ForegroundColor Cyan -Write-Host "" -Write-Host "Don't forget installing composer dependencies '" -NoNewline -Write-Host "runtime/composer install" -ForegroundColor Cyan -NoNewline -Write-Host "' before using static-php-cli !" -Write-Host "" -Write-Host "If you want to use this PHP for quality tools (like phpstan, php-cs-fixer) or other project," -Write-Host "or use PHP, Composer as system executable," -Write-Host "use '" -NoNewline -Write-Host "bin/setup-runtime add-path" -ForegroundColor Cyan -NoNewline -Write-Host "' to add runtime dir in Path." +param ( + [string] ${action} +) + +function AddToPath { + param ( + [string]$pathToAdd + ) + + $currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') + + if ($currentPath -notlike "*$pathToAdd*") { + $newPath = $currentPath + ";$pathToAdd" + [System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + Write-Host "Added '$pathToAdd' to Path." + Write-Host "To remove path, use: " -NoNewline + Write-Host "bin/setup-runtime remove-path" -ForegroundColor Cyan + } else { + Write-Host "Path already exists." + } +} + +function RemoveFromPath { + param ( + [string]$pathToRemove + ) + + $currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') + + if ($currentPath -like "*$pathToRemove*") { + $newPath = $currentPath -replace [regex]::Escape(';' + $pathToRemove), '' + [System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + Write-Host "Removed Path '$pathToRemove'" + } else { + Write-Host "Path '$pathToRemove' not in Path" + } +} + +# working dir +$WorkingDir = (Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Definition)) + +if ($action -eq 'add-path') { + AddToPath ($WorkingDir + '\runtime') + exit 0 +} elseif ($action -eq 'remove-path') { + RemoveFromPath ($WorkingDir + '\runtime') + exit 0 +} elseif (-not($action -eq '')) { + Write-Host ("Invalid action: " + $action) -ForegroundColor Red + exit 1 +} + +# get php 8.1 specific version + +# php windows download +$PHPRuntimeUrl = "https://windows.php.net/downloads/releases/archives/php-8.4.4-nts-Win32-vs17-x64.zip" +Write-Host "Downloading PHP from: " -NoNewline +Write-Host $PHPRuntimeUrl -ForegroundColor Cyan +$ComposerUrl = "https://getcomposer.org/download/latest-stable/composer.phar" + +# create dir +New-Item -Path "downloads" -ItemType Directory -Force | Out-Null + +# download php +if (-not(Test-Path "downloads\php.zip")) +{ + Write-Host "Downloading PHP ..." + Invoke-WebRequest $PHPRuntimeUrl -OutFile "downloads\php.zip" +} + +# extract php +New-Item -Path "runtime" -ItemType Directory -Force | Out-Null +Write-Host "Extracting php.zip ..." +Expand-Archive -Path "downloads/php.zip" -DestinationPath "runtime" -Force +# make php.ini +Move-Item -Path "runtime\php.ini-production" -Destination "runtime\php.ini" -Force +$OriginINI = Get-Content -Path "runtime\php.ini" -Raw +$OriginINI = $OriginINI -replace ';extension=openssl', 'extension=openssl' +$OriginINI = $OriginINI -replace ';extension=curl', 'extension=curl' +$OriginINI = $OriginINI -replace ';extension=mbstring', 'extension=mbstring' +$OriginINI = $OriginINI -replace ';extension=sodium', 'extension=sodium' +$OriginINI = $OriginINI -replace ';extension_dir = "./"', ('extension_dir = "' + (Split-Path -Parent $MyInvocation.MyCommand.Definition) + '\..\runtime\ext"') +$OriginINI | Set-Content -Path "runtime\php.ini" + +# download composer +if (-not(Test-Path "runtime\composer.phar")) +{ + Write-Host "Downloading composer ..." + Invoke-WebRequest $ComposerUrl -OutFile "downloads\composer.phar" + Move-Item -Path "downloads\composer.phar" -Destination "runtime\composer.phar" -Force +} + +# create runtime\composer.ps1 +$ComposerContent = ' +$WorkingDir = (Split-Path -Parent $MyInvocation.MyCommand.Definition) +& ($WorkingDir + "\php.exe") (Join-Path $WorkingDir "\composer.phar") @args +' +$ComposerContent | Set-Content -Path 'runtime\composer.ps1' -Encoding UTF8 + +Write-Host "Successfully downloaded PHP and Composer !" -ForegroundColor Green +Write-Host "Use static-php-cli: " -NoNewline +Write-Host "bin/spc" -ForegroundColor Cyan +Write-Host "Use php: " -NoNewline +Write-Host "runtime/php" -ForegroundColor Cyan +Write-Host "Use composer: " -NoNewline +Write-Host "runtime/composer" -ForegroundColor Cyan +Write-Host "" +Write-Host "Don't forget installing composer dependencies '" -NoNewline +Write-Host "runtime/composer install" -ForegroundColor Cyan -NoNewline +Write-Host "' before using static-php-cli !" +Write-Host "" +Write-Host "If you want to use this PHP for quality tools (like phpstan, php-cs-fixer) or other project," +Write-Host "or use PHP, Composer as system executable," +Write-Host "use '" -NoNewline +Write-Host "bin/setup-runtime add-path" -ForegroundColor Cyan -NoNewline +Write-Host "' to add runtime dir in Path." diff --git a/bin/spc-debug.ps1 b/bin/spc-debug.ps1 index 015dae9c..4ff91d17 100644 --- a/bin/spc-debug.ps1 +++ b/bin/spc-debug.ps1 @@ -1,12 +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 +$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 diff --git a/bin/spc.ps1 b/bin/spc.ps1 index 2efd09ea..cf7769f9 100644 --- a/bin/spc.ps1 +++ b/bin/spc.ps1 @@ -1,12 +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" ("bin/spc") @args -exit $LASTEXITCODE +$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" ("bin/spc") @args +exit $LASTEXITCODE diff --git a/box.json b/box.json index 13b78544..f6ca710e 100644 --- a/box.json +++ b/box.json @@ -10,7 +10,6 @@ "config", "src", "vendor/psr", - "vendor/laravel/prompts", "vendor/symfony", "vendor/php-di", "vendor/zhamao" diff --git a/composer.json b/composer.json index 60c12aaa..88ab5a9c 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,8 @@ "php": ">=8.4", "ext-mbstring": "*", "ext-zlib": "*", - "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.4" }, diff --git a/composer.lock b/composer.lock index 4c4b9e8e..5127cb94 100644 --- a/composer.lock +++ b/composer.lock @@ -4,67 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1d5518bdf7730190aead0e953abff538", + "content-hash": "60c8ac8087e3075e01e78bd03c609017", "packages": [ - { - "name": "laravel/prompts", - "version": "v0.3.17", - "source": { - "type": "git", - "url": "https://github.com/laravel/prompts.git", - "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818", - "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.2", - "ext-mbstring": "*", - "php": "^8.1", - "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|^13.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": { - "ext-pcntl": "Required for the spinner to be animated." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "0.3.x-dev" - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Laravel\\Prompts\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "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.17" - }, - "time": "2026-04-20T16:07:33+00:00" - }, { "name": "laravel/serializable-closure", "version": "v2.0.13", @@ -359,16 +300,16 @@ }, { "name": "symfony/console", - "version": "v7.4.9", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d7d2b64a45a89d607865927b176fa51c33ddbb58", - "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -433,7 +374,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.9" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -453,20 +394,20 @@ "type": "tidelift" } ], - "time": "2026-04-22T15:21:55+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -479,7 +420,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -504,7 +445,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -515,12 +456,16 @@ "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": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/polyfill-ctype", @@ -607,16 +552,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -665,7 +610,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -685,20 +630,20 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:13:48+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.37.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -750,7 +695,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -770,20 +715,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -835,7 +780,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -855,85 +800,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" - }, - { - "name": "symfony/process", - "version": "v7.4.8", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "60f19cd3badc8de688421e21e4305eba50f8089a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a", - "reference": "60f19cd3badc8de688421e21e4305eba50f8089a", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v7.4.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "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": "2026-03-24T13:12:05+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -951,7 +831,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -987,7 +867,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -1007,24 +887,24 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-intl-grapheme": "^1.33", "symfony/polyfill-intl-normalizer": "^1.0", @@ -1077,7 +957,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v8.1.0" }, "funding": [ { @@ -1097,20 +977,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/yaml", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", - "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", "shasum": "" }, "require": { @@ -1153,7 +1033,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.8" + "source": "https://github.com/symfony/yaml/tree/v7.4.13" }, "funding": [ { @@ -1173,7 +1053,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-25T06:06:12+00:00" }, { "name": "zhamao/logger", @@ -1558,16 +1438,16 @@ }, { "name": "amphp/parallel", - "version": "v2.3.3", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" + "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", + "url": "https://api.github.com/repos/amphp/parallel/zipball/37f5b2754fadc229c00f9416bd68fb8d04529a81", + "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81", "shasum": "" }, "require": { @@ -1587,7 +1467,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -1630,7 +1510,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.3" + "source": "https://github.com/amphp/parallel/tree/v2.4.0" }, "funding": [ { @@ -1638,7 +1518,7 @@ "type": "github" } ], - "time": "2025-11-15T06:23:42+00:00" + "time": "2026-05-16T16:54:01+00:00" }, { "name": "amphp/parser", @@ -1704,16 +1584,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" + "reference": "a044733e080940d1483f56caff0c412ad6982776" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", + "reference": "a044733e080940d1483f56caff0c412ad6982776", "shasum": "" }, "require": { @@ -1725,7 +1605,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -1759,7 +1639,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + "source": "https://github.com/amphp/pipeline/tree/v1.2.4" }, "funding": [ { @@ -1767,20 +1647,20 @@ "type": "github" } ], - "time": "2025-03-16T16:33:53+00:00" + "time": "2026-05-06T05:37:57+00:00" }, { "name": "amphp/process", - "version": "v2.0.3", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + "reference": "583959df17d00304ad7b0b32285373f985935643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", "shasum": "" }, "require": { @@ -1794,7 +1674,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -1827,7 +1707,7 @@ "homepage": "https://amphp.org/process", "support": { "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" + "source": "https://github.com/amphp/process/tree/v2.1.0" }, "funding": [ { @@ -1835,7 +1715,7 @@ "type": "github" } ], - "time": "2024-04-19T03:13:44+00:00" + "time": "2026-05-31T15:11:55+00:00" }, { "name": "amphp/serialization", @@ -2242,28 +2122,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -2301,7 +2182,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -2311,13 +2192,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -2556,42 +2433,42 @@ }, { "name": "ergebnis/agent-detector", - "version": "1.1.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/ergebnis/agent-detector.git", - "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64" + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", - "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", "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/composer-normalize": "^2.51.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", + "ergebnis/rector-rules": "^1.18.1", "fakerphp/faker": "^1.24.1", "infection/infection": "^0.26.6", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan": "^2.1.54", "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" + "rector/rector": "^2.4.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.0-dev" + "dev-main": "1.2-dev" }, "composer-normalize": { "indent-size": 2, @@ -2621,7 +2498,7 @@ "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" + "time": "2026-05-07T08:19:07+00:00" }, { "name": "evenement/evenement", @@ -2885,23 +2762,23 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.1", + "version": "v3.95.7", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065" + "reference": "4fa4102a5617acae53f804f7c81475aaa2d6e813" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a9727678fbd12997f1d9de8f4a37824ed9df1065", - "reference": "a9727678fbd12997f1d9de8f4a37824ed9df1065", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4fa4102a5617acae53f804f7c81475aaa2d6e813", + "reference": "4fa4102a5617acae53f804f7c81475aaa2d6e813", "shasum": "" }, "require": { "clue/ndjson-react": "^1.3", "composer/semver": "^3.4", "composer/xdebug-handler": "^3.0.5", - "ergebnis/agent-detector": "^1.1.1", + "ergebnis/agent-detector": "^1.2", "ext-filter": "*", "ext-hash": "*", "ext-json": "*", @@ -2912,32 +2789,32 @@ "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 || ^8.0", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.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/polyfill-mbstring": "^1.37", + "symfony/polyfill-php80": "^1.37", + "symfony/polyfill-php81": "^1.37", + "symfony/polyfill-php84": "^1.37", "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.8.0", - "infection/infection": "^0.32.6", - "justinrainbow/json-schema": "^6.8.0", + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.9.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.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.8", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.8" + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.0", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2978,7 +2855,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.95.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.7" }, "funding": [ { @@ -2986,7 +2863,7 @@ "type": "github" } ], - "time": "2026-04-12T17:00:09+00:00" + "time": "2026-06-13T17:51:53+00:00" }, { "name": "humbug/box", @@ -3235,16 +3112,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.8.2", + "version": "6.9.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76" + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2c89ebb95ca9cedc9347f780333f7b25792dcb76", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/bd1bda2ebfc8bff418565941771ea8f03c557886", + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886", "shasum": "" }, "require": { @@ -3254,7 +3131,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "dev-main", + "json-schema/json-schema-test-suite": "^23.2", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -3304,9 +3181,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.9.0" }, - "time": "2026-05-05T05:39:01+00:00" + "time": "2026-06-05T14:05:24+00:00" }, { "name": "kelunik/certificate", @@ -4347,11 +4224,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.54", + "version": "2.2.2", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", - "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", "shasum": "" }, "require": { @@ -4374,6 +4251,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -4396,7 +4284,7 @@ "type": "github" } ], - "time": "2026-04-29T13:31:09+00:00" + "time": "2026-06-05T09:00:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5514,16 +5402,16 @@ }, { "name": "revolt/event-loop", - "version": "v1.0.8", + "version": "v1.0.9", "source": { "type": "git", "url": "https://github.com/revoltphp/event-loop.git", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" + "reference": "44061cf513e53c6200372fc935ac42271566295d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", "shasum": "" }, "require": { @@ -5533,7 +5421,7 @@ "ext-json": "*", "jetbrains/phpstorm-stubs": "^2019.3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" + "psalm/phar": "6.16.*" }, "type": "library", "extra": { @@ -5580,9 +5468,9 @@ ], "support": { "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" }, - "time": "2025-08-27T21:33:23+00:00" + "time": "2026-05-16T17:55:38+00:00" }, { "name": "sebastian/cli-parser", @@ -6539,16 +6427,16 @@ }, { "name": "seld/jsonlint", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", - "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", "shasum": "" }, "require": { @@ -6587,7 +6475,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" }, "funding": [ { @@ -6599,7 +6487,7 @@ "type": "tidelift" } ], - "time": "2024-07-11T14:55:45+00:00" + "time": "2026-06-12T11:32:29+00:00" }, { "name": "seld/phar-utils", @@ -6651,20 +6539,21 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.0.9", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f" + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f", - "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { @@ -6712,7 +6601,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.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" }, "funding": [ { @@ -6732,20 +6621,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:51:42+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -6759,7 +6648,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -6792,7 +6681,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -6803,25 +6692,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": "2024-09-25T14:21:43+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.9", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/dcd8f96bcdc0f128ec406c765cc066c6035d1be3", - "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { @@ -6858,7 +6751,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.9" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -6878,7 +6771,7 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/finder", @@ -6950,20 +6843,20 @@ }, { "name": "symfony/options-resolver", - "version": "v8.0.8", + "version": "v8.1.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8" + "reference": "88f9c561f678a02d54b897014049fa839e33ff82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8", - "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.4.1", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -6997,7 +6890,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.8" + "source": "https://github.com/symfony/options-resolver/tree/v8.1.0" }, "funding": [ { @@ -7017,7 +6910,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/polyfill-iconv", @@ -7105,16 +6998,16 @@ }, { "name": "symfony/polyfill-php84", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -7161,7 +7054,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -7181,24 +7074,89 @@ "type": "tidelift" } ], - "time": "2026-04-10T18:47:49+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { - "name": "symfony/stopwatch", - "version": "v8.0.8", + "name": "symfony/process", + "version": "v7.4.13", "source": { "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3" + "url": "https://github.com/symfony/process.git", + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/85954ed72d5440ea4dc9a10b7e49e01df766ffa3", - "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "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": "2026-05-23T16:05:06+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "21c07b026905d596e8379caeb115d87aa479499d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/21c07b026905d596e8379caeb115d87aa479499d", + "reference": "21c07b026905d596e8379caeb115d87aa479499d", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -7227,7 +7185,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v8.0.8" + "source": "https://github.com/symfony/stopwatch/tree/v8.1.0" }, "funding": [ { @@ -7247,7 +7205,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-05-29T05:06:50+00:00" }, { "name": "symfony/var-dumper", diff --git a/config/artifact/ncurses.yml b/config/artifact/ncurses.yml index 8d39e81b..c13e8787 100644 --- a/config/artifact/ncurses.yml +++ b/config/artifact/ncurses.yml @@ -3,6 +3,10 @@ ncurses: license-files: - COPYING source: + type: filelist + url: 'https://ftp.gnu.org/gnu/ncurses/' + regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' + source-mirror: type: filelist url: 'https://ftpmirror.gnu.org/gnu/ncurses/' regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' diff --git a/config/env.ini b/config/env.ini index 483ae408..0be1abdf 100644 --- a/config/env.ini +++ b/config/env.ini @@ -53,8 +53,8 @@ PHP_BUILD_PROVIDER="StaticPHP ${SPC_VERSION}" 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" +; Preserve old logs when running new builds (code checks SPC_PRESERVE_LOG, not SPC_PRESERVE_LOGS) +SPC_PRESERVE_LOG="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 @@ -68,8 +68,8 @@ SPC_PRESERVE_LOGS="no" [windows] ; build target: win7-static SPC_TARGET=native-windows -; php-sdk-binary-tools path -PHP_SDK_PATH="${WORKING_DIR}\php-sdk-binary-tools" +; MSYS2 root directory (msys64 subfolder), used by the Windows toolchain +SPC_MSYS2_PATH="${PKG_ROOT_PATH}\msys2-build-essentials\msys64" ; upx executable path UPX_EXEC="${PKG_ROOT_PATH}\bin\upx.exe" ; phpmicro patches, for more info, see: https://github.com/easysoft/phpmicro/tree/master/patches diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml index 7ed8981d..41a32fb9 100644 --- a/config/pkg/ext/ext-gmssl.yml +++ b/config/pkg/ext/ext-gmssl.yml @@ -10,3 +10,5 @@ ext-gmssl: license: PHP-3.01 depends: - gmssl + php-extension: + arg-type: with-path diff --git a/config/pkg/ext/ext-zip.yml b/config/pkg/ext/ext-zip.yml index 9a4b0282..3b164e38 100644 --- a/config/pkg/ext/ext-zip.yml +++ b/config/pkg/ext/ext-zip.yml @@ -10,6 +10,11 @@ ext-zip: license: PHP-3.01 depends: - libzip + depends@windows: + - libzip + - zlib + - xz + - bzip2 php-extension: arg-type: custom arg-type@windows: enable diff --git a/config/pkg/lib/gettext.yml b/config/pkg/lib/gettext.yml index 04743033..5e81f795 100644 --- a/config/pkg/lib/gettext.yml +++ b/config/pkg/lib/gettext.yml @@ -2,6 +2,10 @@ gettext: type: library artifact: source: + type: filelist + url: 'https://ftp.gnu.org/gnu/gettext/' + regex: '/href="(?gettext-(?[^"]+)\.tar\.xz)"/' + source-mirror: type: filelist url: 'https://ftpmirror.gnu.org/gnu/gettext/' regex: '/href="(?gettext-(?[^"]+)\.tar\.xz)"/' diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml index 80303056..2d70c7ee 100644 --- a/config/pkg/lib/gmp.yml +++ b/config/pkg/lib/gmp.yml @@ -3,11 +3,12 @@ gmp: artifact: source: type: filelist - url: 'https://ftpmirror.gnu.org/gnu/gmp/' + url: 'https://ftp.gnu.org/gnu/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' + type: filelist + url: 'https://ftpmirror.gnu.org/gnu/gmp/' + regex: '/href="(?gmp-(?[^"]+)\.tar\.xz)"/' metadata: license-files: ['@/gmp.txt'] license: Custom diff --git a/config/pkg/lib/idn2.yml b/config/pkg/lib/idn2.yml index 3b597b4d..6a152467 100644 --- a/config/pkg/lib/idn2.yml +++ b/config/pkg/lib/idn2.yml @@ -2,6 +2,10 @@ idn2: type: library artifact: source: + type: filelist + url: 'https://ftp.gnu.org/gnu/libidn/' + regex: '/href="(?libidn2-(?[^"]+)\.tar\.gz)"/' + source-mirror: type: filelist url: 'https://ftpmirror.gnu.org/gnu/libidn/' regex: '/href="(?libidn2-(?[^"]+)\.tar\.gz)"/' diff --git a/config/pkg/lib/libiconv.yml b/config/pkg/lib/libiconv.yml index 2893679c..5b21f603 100644 --- a/config/pkg/lib/libiconv.yml +++ b/config/pkg/lib/libiconv.yml @@ -2,6 +2,10 @@ libiconv: type: library artifact: source: + type: filelist + url: 'https://ftp.gnu.org/gnu/libiconv/' + regex: '/href="(?libiconv-(?[^"]+)\.tar\.gz)"/' + source-mirror: type: filelist url: 'https://ftpmirror.gnu.org/gnu/libiconv/' regex: '/href="(?libiconv-(?[^"]+)\.tar\.gz)"/' diff --git a/config/pkg/lib/libssh2.yml b/config/pkg/lib/libssh2.yml index 72447416..fa43fb27 100644 --- a/config/pkg/lib/libssh2.yml +++ b/config/pkg/lib/libssh2.yml @@ -9,8 +9,10 @@ libssh2: metadata: license-files: [COPYING] license: BSD-3-Clause - depends: + depends@unix: - openssl + depends@windows: + - zlib headers: - libssh2.h - libssh2_publickey.h diff --git a/config/pkg/lib/libunistring.yml b/config/pkg/lib/libunistring.yml index 5a78fa14..47530c6a 100644 --- a/config/pkg/lib/libunistring.yml +++ b/config/pkg/lib/libunistring.yml @@ -2,6 +2,10 @@ libunistring: type: library artifact: source: + type: filelist + url: 'https://ftp.gnu.org/gnu/libunistring/' + regex: '/href="(?libunistring-(?[^"]+)\.tar\.gz)"/' + source-mirror: type: filelist url: 'https://ftpmirror.gnu.org/gnu/libunistring/' regex: '/href="(?libunistring-(?[^"]+)\.tar\.gz)"/' 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/lib/readline.yml b/config/pkg/lib/readline.yml index 2dd25e49..9b604571 100644 --- a/config/pkg/lib/readline.yml +++ b/config/pkg/lib/readline.yml @@ -2,6 +2,10 @@ readline: type: library artifact: source: + type: filelist + url: 'https://ftp.gnu.org/gnu/readline/' + regex: '/href="(?readline-(?[^"]+)\.tar\.gz)"/' + source-mirror: type: filelist url: 'https://ftpmirror.gnu.org/gnu/readline/' regex: '/href="(?readline-(?[^"]+)\.tar\.gz)"/' 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/config/pkg/target/msys2-build-essentials.yml b/config/pkg/target/msys2-build-essentials.yml new file mode 100644 index 00000000..39f10fe0 --- /dev/null +++ b/config/pkg/target/msys2-build-essentials.yml @@ -0,0 +1,8 @@ +msys2-build-essentials: + type: target + artifact: + binary: custom + env: + SPC_MSYS2_PATH: '{pkg_root_path}/msys2-build-essentials/msys64' + path@windows: + - '{pkg_root_path}/msys2-build-essentials/msys64/usr/bin' diff --git a/config/pkg/target/nasm.yml b/config/pkg/target/nasm.yml deleted file mode 100644 index 3f483e8b..00000000 --- a/config/pkg/target/nasm.yml +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 81180007..00000000 --- a/config/pkg/target/php-sdk-binary-tools.yml +++ /dev/null @@ -1,5 +0,0 @@ -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/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/tool/7za-win.yml b/config/pkg/tool/7za-win.yml new file mode 100644 index 00000000..c8d35bfe --- /dev/null +++ b/config/pkg/tool/7za-win.yml @@ -0,0 +1,9 @@ +7za-win: + 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/tool/nasm.yml b/config/pkg/tool/nasm.yml new file mode 100644 index 00000000..928adc2b --- /dev/null +++ b/config/pkg/tool/nasm.yml @@ -0,0 +1,10 @@ +nasm: + 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/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/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/docs/en/develop/package-model.md b/docs/en/develop/package-model.md index 2e9a290d..4bac3de2 100644 --- a/docs/en/develop/package-model.md +++ b/docs/en/develop/package-model.md @@ -229,7 +229,7 @@ The following path placeholders are supported in string values of the `path`, `e | `{working_dir}` | Working directory (project root) | | `{download_path}` | Download cache directory (`downloads/`) | | `{source_path}` | Extracted source directory (`source/`) | -| `{php_sdk_path}` | Windows PHP SDK directory | +| `{spc_msys2_path}` | MSYS2 root directory (`msys64/`) — Windows only | ## target Package Type diff --git a/docs/en/guide/migrate-from-v2.md b/docs/en/guide/migrate-from-v2.md index 54bfa163..3fee7bd3 100644 --- a/docs/en/guide/migrate-from-v2.md +++ b/docs/en/guide/migrate-from-v2.md @@ -58,7 +58,13 @@ A single-file hook API for lightweight patches may be provided in a future relea ### Windows-only: `--with-sdk-binary-dir` and `--vs-ver` -These options are no longer accepted on the command line. Instead, set the `PHP_SDK_PATH` environment variable to point to your PHP SDK binary tools directory. The Visual Studio version is now managed by the toolchain configuration. +These options are no longer accepted on the command line. In v3, the `php-sdk-binary-tools` dependency has been completely removed. v3 now manages its own **MSYS2** environment to support autotools-based library builds on Windows. Run `spc doctor --install` to download and configure MSYS2 automatically. + +If you need to point to a custom MSYS2 installation, set the `SPC_MSYS2_PATH` environment variable to the `msys64` directory (e.g. `C:\msys64`). Visual Studio is now auto-detected by the toolchain — no manual version flag needed. + +::: warning Migrating from v2 +v2 relied on `php-sdk-binary-tools` and required `--with-sdk-binary-dir` and `--vs-ver` on every build invocation. In v3 these options are gone. Remove them from all CI scripts and run `spc doctor --install` once to set up the Windows build environment. +::: ## Renamed / Deprecated Options diff --git a/docs/zh/develop/package-model.md b/docs/zh/develop/package-model.md index c7fa9dff..af87a8da 100644 --- a/docs/zh/develop/package-model.md +++ b/docs/zh/develop/package-model.md @@ -223,7 +223,7 @@ openssl: | `{working_dir}` | 工作目录(项目根目录) | | `{download_path}` | 下载缓存目录(`downloads/`) | | `{source_path}` | 解压源码目录(`source/`) | -| `{php_sdk_path}` | Windows PHP SDK 目录 | +| `{spc_msys2_path}` | MSYS2 根目录(`msys64/`)——仅 Windows | ## target 包类型 diff --git a/docs/zh/guide/migrate-from-v2.md b/docs/zh/guide/migrate-from-v2.md index 81297acf..805a8549 100644 --- a/docs/zh/guide/migrate-from-v2.md +++ b/docs/zh/guide/migrate-from-v2.md @@ -58,7 +58,13 @@ curl -o spc https://dl.static-php.dev/v3/spc-bin/nightly/spc-linux-x86_64 ### Windows 专有:`--with-sdk-binary-dir` 和 `--vs-ver` -这两个选项已不再被命令行接受。请改为设置 `PHP_SDK_PATH` 环境变量,指向你的 PHP SDK binary tools 目录。Visual Studio 版本现在由工具链配置统一管理。 +这两个选项已不再被命令行接受。在 v3 中,`php-sdk-binary-tools` 依赖已被完全移除。v3 现在通过管理自己的 **MSYS2** 环境来支持 Windows 上基于 autotools 的库构建。运行 `spc doctor --install` 即可自动下载并配置 MSYS2。 + +如需指向自定义 MSYS2 安装目录,请设置 `SPC_MSYS2_PATH` 环境变量,值为 `msys64` 目录路径(例如 `C:\msys64`)。Visual Studio 版本现在由工具链自动检测,无需手动指定版本号。 + +::: warning 从 v2 迁移 +v2 依赖 `php-sdk-binary-tools`,并在每次构建时需要传入 `--with-sdk-binary-dir` 和 `--vs-ver` 参数。在 v3 中这些选项已被移除。请从所有 CI 脚本中删除这些参数,并使用 `spc doctor --install` 一次性完成 Windows 构建环境的配置。 +::: ## 已重命名 / 已弃用的选项 diff --git a/phpstan.neon b/phpstan.neon index f54fc35f..46efc171 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,6 @@ parameters: reportUnmatchedIgnoredErrors: false - level: 4 + level: 5 phpVersion: 80400 paths: - ./src/ 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/Artifact/msys2_build_essentials.php b/src/Package/Artifact/msys2_build_essentials.php new file mode 100644 index 00000000..87d52218 --- /dev/null +++ b/src/Package/Artifact/msys2_build_essentials.php @@ -0,0 +1,93 @@ +executeCurlDownload($url, $path, retries: $downloader->getRetry()); + + return DownloadResult::file( + $filename, + ['url' => $url, 'version' => 'nightly'], + version: 'nightly', + extract: '{pkg_root_path}/msys2-build-essentials', + ); + } + + #[BinaryExtract('msys2-build-essentials', ['windows-x86_64'])] + public function extractBinary(string $source_file, string $target_path): void + { + $target_path = FileSystem::convertPath($target_path); + $source_file = FileSystem::convertPath($source_file); + + // Guard: skip re-extraction if already initialized (marker written at end of this method). + $marker = "{$target_path}\\.spc-msys2-initialized"; + if (file_exists($marker)) { + return; + } + + if (!is_dir($target_path)) { + FileSystem::createDir($target_path); + } + + cmd()->exec("\"{$source_file}\" -y -o\"{$target_path}\""); + + $msys2_bin = "{$target_path}\\msys64\\usr\\bin"; + if (!file_exists("{$msys2_bin}\\bash.exe")) { + throw new DownloaderException("MSYS2 extraction failed: bash.exe not found at {$msys2_bin}\\bash.exe"); + } + + // Add MSYS2 usr\bin to PATH so pacman.exe can load msys-2.0.dll. + GlobalEnvManager::addPathIfNotExists($msys2_bin); + GlobalEnvManager::putenv('CHERE_INVOKING=yes'); + GlobalEnvManager::putenv('MSYSTEM=MSYS'); + + // Disable PGP signature checking: pacman-key --init requires a pseudo-TTY which is unavailable + // from PHP. Patching pacman.conf is the standard approach for CI pipelines. + $pacman_conf = "{$target_path}\\msys64\\etc\\pacman.conf"; + FileSystem::replaceFileRegex($pacman_conf, '/^SigLevel\s*=.*$/m', 'SigLevel = Never'); + + $pacman = "{$target_path}\\msys64\\usr\\bin\\pacman.exe"; + + // Two-pass update as recommended by MSYS2 CI docs. + cmd()->exec("\"{$pacman}\" --noconfirm -Syuu"); + cmd()->exec("\"{$pacman}\" --noconfirm -Syuu"); + + $pkgs = implode(' ', self::REQUIRED_PACKAGES); + cmd()->exec("\"{$pacman}\" --noconfirm -S --needed {$pkgs}"); + + FileSystem::writeFile($marker, date('Y-m-d H:i:s')); + } + + #[AfterBinaryExtract('msys2-build-essentials', ['windows-x86_64'])] + public function afterExtract(string $target_path): void + { + $target_path = FileSystem::convertPath($target_path); + $msys2_root = "{$target_path}\\msys64"; + + GlobalEnvManager::putenv("SPC_MSYS2_PATH={$msys2_root}"); + GlobalEnvManager::addPathIfNotExists("{$msys2_root}\\usr\\bin"); + } +} diff --git a/src/Package/Extension/curl.php b/src/Package/Extension/curl.php index eef43a56..fc194491 100644 --- a/src/Package/Extension/curl.php +++ b/src/Package/Extension/curl.php @@ -20,7 +20,7 @@ class curl // which requires secur32.lib for SSL/TLS functions (SslEncryptPackage, etc.). $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; if (!str_contains($extra_libs, 'secur32.lib')) { - putenv('SPC_EXTRA_LIBS=' . trim($extra_libs . ' secur32.lib')); + putenv('SPC_EXTRA_LIBS=' . trim("{$extra_libs} secur32.lib")); } } } diff --git a/src/Package/Extension/gmssl.php b/src/Package/Extension/gmssl.php new file mode 100644 index 00000000..a230f407 --- /dev/null +++ b/src/Package/Extension/gmssl.php @@ -0,0 +1,57 @@ += 3.1.0 where SM2_VERIFY_CTX was removed (unified into SM2_SIGN_CTX)')] + public function patchSm2VerifyCtx(): void + { + // See: https://github.com/crazywhalecc/static-php-cli/issues/1182 + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/gmssl.c", + 'SM2_VERIFY_CTX', + 'SM2_SIGN_CTX' + ); + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-gmssl')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-gmssl')] + #[PatchDescription('Fix ext-gmssl v1.1.1: pbkdf2_hmac_sm3_genkey was renamed to sm3_pbkdf2 in GmSSL >= 3.2.0')] + public function patchPbkdf2Rename(): void + { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/gmssl.c", + 'pbkdf2_hmac_sm3_genkey', + 'sm3_pbkdf2' + ); + } + + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-gmssl')] + #[PatchDescription('Add CHECK_LIB to config.w32 for static Windows builds')] + public function patchBeforeBuildconfWin(): bool + { + $configW32 = "{$this->getSourceDir()}/config.w32"; + if (str_contains(FileSystem::readFile($configW32), 'CHECK_LIB(')) { + return false; + } + FileSystem::replaceFileStr( + $configW32, + 'AC_DEFINE(', + 'CHECK_LIB("gmssl.lib", "gmssl", PHP_GMSSL);' . PHP_EOL . 'AC_DEFINE(' + ); + return true; + } +} diff --git a/src/Package/Extension/pdo_sqlite.php b/src/Package/Extension/pdo_sqlite.php deleted file mode 100644 index b0429f62..00000000 --- a/src/Package/Extension/pdo_sqlite.php +++ /dev/null @@ -1,25 +0,0 @@ -getTargetPackage('php')->getSourceDir()}/configure", - '/sqlite3_column_table_name=yes/', - 'sqlite3_column_table_name=no' - ); - } -} diff --git a/src/Package/Extension/simdjson.php b/src/Package/Extension/simdjson.php index e04c415a..4939c811 100644 --- a/src/Package/Extension/simdjson.php +++ b/src/Package/Extension/simdjson.php @@ -50,8 +50,9 @@ class simdjson extends PhpExtensionPackage 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'; + + $env['CFLAGS'] .= ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512'; + $env['CXXFLAGS'] .= ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512'; } return $env; } @@ -63,8 +64,8 @@ class simdjson extends PhpExtensionPackage 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')); + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags . ' -fno-sanitize=undefined -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')); + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim($extra_cxxflags . ' -fno-sanitize=undefined -Xclang -target-feature -Xclang +evex512')); } } diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php index c269ba54..10fb1928 100644 --- a/src/Package/Extension/swoole.php +++ b/src/Package/Extension/swoole.php @@ -30,6 +30,7 @@ class swoole extends PhpExtensionPackage if ($installer->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.'); 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/Package/Library/gettext_win.php b/src/Package/Library/gettext_win.php index 093e4a3a..acb4946a 100644 --- a/src/Package/Library/gettext_win.php +++ b/src/Package/Library/gettext_win.php @@ -22,6 +22,7 @@ class gettext_win { $ver = WindowsUtil::findVisualStudio(); $vs_ver_dir = match ($ver['major_version']) { + '18', // VS 2026 reuses the VS2022 (MSVC17) solution, which msbuild builds via forward compatibility. '17' => '\MSVC17', '16' => '\MSVC16', default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), @@ -44,7 +45,9 @@ class gettext_win { $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'); + // WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot + // read ("is not a native COFF file"); disable it so the .lib stays plain COFF. + ->exec('msbuild libintl_static.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=10.0 /p:WholeProgramOptimization=false'); FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); // libintl_a.lib is the static library output; copy as libintl.lib for linker compatibility diff --git a/src/Package/Library/gmssl.php b/src/Package/Library/gmssl.php index 7785e55a..eff598a3 100644 --- a/src/Package/Library/gmssl.php +++ b/src/Package/Library/gmssl.php @@ -18,7 +18,9 @@ class gmssl #[BuildFor('Darwin')] public function build(LibraryPackage $lib): void { - UnixCMakeExecutor::create($lib)->build(); + UnixCMakeExecutor::create($lib) + ->addConfigureArgs('-DENABLE_SM2_PRIVATE_KEY_EXPORT=ON') + ->build(); } #[BuildFor('Windows')] @@ -33,6 +35,7 @@ class gmssl '-G "NMake Makefiles"', '-DWIN32=ON', '-DBUILD_SHARED_LIBS=OFF', + '-DENABLE_SM2_PRIVATE_KEY_EXPORT=ON', '-DCMAKE_BUILD_TYPE=Release', '-DCMAKE_C_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG"', '-DCMAKE_CXX_FLAGS_RELEASE="/MT /O2 /Ob2 /DNDEBUG"', @@ -42,13 +45,13 @@ class gmssl ->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 gmssl XCFLAGS=/MT'); - cmd()->cd($buildDir)->exec('nmake install XCFLAGS=/MT'); + $libPath = "{$lib->getBuildRootPath()}/lib"; + $incPath = "{$lib->getBuildRootPath()}/include/gmssl"; + FileSystem::createDir($libPath); + FileSystem::createDir($incPath); + FileSystem::copy("{$buildDir}\\bin\\gmssl.lib", "{$libPath}/gmssl.lib"); + FileSystem::copyDir("{$lib->getSourceDir()}\\include\\gmssl", $incPath); } } diff --git a/src/Package/Library/libde265.php b/src/Package/Library/libde265.php index d0fd83a5..c2134aa7 100644 --- a/src/Package/Library/libde265.php +++ b/src/Package/Library/libde265.php @@ -20,6 +20,7 @@ class libde265 extends LibraryPackage ->addConfigureArgs( '-DENABLE_SDL=OFF', '-DENABLE_DECODER=OFF', + '-DENABLE_SIMD=OFF', '-DHAVE_NEON=OFF', ) ->build(); diff --git a/src/Package/Library/libffi_win.php b/src/Package/Library/libffi_win.php index 12faaabe..9e1640d8 100644 --- a/src/Package/Library/libffi_win.php +++ b/src/Package/Library/libffi_win.php @@ -21,6 +21,7 @@ class libffi_win { $ver = WindowsUtil::findVisualStudio(); $vs_ver_dir = match ($ver['major_version']) { + '18', // VS 2026 reuses the vs17 solution, which msbuild builds via forward compatibility. '17' => '\win32\vs17_x64', '16' => '\win32\vs16_x64', default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported!"), @@ -33,7 +34,9 @@ class libffi_win { $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'); + // WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot + // read ("is not a native COFF file"); disable it so the .lib stays plain COFF. + ->exec('msbuild libffi-msvc.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WholeProgramOptimization=false'); FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); diff --git a/src/Package/Library/libiconv_win.php b/src/Package/Library/libiconv_win.php index b6b0531d..630366c2 100644 --- a/src/Package/Library/libiconv_win.php +++ b/src/Package/Library/libiconv_win.php @@ -21,6 +21,7 @@ class libiconv_win { $ver = WindowsUtil::findVisualStudio(); $vs_ver_dir = match ($ver['major_version']) { + '18', // VS 2026 reuses the VS2022 (MSVC17) solution, which msbuild builds via forward compatibility. '17' => '\MSVC17', '16' => '\MSVC16', default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), @@ -33,7 +34,9 @@ class libiconv_win { $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'); + // WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot + // read ("is not a native COFF file"); disable it so the .lib stays plain COFF. + ->exec('msbuild libiconv.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WholeProgramOptimization=false'); FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\x64\\lib\\libiconv.lib", "{$lib->getLibDir()}\\libiconv.lib"); diff --git a/src/Package/Library/libsodium.php b/src/Package/Library/libsodium.php index 0d4c8afe..bd70f7be 100644 --- a/src/Package/Library/libsodium.php +++ b/src/Package/Library/libsodium.php @@ -42,13 +42,16 @@ class libsodium { $ver = WindowsUtil::findVisualStudio(); $vs_ver_dir = match ($ver['major_version']) { + '18', // VS 2026 reuses the vs2022 solution, which msbuild builds via forward compatibility. '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"'); + // WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot + // read ("is not a native COFF file"); disable it so the .lib stays plain COFF. + ->exec('msbuild libsodium.sln /t:Rebuild /p:Configuration=StaticRelease /p:Platform=x64 /p:WholeProgramOptimization=false /p:PreprocessorDefinitions="SODIUM_STATIC=1"'); FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); diff --git a/src/Package/Library/libssh2.php b/src/Package/Library/libssh2.php index b41434e0..27761d7f 100644 --- a/src/Package/Library/libssh2.php +++ b/src/Package/Library/libssh2.php @@ -18,6 +18,7 @@ class libssh2 { WindowsCMakeExecutor::create($lib) ->addConfigureArgs( + '-DCRYPTO_BACKEND=WinCNG', '-DENABLE_ZLIB_COMPRESSION=ON', '-DBUILD_TESTING=OFF' ) diff --git a/src/Package/Library/mpir.php b/src/Package/Library/mpir.php index 8063b782..53cf8658 100644 --- a/src/Package/Library/mpir.php +++ b/src/Package/Library/mpir.php @@ -21,6 +21,7 @@ class mpir { $ver = WindowsUtil::findVisualStudio(); $vs_ver_dir = match ($ver['major_version']) { + '18', // VS 2026 reuses the build.vc17 solution, which msbuild builds via forward compatibility. '17' => '\build.vc17', '16' => '\build.vc16', default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"), diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php index e0493ea5..5ad5049d 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -24,7 +24,7 @@ class openssl { if (SystemTarget::getTargetOS() === 'Windows') { global $argv; - $perl_path_native = PKG_ROOT_PATH . '\strawberry-perl-' . arch2gnu(php_uname('m')) . '-win\perl\bin\perl.exe'; + $perl_path_native = PKG_ROOT_PATH . '\strawberry-perl\perl\bin\perl.exe'; $perl = file_exists($perl_path_native) ? ($perl_path_native) : WindowsUtil::findCommand('perl.exe'); if ($perl === null) { throw new EnvironmentException( diff --git a/src/Package/Library/sqlite.php b/src/Package/Library/sqlite.php index a3d15f9b..9d4ce0f5 100644 --- a/src/Package/Library/sqlite.php +++ b/src/Package/Library/sqlite.php @@ -19,7 +19,12 @@ class sqlite #[BuildFor('Linux')] public function buildUnix(LibraryPackage $lib): void { - UnixAutoconfExecutor::create($lib)->configure()->make(); + UnixAutoconfExecutor::create($lib) + ->appendEnv([ + 'CFLAGS' => '-DSQLITE_ENABLE_COLUMN_METADATA=1', + ]) + ->configure() + ->make(); $lib->patchPkgconfPrefix(['sqlite3.pc']); } diff --git a/src/Package/Target/curl.php b/src/Package/Target/curl.php index 35898254..39945cdc 100644 --- a/src/Package/Target/curl.php +++ b/src/Package/Target/curl.php @@ -40,7 +40,7 @@ class curl ->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI')) ->addConfigureArgs( '-DBUILD_CURL_EXE=ON', - '-DZSTD_LIBRARY=zstd_static.lib', + '-DZSTD_LIBRARY=' . BUILD_LIB_PATH . '/zstd_static.lib', '-DBUILD_TESTING=OFF', '-DBUILD_EXAMPLES=OFF', '-DBUILD_LIBCURL_DOCS=OFF', diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index a4ae5c56..9efd5867 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -255,6 +255,13 @@ class php extends TargetPackage $installer->addBuildPackage('php-embed'); } + // 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')) { + $installer->addInstallPackage('upx'); + } + return [...$extensions_pkg, ...$additional_packages]; } diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index d2f86332..7697ce4b 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -39,6 +39,13 @@ trait windows InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat')); cmd()->cd($package->getSourceDir())->exec('.\buildconf.bat'); + // Bypass the phpsdk_version check in configure.js: we use MSVC + msys2 instead of PHP SDK, so phpsdk_version is not available and the check would always fail. + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\configure.js", + 'check_binary_tools_sdk();', + '/* check_binary_tools_sdk(); skipped: using MSVC + msys2 without PHP SDK */' + ); + if ($package->getBuildOption('enable-micro-win32') && $installer->isPackageResolved('php-micro')) { SourcePatcher::patchMicroWin32(); } else { @@ -88,6 +95,17 @@ trait windows cmd()->cd($package->getSourceDir())->exec(".\\configure.bat {$args} {$static_extension_str}"); } + #[BeforeStage('php', [self::class, 'makeCliForWindows'])] + #[PatchDescription('Patch Makefile to ensure buildroot/include comes before extension CFLAGS (fixes zip.h conflict with minizip)')] + public function patchMakefileIncludeOrder(TargetPackage $package): void + { + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\Makefile", + '$(CFLAGS_PHP_OBJ) $(CFLAGS)', + '$(CFLAGS) $(CFLAGS_PHP_OBJ)' + ); + } + #[BeforeStage('php', [self::class, 'makeCliForWindows'])] #[PatchDescription('Patch Windows Makefile for CLI target')] public function patchCLITarget(TargetPackage $package): void @@ -512,6 +530,7 @@ HEADER; $vc_matches = ['unknown', 'unknown']; } else { $vc_matches = match ($vc['major_version']) { + '18', // VS 2026 shares the VS2022 (v143) runtime conventions, so it reports as VS17. '17' => ['VS17', 'Visual C++ 2022'], '16' => ['VS16', 'Visual C++ 2019'], default => ['unknown', 'unknown'], diff --git a/src/Package/Target/pkgconfig.php b/src/Package/Tool/pkgconfig.php similarity index 83% rename from src/Package/Target/pkgconfig.php rename to src/Package/Tool/pkgconfig.php index aa75fc88..1cf84ef2 100644 --- a/src/Package/Target/pkgconfig.php +++ b/src/Package/Tool/pkgconfig.php @@ -2,17 +2,17 @@ declare(strict_types=1); -namespace Package\Target; +namespace Package\Tool; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\InitPackage; -use StaticPHP\Attribute\Package\Target; +use StaticPHP\Attribute\Package\Tool; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Package\TargetPackage; +use StaticPHP\Package\ToolPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Toolchain\Interface\ToolchainInterface; -#[Target('pkg-config')] +#[Tool('pkg-config')] class pkgconfig { #[InitPackage] @@ -23,7 +23,7 @@ class pkgconfig #[BuildFor('Linux')] #[BuildFor('Darwin')] - public function build(TargetPackage $package, ToolchainInterface $toolchain): void + public function build(ToolPackage $package, ToolchainInterface $toolchain): void { UnixAutoconfExecutor::create($package) ->appendEnv([ diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 224bcff3..666939fe 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -644,7 +644,7 @@ class Artifact '{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', + '{spc_msys2_path}' => getenv('SPC_MSYS2_PATH'), '{working_dir}' => WORKING_DIR, '{download_path}' => DOWNLOAD_PATH, '{source_path}' => SOURCE_PATH, 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/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 987ec554..3630981a 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -614,7 +614,7 @@ class ArtifactExtractor '{source_path}' => SOURCE_PATH, '{download_path}' => DOWNLOAD_PATH, '{working_dir}' => WORKING_DIR, - '{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: '', + '{spc_msys2_path}' => getenv('SPC_MSYS2_PATH') ?: '', ]; return str_replace(array_keys($replacement), array_values($replacement), $path); } diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index 2efe6945..9115fb59 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -76,9 +76,10 @@ class DownloadResult ?string $version = null, array $metadata = [], ?string $downloader = null, + mixed $extract = null, ): DownloadResult { $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } /** diff --git a/src/StaticPHP/Artifact/Downloader/Type/FileList.php b/src/StaticPHP/Artifact/Downloader/Type/FileList.php index b868210d..4c895515 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/FileList.php +++ b/src/StaticPHP/Artifact/Downloader/Type/FileList.php @@ -45,7 +45,11 @@ class FileList implements DownloadTypeInterface, CheckUpdateInterface throw new DownloaderException("Failed to get {$name} file list from {$config['url']}"); } $versions = []; - logger()->debug('Matched ' . count($matches['version']) . " versions for {$name}"); + $cnt = count($matches['version']); + if ($cnt === 0) { + throw new DownloaderException("Failed to get {$name} file list from {$config['url']}: no version parsed"); + } + logger()->debug("Matched {$cnt} versions for {$name}"); foreach ($matches['version'] as $i => $version) { $lower = strtolower($version); foreach (['alpha', 'beta', 'rc', 'pre', 'nightly', 'snapshot', 'dev'] as $beta) { diff --git a/src/StaticPHP/Attribute/Package/Tool.php b/src/StaticPHP/Attribute/Package/Tool.php new file mode 100644 index 00000000..35442d45 --- /dev/null +++ b/src/StaticPHP/Attribute/Package/Tool.php @@ -0,0 +1,14 @@ + ['PHP Warning: ', 'warning'], E_NOTICE => ['PHP Notice: ', 'notice'], 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/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php index f390e433..4ec67e6b 100644 --- a/src/StaticPHP/Command/CraftCommand.php +++ b/src/StaticPHP/Command/CraftCommand.php @@ -36,6 +36,15 @@ class CraftCommand extends BaseCommand // set verbosity $this->output->setVerbosity($craft['verbosity']); + // sync logger level and ApplicationContext debug mode to match the new verbosity + $level = match ($this->output->getVerbosity()) { + OutputInterface::VERBOSITY_VERBOSE => 'info', + OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug', + default => 'warning', + }; + logger()->setLevel($level); + ApplicationContext::setDebug($this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG); + // apply env array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}")); @@ -110,7 +119,7 @@ class CraftCommand extends BaseCommand * shared-extensions: array, * packages: array, * sapi: array, - * verbosity: int, + * verbosity: 128|16|256|32|64|8, * debug: bool, * clean-build: bool, * build-options: array, @@ -171,11 +180,16 @@ class CraftCommand extends BaseCommand } // verbosity - $verbosity_level = $craft['verbosity'] ?? OutputInterface::VERBOSITY_NORMAL; $debug = $craft['debug'] ?? false; - if ($debug) { - $verbosity_level = OutputInterface::VERBOSITY_DEBUG; - } + $verbosity_level = $debug + ? OutputInterface::VERBOSITY_DEBUG + : match ((int) ($craft['verbosity'] ?? 0)) { + OutputInterface::VERBOSITY_QUIET => OutputInterface::VERBOSITY_QUIET, + OutputInterface::VERBOSITY_VERBOSE => OutputInterface::VERBOSITY_VERBOSE, + OutputInterface::VERBOSITY_VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE, + OutputInterface::VERBOSITY_DEBUG => OutputInterface::VERBOSITY_DEBUG, + default => OutputInterface::VERBOSITY_NORMAL, + }; $craft['verbosity'] = $verbosity_level; // clean-build (if true, reset before all builds) 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/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index a1743200..6b5c9e6d 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -16,7 +16,7 @@ class GenExtTestMatrixCommand extends BaseCommand private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], - 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-latest', 'os_key' => 'Windows'], + 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-2025', 'os_key' => 'Windows'], 'macos' => ['arch' => 'aarch64', 'runner' => 'macos-15', 'os_key' => 'Darwin'], ]; @@ -107,7 +107,12 @@ class GenExtTestMatrixCommand extends BaseCommand // Separate into regular and virtual extensions (build-static:false excluded globally) $all_regular = []; $all_virtual = []; + $all_libraries = []; foreach ($all as $pkg_name => $config) { + if (($config['type'] ?? '') === 'library') { + $all_libraries[$pkg_name] = $config; + continue; + } if (($config['type'] ?? '') !== 'php-extension') { continue; } @@ -154,10 +159,7 @@ class GenExtTestMatrixCommand extends BaseCommand $raw, fn ($d) => isset($pool_set[$d]) && $d !== $pkg_name )); - $os_lib_deps[$this->displayName($pkg_name)] = array_values(array_filter( - $raw, - fn ($d) => !str_starts_with($d, 'ext-') - )); + $os_lib_deps[$this->displayName($pkg_name)] = $this->collectLibraryDeps($raw, $all_libraries, $os); } $all_ext_lib_deps[$os] = $os_lib_deps; @@ -246,22 +248,23 @@ class GenExtTestMatrixCommand extends BaseCommand } } - if (!empty($filter_extensions)) { - $entries = array_values(array_filter($entries, function (array $entry) use ($filter_extensions): bool { + if (!empty($filter_extensions) || !empty($filter_libs)) { + $entries = array_values(array_filter($entries, function (array $entry) use ($filter_extensions, $filter_libs, $all_ext_lib_deps): bool { $names = explode(',', $entry['extension']); - return count(array_intersect($names, $filter_extensions)) > 0; - })); - } - if (!empty($filter_libs)) { - $entries = array_values(array_filter($entries, function (array $entry) use ($filter_libs, $all_ext_lib_deps): bool { - $names = explode(',', $entry['extension']); - $lib_deps = $all_ext_lib_deps[$entry['os']] ?? []; - foreach ($names as $name) { - if (count(array_intersect($lib_deps[$name] ?? [], $filter_libs)) > 0) { - return true; + if (!empty($filter_extensions) && count(array_intersect($names, $filter_extensions)) > 0) { + return true; + } + + if (!empty($filter_libs)) { + $lib_deps = $all_ext_lib_deps[$entry['os']] ?? []; + foreach ($names as $name) { + if (count(array_intersect($lib_deps[$name] ?? [], $filter_libs)) > 0) { + return true; + } } } + return false; })); } @@ -300,6 +303,41 @@ class GenExtTestMatrixCommand extends BaseCommand return str_starts_with($pkg_name, 'ext-') ? substr($pkg_name, 4) : $pkg_name; } + /** + * Collect direct and transitive library dependencies from a package dependency list. + * + * @param string[] $deps + * @param array $library_configs + * @param array $seen + * + * @return string[] + */ + private function collectLibraryDeps(array $deps, array $library_configs, string $platform, array $seen = []): array + { + $collected = []; + foreach ($deps as $dep) { + if (str_starts_with($dep, 'ext-') || isset($seen[$dep])) { + continue; + } + + $seen[$dep] = true; + $collected[$dep] = $dep; + + if (!isset($library_configs[$dep])) { + continue; + } + + $child_deps = array_merge( + $this->resolvePlatformList($library_configs[$dep], 'depends', $platform), + $this->resolvePlatformList($library_configs[$dep], 'suggests', $platform), + ); + foreach ($this->collectLibraryDeps($child_deps, $library_configs, $platform, $seen) as $child_dep) { + $collected[$child_dep] = $child_dep; + } + } + return array_values($collected); + } + /** * Split orphans into batches such that no two conflicting extensions share a batch. * Uses a greedy graph-coloring approach. diff --git a/src/StaticPHP/Command/Dev/TestBotCommand.php b/src/StaticPHP/Command/Dev/TestBotCommand.php index dc34444d..6ee1e1e3 100644 --- a/src/StaticPHP/Command/Dev/TestBotCommand.php +++ b/src/StaticPHP/Command/Dev/TestBotCommand.php @@ -154,7 +154,7 @@ class TestBotCommand extends BaseCommand 'targets' => array_values($targets), 'gen_matrix_args' => $gen_matrix_args, 'gen_matrix_args_tier2' => $gen_matrix_args_tier2, - 'php_versions' => array_values($php_versions), + 'php_versions' => $php_versions, 'tier2' => $tier2, 'comment_body' => $comment_body, ]; @@ -253,6 +253,13 @@ class TestBotCommand extends BaseCommand $fmt($targets), ); + $available_labels = implode(', ', [ + '`need-test` (gate)', + '`test/linux` `test/windows` `test/macos` (platform)', + '`test/tier2` (extra arch)', + '`test/php-83` `test/php-84` (PHP version)', + ]); + // Case 1: need-test absent → invite the author to add it if (!$need_test) { return implode("\n", [ @@ -261,11 +268,9 @@ class TestBotCommand extends BaseCommand '', $detected, '', - 'To trigger extension build tests on this PR, add the `need-test` label:', + 'To trigger extension build tests on this PR, add the `need-test` label.', '', - '**Gate**: `need-test`', - '**Platform filter** (optional, default all): `test/linux` `test/windows` `test/macos` · `test/tier2`', - '**PHP version** (optional, default 8.5): `test/php-83` `test/php-84`', + '**Available labels**: ' . $available_labels, ]); } @@ -307,6 +312,7 @@ class TestBotCommand extends BaseCommand '', $detected, '**Active labels**: ' . $labels_str, + '**Available labels**: ' . $available_labels, '**Config**: ' . implode(' + ', $platform_parts) . ' | ' . $php_str, ]); } 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/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/Command/ResetCommand.php b/src/StaticPHP/Command/ResetCommand.php index 85928c2f..ce22e7a9 100644 --- a/src/StaticPHP/Command/ResetCommand.php +++ b/src/StaticPHP/Command/ResetCommand.php @@ -4,13 +4,14 @@ declare(strict_types=1); namespace StaticPHP\Command; +use StaticPHP\Exception\SPCInternalException; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputOption; - -use function Laravel\Prompts\confirm; +use Symfony\Component\Console\Question\ConfirmationQuestion; #[AsCommand('reset')] class ResetCommand extends BaseCommand @@ -46,7 +47,11 @@ class ResetCommand extends BaseCommand // Confirm with user unless --yes is specified if (!$this->input->getOption('yes')) { - if (!confirm('Are you sure you want to continue?', false)) { + $helper = $this->getHelper('question'); + if (!$helper instanceof QuestionHelper) { + throw new SPCInternalException('Question helper not provided'); + } + if (!$helper->ask($this->input, $this->output, new ConfirmationQuestion('Are you sure you want to continue? [y/N] ', false))) { InteractiveTerm::error(message: 'Reset operation cancelled.'); return static::SUCCESS; } diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index 8462091d..4e3b6d09 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -38,7 +38,7 @@ class ArtifactConfig */ public static function loadFromFile(string $file, string $registry_name): string { - $content = file_get_contents($file); + $content = @file_get_contents($file); if ($content === false) { throw new WrongUsageException("Failed to read artifact config file: {$file}"); } diff --git a/src/StaticPHP/Config/ConfigType.php b/src/StaticPHP/Config/ConfigType.php index 31f96ee4..35a60fb2 100644 --- a/src/StaticPHP/Config/ConfigType.php +++ b/src/StaticPHP/Config/ConfigType.php @@ -19,6 +19,7 @@ enum ConfigType 'php-extension', 'target', 'virtual-target', + 'tool', ]; public static function validateLicenseField(mixed $value): bool diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index b66250cc..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, @@ -44,6 +48,13 @@ class ConfigValidator 'path' => ConfigType::LIST_ARRAY, // @ 'env' => ConfigType::ASSOC_ARRAY, // @ 'append-env' => ConfigType::ASSOC_ARRAY, // @ + + // tool type fields (nested under 'tool' key) + 'tool' => ConfigType::ASSOC_ARRAY, + 'provides' => ConfigType::LIST_ARRAY, + 'binary-subdir' => ConfigType::STRING, + 'install-root' => ConfigType::STRING, + 'min-version' => ConfigType::STRING, ]; public const array PACKAGE_FIELDS = [ @@ -56,6 +67,9 @@ class ConfigValidator 'lang' => false, 'frameworks' => false, // @ + // build-time tool dependency declaration + 'tools' => false, // @ + // php-extension type fields 'php-extension' => false, @@ -67,6 +81,9 @@ class ConfigValidator 'path' => false, // @ 'env' => false, // @ 'append-env' => false, // @ + + // tool fields (nested object) + 'tool' => false, ]; public const array SUFFIX_ALLOWED_FIELDS = [ @@ -78,6 +95,7 @@ class ConfigValidator 'path', 'env', 'append-env', + 'tools', ]; public const array PHP_EXTENSION_FIELDS = [ @@ -92,6 +110,13 @@ class ConfigValidator 'os' => false, ]; + public const array TOOL_FIELDS = [ + 'provides' => true, + 'binary-subdir' => false, + 'install-root' => false, + 'min-version' => false, + ]; + public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] 'filelist' => [['url', 'regex'], ['extract']], 'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']], @@ -220,8 +245,8 @@ class ConfigValidator $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'])) { + // check if "library|target|tool" package has artifact field + if (in_array($pkg['type'], ['target', 'library', 'tool']) && !isset($pkg['artifact'])) { throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field"); } @@ -235,6 +260,11 @@ class ConfigValidator self::validatePhpExtensionFields($name, $pkg); } + // check if "tool" package has tool specific fields and validate inside + if ($pkg['type'] === 'tool') { + self::validateToolFields($name, $pkg); + } + // check for unknown fields self::validateNoInvalidFields('package', $name, $pkg, array_keys(self::PACKAGE_FIELD_TYPES)); } @@ -397,6 +427,29 @@ class ConfigValidator self::validateNoInvalidFields('php-extension', $name, $pkg['php-extension'], array_keys(self::PHP_EXTENSION_FIELDS)); } + /** + * Validate tool specific fields for tool package type. + */ + private static function validateToolFields(int|string $name, mixed $pkg): void + { + if (!isset($pkg['tool'])) { + throw new ValidationException("Package {$name} of type 'tool' must have a 'tool' field"); + } + if (!is_assoc_array($pkg['tool'])) { + throw new ValidationException("Package {$name} [tool] must be an object"); + } + foreach (self::TOOL_FIELDS as $field => $required) { + if ($required && !isset($pkg['tool'][$field])) { + throw new ValidationException("Package {$name} [tool] must have required field [{$field}]"); + } + if (isset($pkg['tool'][$field])) { + self::validatePackageFieldType($field, $pkg['tool'][$field], $name); + } + } + // check for unknown fields in tool + self::validateNoInvalidFields('tool', $name, $pkg['tool'], array_keys(self::TOOL_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) { diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index e4474bf5..ba1c8873 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -16,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'. + * Only processes .json, .yml, and .yaml files (skips .gitkeep etc.). */ public static function loadFromDir(string $dir, string $registry_name): array { @@ -28,6 +28,10 @@ class PackageConfig $files = FileSystem::scanDirFiles($dir, false); if (is_array($files)) { foreach ($files as $file) { + $ext = pathinfo($file, PATHINFO_EXTENSION); + if (!in_array($ext, ['json', 'yml', 'yaml'], true)) { + continue; + } self::loadFromFile($file, $registry_name); $loaded[] = $file; } 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/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index 73ff9f2d..f46ea8ba 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -79,11 +79,11 @@ class ApplicationContext /** * Get a service from the container. * - * @template T + * @template T of object * - * @param class-string $id Service identifier + * @param class-string|string $id Service identifier * - * @return null|T + * @return ($id is class-string ? T : mixed) */ public static function get(string $id): mixed { diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 05f3c4a1..b888d241 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -11,11 +11,14 @@ use StaticPHP\Registry\DoctorLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\InteractiveTerm; +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 ZM\Logger\ConsoleColor; -use function Laravel\Prompts\confirm; - readonly class Doctor { public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT, public readonly bool $interactive = true) @@ -125,9 +128,14 @@ readonly class Doctor 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; + if ($this->auto_fix === FIX_POLICY_PROMPT) { + $helper = new QuestionHelper(); + $input = ApplicationContext::has(InputInterface::class) ? ApplicationContext::get(InputInterface::class) : new ArrayInput([]); + $output = ApplicationContext::has(OutputInterface::class) ? ApplicationContext::get(OutputInterface::class) : $this->output ?? new ConsoleOutput(); + if (!$helper->ask($input, $output, new ConfirmationQuestion('Do you want to try to fix this issue now? [Y/n] ', true))) { + $this->output?->writeln('You canceled fix.'); + return false; + } } // perform fix InteractiveTerm::indicateProgress("Fixing {$result->getFixItem()} ... "); diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php index 08e140f4..3fe1d312 100644 --- a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -54,13 +54,24 @@ class WindowsToolCheck return CheckResult::ok(); } - #[CheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 996)] - public function checkSDK(): ?CheckResult + #[CheckItem('if msys2-build-essentials is installed', limit_os: 'Windows', level: 996)] + public function checkMsys2(): ?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'); + $marker = PKG_ROOT_PATH . '\msys2-build-essentials\.spc-msys2-initialized'; + if (!file_exists($marker)) { + return CheckResult::fail('msys2-build-essentials not installed', 'install-msys2-build-essentials'); } - return CheckResult::ok(getenv('PHP_SDK_PATH')); + return CheckResult::ok(PKG_ROOT_PATH . '\msys2-build-essentials\msys64'); + } + + #[CheckItem('if 7za.exe is installed', limit_os: 'Windows', level: 999)] + public function check7zaWin(): ?CheckResult + { + $path = FileSystem::convertPath(PKG_ROOT_PATH . '\bin\7za.exe'); + if (!file_exists($path)) { + return CheckResult::fail('7za.exe not found', 'install-7za-win'); + } + return CheckResult::ok($path); } #[CheckItem('if nasm installed', level: 995)] @@ -112,12 +123,20 @@ class WindowsToolCheck return true; } - #[FixItem('install-php-sdk')] - public function installSDK(): bool + #[FixItem('install-msys2-build-essentials')] + public function installMsys2(): bool { - FileSystem::removeDir(getenv('PHP_SDK_PATH')); $installer = new PackageInstaller(interactive: false); - $installer->addInstallPackage('php-sdk-binary-tools'); + $installer->addInstallPackage('msys2-build-essentials'); + $installer->run(true); + return true; + } + + #[FixItem('install-7za-win')] + public function install7zaWin(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('7za-win'); $installer->run(true); return true; } 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 1ec1a503..ba53f81d 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -120,6 +120,20 @@ abstract class Package return false; } + /** + * Get the target directory where this package's artifacts should be placed. + * + * Libraries install to BUILD_ROOT_PATH (static-libs, headers, pkg-configs). + * Tools install to PKG_ROOT_PATH (executables). + * Extensions install to php-src/ext/ (shared objects). + * + * Override in subclasses to change the default. + */ + public function getInstallTarget(): string + { + return BUILD_ROOT_PATH; + } + /** * Add a stage to the package. */ @@ -270,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 9cd0a106..cf93368d 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -11,6 +11,7 @@ use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; @@ -75,6 +76,9 @@ class PackageInstaller } // special check for php target packages if (in_array($package->getName(), ['php', 'php-cli', 'php-fpm', 'php-micro', 'php-cgi', 'php-embed', 'frankenphp'], true)) { + if (!$package instanceof TargetPackage) { + throw new WrongUsageException("Package '{$package->getName()}' is expected to be a TargetPackage."); + } $this->handlePhpTargetPackage($package); return $this; } @@ -171,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) @@ -209,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 @@ -273,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 @@ -350,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; } @@ -504,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; } @@ -571,6 +594,66 @@ class PackageInstaller return null; } + /** + * Collect all tool packages required by the currently resolved packages. + * + * Reads the 'tools' field from each resolved package's YAML config. + * The field supports platform suffixes (tools@windows, tools@linux, etc.) + * resolved automatically by PackageConfig::get(). + * + * Tools are NOT part of the library dependency graph — they are + * build-time prerequisites that must be installed before any library + * build begins. + * + * @return string[] Unique tool package names required for this build + */ + public function collectRequiredTools(): array + { + $tools = []; + foreach ($this->packages as $package) { + $deps = PackageConfig::get($package->getName(), 'tools', []); + foreach ((array) $deps as $tool_name) { + $tools[$tool_name] = true; + } + } + return array_keys($tools); + } + + /** + * Check that all required tools are installed. + * + * Iterates through tools collected by collectRequiredTools(), + * resolves each to a ToolPackage instance, and checks isInstalled(). + * + * @return array{missing: array, installed: array} + */ + public function checkRequiredTools(): array + { + $missing = []; + $installed = []; + foreach ($this->collectRequiredTools() as $tool_name) { + try { + $tool = PackageLoader::getPackage($tool_name); + } catch (WrongUsageException) { + $missing[] = $tool_name; + logger()->warning("Required tool '{$tool_name}' is not registered as a package."); + continue; + } + + if (!$tool instanceof ToolPackage) { + logger()->warning("Package '{$tool_name}' is declared as a tool dependency but is not a ToolPackage (type: {$tool->getType()})."); + continue; + } + + if ($tool->isInstalled()) { + $installed[] = $tool_name; + } else { + $missing[] = $tool_name; + } + } + return ['missing' => $missing, 'installed' => $installed]; + } + /** * @param Package[] $packages */ @@ -594,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.'); } } @@ -628,11 +711,62 @@ 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); } } + /** + * Ensure all required tools are installed, throwing if any are missing. + * + * 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 + { + $status = $this->checkRequiredTools(); + if (empty($status['missing'])) { + if (!empty($status['installed'])) { + logger()->info('Required tools: ' . implode(', ', $status['installed']) . ' — all installed.'); + } + return; + } + + $msg = 'Missing required build tools: ' . implode(', ', $status['missing']) . "\n"; + $msg .= "Run 'bin/spc doctor' to check your environment, or install the missing tools manually."; + throw new EnvironmentException($msg); + } + private function injectPackageEnvs(Package $package): void { $name = $package->getName(); diff --git a/src/StaticPHP/Package/ToolPackage.php b/src/StaticPHP/Package/ToolPackage.php new file mode 100644 index 00000000..7e0e6ba3 --- /dev/null +++ b/src/StaticPHP/Package/ToolPackage.php @@ -0,0 +1,233 @@ +.yml): + * + * nasm: + * type: tool + * tool: + * 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: + * type: url + * url: 'https://...' + * extract: + * nasm.exe: '{php_sdk_path}/bin/nasm.exe' + */ +class ToolPackage extends Package +{ + /** + * 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. + * + * Defaults to PKG_ROOT_PATH. Override via 'tool.install-root' in YAML + * or via the TOOL_INSTALL_ROOT_{NAME} environment variable. + */ + public function getInstallRoot(): string + { + $env_var = 'TOOL_INSTALL_ROOT_' . strtoupper(str_replace('-', '_', $this->name)); + if ($root = getenv($env_var)) { + return $root; + } + $config_root = $this->getToolField('install-root'); + if ($config_root !== null) { + return FileSystem::replacePathVariable((string) $config_root); + } + return PKG_ROOT_PATH; + } + + /** + * Get the directory where this tool's binaries reside. + * + * This is {install-root}/{binary-subdir}. If binary-subdir is not + * configured, returns the install root directly. + */ + public function getBinaryDir(): string + { + $subdir = $this->getToolField('binary-subdir') ?? ''; + if ($subdir === '') { + return $this->getInstallRoot(); + } + return $this->getInstallRoot() . DIRECTORY_SEPARATOR . $subdir; + } + + /** + * Get the list of executables this tool provides. + * + * 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->getToolField('provides') ?? []; + } + + /** + * Get the full path to a specific binary provided by this tool. + * + * @param string $name Bare executable name (must be listed in tool.provides). + * If empty, defaults to the first entry in provides. + * @return string Full absolute path to the binary + */ + public function getBinary(string $name = ''): string + { + $provides = $this->getProvides(); + if ($name === '') { + $name = $provides[0] ?? throw new \RuntimeException("Tool '{$this->name}' has no 'tool.provides' configured."); + } + if (!in_array($name, $provides, true)) { + throw new \RuntimeException("Binary '{$name}' is not listed in tool.provides for '{$this->name}'. Available: " . implode(', ', $provides)); + } + return $this->getBinaryDir() . DIRECTORY_SEPARATOR . $name; + } + + /** + * Check whether this tool is installed (all provided binaries exist on disk). + */ + public function isInstalled(): bool + { + 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. + * + * Returns null if no version constraint is configured. + */ + public function getMinVersion(): ?string + { + $version = $this->getToolField('min-version'); + return $version !== null ? (string) $version : null; + } + + /** + * Tools install to PKG_ROOT_PATH (or the configured install-root), + * not BUILD_ROOT_PATH. + */ + public function getInstallTarget(): string + { + return $this->getBinaryDir(); + } + + /** + * Get the 'tool' sub-config for this package. + * + * Returns the nested array under the 'tool' key in the package YAML, + * or an empty array if not configured. + * + * @return array + */ + private function getToolConfig(): array + { + $config = PackageConfig::get($this->name); + if (!is_array($config) || !isset($config['tool']) || !is_array($config['tool'])) { + return []; + } + 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/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)); + } +} diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index cf0301c0..acb1ab28 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -17,6 +17,7 @@ use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\Package\ResolveBuild; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; +use StaticPHP\Attribute\Package\Tool; use StaticPHP\Attribute\Package\Validate; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; @@ -27,6 +28,7 @@ use StaticPHP\Package\Package; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\TargetPackage; +use StaticPHP\Package\ToolPackage; use StaticPHP\Util\FileSystem; class PackageLoader @@ -88,6 +90,7 @@ class PackageLoader 'target', 'virtual-target' => new TargetPackage($name, $item['type']), 'library' => new LibraryPackage($name, $item['type']), 'php-extension' => new PhpExtensionPackage($name, $item['type']), + 'tool' => new ToolPackage($name, $item['type']), default => null, }; if ($pkg !== null) { @@ -190,7 +193,8 @@ class PackageLoader $attribute_instance = $attribute->newInstance(); if ($attribute_instance instanceof Target === false && $attribute_instance instanceof Library === false && - $attribute_instance instanceof Extension === false) { + $attribute_instance instanceof Extension === false && + $attribute_instance instanceof Tool === false) { // not a package attribute continue; } @@ -216,6 +220,7 @@ class PackageLoader Target::class => ['target', 'virtual-target'], Library::class => ['library'], Extension::class => ['php-extension'], + Tool::class => ['tool'], default => null, }; if (!in_array($package_type, $pkg_type_attr, true)) { @@ -370,7 +375,10 @@ class PackageLoader // match condition $installer = ApplicationContext::get(PackageInstaller::class); $stages = self::$before_stages[$package_name][$stage] ?? []; - foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) { + foreach ($stages as $entry) { + $callback = $entry[0]; + $only_when_package_resolved = $entry[1] ?? null; + $conditionals = $entry[2] ?? []; if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } @@ -389,7 +397,10 @@ class PackageLoader $installer = ApplicationContext::get(PackageInstaller::class); $stages = self::$after_stages[$package_name][$stage] ?? []; $result = []; - foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) { + foreach ($stages as $entry) { + $callback = $entry[0]; + $only_when_package_resolved = $entry[1] ?? null; + $conditionals = $entry[2] ?? []; if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } @@ -433,7 +444,9 @@ class PackageLoader } $pkg = self::getPackage($package_name); foreach ($stages as $stage_name => $before_events) { - foreach ($before_events as [$event_callable, $only_when_package_resolved, $conditionals]) { + foreach ($before_events as $entry) { + $event_callable = $entry[0]; + $only_when_package_resolved = $entry[1] ?? null; // 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}]."); 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 c59859cf..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) { @@ -117,7 +118,7 @@ class UnixAutoconfExecutor extends Executor /** * Add configure args. */ - public function addConfigureArgs(...$args): static + public function addConfigureArgs(string ...$args): static { $this->configure_args = [...$this->configure_args, ...$args]; return $this; @@ -126,7 +127,7 @@ class UnixAutoconfExecutor extends Executor /** * Remove some configure args, to bypass the configure option checking for some libs. */ - public function removeConfigureArgs(...$args): static + public function removeConfigureArgs(string ...$args): static { $this->configure_args = array_diff($this->configure_args, $args); return $this; diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 9fc00840..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) { @@ -135,7 +136,7 @@ class UnixCMakeExecutor extends Executor /** * Add configure args. */ - public function addConfigureArgs(...$args): static + public function addConfigureArgs(string ...$args): static { $this->configure_args = [...$this->configure_args, ...$args]; return $this; @@ -144,7 +145,7 @@ class UnixCMakeExecutor extends Executor /** * Remove some configure args, to bypass the configure option checking for some libs. */ - public function removeConfigureArgs(...$args): static + public function removeConfigureArgs(string ...$args): static { $this->ignore_args = [...$this->ignore_args, ...$args]; return $this; diff --git a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php index 1f057f12..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); @@ -99,7 +100,7 @@ class WindowsCMakeExecutor extends Executor /** * Add configure args. */ - public function addConfigureArgs(...$args): static + public function addConfigureArgs(string ...$args): static { $this->configure_args = [...$this->configure_args, ...$args]; return $this; @@ -108,7 +109,7 @@ class WindowsCMakeExecutor extends Executor /** * Remove some configure args, to bypass the configure option checking for some libs. */ - public function removeConfigureArgs(...$args): static + public function removeConfigureArgs(string ...$args): static { $this->ignore_args = [...$this->ignore_args, ...$args]; return $this; @@ -189,6 +190,10 @@ class WindowsCMakeExecutor extends Executor { return $this->custom_default_args ?? [ '-A x64', + // CMake 4.x hard-errors on projects requesting compatibility with CMake < 3.5 + // (e.g. wineditline). This is the documented escape hatch; modern projects and + // older CMake releases ignore it. + '-DCMAKE_POLICY_VERSION_MINIMUM=3.5', '-DCMAKE_BUILD_TYPE=Release', '-DBUILD_SHARED_LIBS=OFF', '-DBUILD_STATIC_LIBS=ON', diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index f20fca33..2761f32f 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -184,12 +184,14 @@ class DefaultShell extends Shell */ public function execute7zExtract(string $archive_path, string $target_path): bool { - $sdk_path = getenv('PHP_SDK_PATH'); - if ($sdk_path === false) { - throw new SPCInternalException('PHP_SDK_PATH environment variable is not set'); + // 7za.exe is installed by the 7za-win target package into PKG_ROOT_PATH\bin, + // which is added to PATH by MSVCToolchain::initEnv(). + $_7z_path = FileSystem::convertPath(PKG_ROOT_PATH . '\bin\7za.exe'); + if (!file_exists($_7z_path)) { + throw new SPCInternalException('7za.exe not found. Please install the 7za-win target package.'); } - $_7z = escapeshellarg(FileSystem::convertPath($sdk_path . '/bin/7za.exe')); + $_7z = escapeshellarg(FileSystem::convertPath($_7z_path)); $archive_arg = escapeshellarg(FileSystem::convertPath($archive_path)); $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); 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(), diff --git a/src/StaticPHP/Toolchain/MSVCToolchain.php b/src/StaticPHP/Toolchain/MSVCToolchain.php index 1449db70..646564fa 100644 --- a/src/StaticPHP/Toolchain/MSVCToolchain.php +++ b/src/StaticPHP/Toolchain/MSVCToolchain.php @@ -14,10 +14,14 @@ class MSVCToolchain implements ToolchainInterface 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'); + // msys2-build-essentials: add MSYS2 usr\bin to PATH so that 7za.exe, make, autoconf, etc. are available. + // This must be done here because msys2-build-essentials is not a dependency of any library package, + // so its path@windows entries are not automatically applied by the package installer at runtime. + $msys2_path = getenv('SPC_MSYS2_PATH') ?: (PKG_ROOT_PATH . '\msys2-build-essentials\msys64'); + if (is_dir($msys2_path)) { + GlobalEnvManager::putenv("SPC_MSYS2_PATH={$msys2_path}"); + GlobalEnvManager::addPathIfNotExists($msys2_path . '\usr\bin'); + GlobalEnvManager::addPathIfNotExists("{$msys2_path}\\usr\\lib\\p7zip"); } // strawberry-perl if (is_dir(PKG_ROOT_PATH . '\strawberry-perl')) { diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 28a4facd..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; } @@ -411,7 +411,7 @@ class FileSystem $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', + '{spc_msys2_path}' => getenv('SPC_MSYS2_PATH') ?: (PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'msys2-build-essentials' . DIRECTORY_SEPARATOR . 'msys64'), '{working_dir}' => WORKING_DIR, '{download_path}' => DOWNLOAD_PATH, '{source_path}' => SOURCE_PATH, diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 0570f31c..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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $no_ansi = self::noAnsi(); $message = $no_ansi ? strip_ansi_colors($message) : $message; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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 = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; - $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + $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; + } } diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 70525e67..98fee7ec 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -45,12 +45,16 @@ 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; // 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' : '') + $cd_cmd = (PHP_OS_FAMILY === 'Windows' ? 'cd /d ' : 'cd ') . escapeshellarg($patch_cwd); + $detect_cmd = $cd_cmd + . ' && patch --binary --dry-run -p1 -s -f' + . ($detect_reverse ? ' -R' : '') + . ' < ' . escapeshellarg($patch_arg) . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; exec($detect_cmd, $output, $detect_status); @@ -60,9 +64,10 @@ class SourcePatcher } // Apply patch - $apply_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' - . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) - . ' | patch -p1 ' . ($reverse ? '-R' : ''); + $apply_cmd = $cd_cmd + . ' && patch --binary -p1' + . ($reverse ? ' -R' : '') + . ' < ' . escapeshellarg($patch_arg); exec($apply_cmd, $apply_output, $apply_status); if ($apply_status !== 0) { diff --git a/src/globals/ext-tests/pdo_sqlite.php b/src/globals/ext-tests/pdo_sqlite.php new file mode 100644 index 00000000..9e5e7f14 --- /dev/null +++ b/src/globals/ext-tests/pdo_sqlite.php @@ -0,0 +1,16 @@ +exec('CREATE TABLE spc_column_metadata_test (id INTEGER)'); + +$stmt = $pdo->query('SELECT id FROM spc_column_metadata_test'); +if ($stmt === false) { + throw new RuntimeException('Failed to query SQLite metadata test table.'); +} + +$metadata = $stmt->getColumnMeta(0); +if (($metadata['table'] ?? null) !== 'spc_column_metadata_test') { + throw new RuntimeException('PDO SQLite column metadata does not include the origin table.'); +} diff --git a/src/globals/ext-tests/sqlite3.php b/src/globals/ext-tests/sqlite3.php new file mode 100644 index 00000000..c3221e35 --- /dev/null +++ b/src/globals/ext-tests/sqlite3.php @@ -0,0 +1,10 @@ +querySingle("SELECT sqlite_compileoption_used('ENABLE_COLUMN_METADATA')"); + +if ((int) $enabled !== 1) { + throw new RuntimeException('SQLite was not built with SQLITE_ENABLE_COLUMN_METADATA.'); +} diff --git a/src/globals/extra/Makefile-sqlite b/src/globals/extra/Makefile-sqlite index 4bf0cee2..e6d2b699 100644 --- a/src/globals/extra/Makefile-sqlite +++ b/src/globals/extra/Makefile-sqlite @@ -1,69 +1,69 @@ - -CC=cl.exe /nologo -AR=lib.exe /nologo -LINK=link.exe /nologo - -!IF "" == "$(MACHINE)" -MACHINE=x64 -!ENDIF - -!IF "" == "$(CRT)" -CRT=vc15 -!ENDIF - -!IF "" == "$(PREFIX)" -PREFIX="$(CRT)-$(MACHINE)" -!ENDIF - -COMMON_CFLAGS=/D SQLITE_THREADSAFE=1 /DSQLITE_ENABLE_FTS3=1 /D SQLITE_ENABLE_FTS4=1 /D SQLITE_ENABLE_FTS5=1 /D SQLITE_ENABLE_JSON1=1 /D SQLITE_ENABLE_COLUMN_METADATA=1 /D SQLITE_CORE=1 -!IF "$(DEBUG)"=="1" -SQLITE3_STATIC_BASE=libsqlite3_a_debug -SQLITE3_DLL_BASE=libsqlite3_debug -SQLITE3_EXE_BASE=sqlite3 -CFLAGS=$(COMMON_CFLAGS) /Zi /MDd /Od /W3 -LDFLAGS=/DEBUG /GUARD:CF /INCREMENTAL:NO -!ELSE -SQLITE3_STATIC_BASE=libsqlite3_a -SQLITE3_DLL_BASE=libsqlite3 -SQLITE3_EXE_BASE=sqlite3 -CFLAGS=$(COMMON_CFLAGS) /Zi /MT /guard:cf /Zc:inline /Qspectre /Ox /W3 /GF /Gw -LDFLAGS=/GUARD:CF /INCREMENTAL:NO /NXCOMPAT /DYNAMICBASE -!ENDIF - - -all: $(SQLITE3_STATIC_BASE).lib $(SQLITE3_EXE_BASE).exe $(SQLITE3_DLL_BASE).dll - -install: all - if not exist $(PREFIX)\bin mkdir $(PREFIX)\bin - if not exist $(PREFIX)\include mkdir $(PREFIX)\include - if not exist $(PREFIX)\lib mkdir $(PREFIX)\lib - copy /Y sqlite3.h $(PREFIX)\include - copy /Y sqlite3ext.h $(PREFIX)\include - copy /Y $(SQLITE3_STATIC_BASE).lib $(PREFIX)\lib - copy /Y $(SQLITE3_STATIC_BASE).pdb $(PREFIX)\lib - copy /Y $(SQLITE3_DLL_BASE).lib $(PREFIX)\lib - copy /Y $(SQLITE3_DLL_BASE).pdb $(PREFIX)\bin - copy /Y $(SQLITE3_DLL_BASE).dll $(PREFIX)\bin - copy /Y $(SQLITE3_EXE_BASE).exe $(PREFIX)\bin - copy /Y $(SQLITE3_EXE_BASE).pdb $(PREFIX)\bin - -install-static: $(SQLITE3_STATIC_BASE).lib - if not exist $(PREFIX)\include mkdir $(PREFIX)\include - if not exist $(PREFIX)\lib mkdir $(PREFIX)\lib - copy /Y sqlite3.h $(PREFIX)\include - copy /Y sqlite3ext.h $(PREFIX)\include - copy /Y $(SQLITE3_STATIC_BASE).lib $(PREFIX)\lib - -clean: - del *.obj *.lib *.exe *.pdb *.dll *.exp - -$(SQLITE3_STATIC_BASE).lib: sqlite3.c sqlite3.h - $(CC) $(CFLAGS) /Fd$(SQLITE3_STATIC_BASE).pdb /c sqlite3.c - $(AR) sqlite3.obj /OUT:$(SQLITE3_STATIC_BASE).lib - -$(SQLITE3_EXE_BASE).exe: shell.c sqlite3.c sqlite3.h - $(CC) $(CFLAGS) shell.c sqlite3.c /Fd$(SQLITE3_EXE_BASE).pdb /Fe$(SQLITE3_EXE_BASE).exe - -$(SQLITE3_DLL_BASE).dll: sqlite3.c sqlite3.h - $(CC) $(CFLAGS) /DSQLITE_API=__declspec(dllexport) /Fd$(SQLITE3_DLL_BASE).pdb /c sqlite3.c + +CC=cl.exe /nologo +AR=lib.exe /nologo +LINK=link.exe /nologo + +!IF "" == "$(MACHINE)" +MACHINE=x64 +!ENDIF + +!IF "" == "$(CRT)" +CRT=vc15 +!ENDIF + +!IF "" == "$(PREFIX)" +PREFIX="$(CRT)-$(MACHINE)" +!ENDIF + +COMMON_CFLAGS=/D SQLITE_THREADSAFE=1 /DSQLITE_ENABLE_FTS3=1 /D SQLITE_ENABLE_FTS4=1 /D SQLITE_ENABLE_FTS5=1 /D SQLITE_ENABLE_JSON1=1 /D SQLITE_ENABLE_COLUMN_METADATA=1 /D SQLITE_CORE=1 +!IF "$(DEBUG)"=="1" +SQLITE3_STATIC_BASE=libsqlite3_a_debug +SQLITE3_DLL_BASE=libsqlite3_debug +SQLITE3_EXE_BASE=sqlite3 +CFLAGS=$(COMMON_CFLAGS) /Zi /MDd /Od /W3 +LDFLAGS=/DEBUG /GUARD:CF /INCREMENTAL:NO +!ELSE +SQLITE3_STATIC_BASE=libsqlite3_a +SQLITE3_DLL_BASE=libsqlite3 +SQLITE3_EXE_BASE=sqlite3 +CFLAGS=$(COMMON_CFLAGS) /Zi /MT /guard:cf /Zc:inline /Qspectre /Ox /W3 /GF /Gw +LDFLAGS=/GUARD:CF /INCREMENTAL:NO /NXCOMPAT /DYNAMICBASE +!ENDIF + + +all: $(SQLITE3_STATIC_BASE).lib $(SQLITE3_EXE_BASE).exe $(SQLITE3_DLL_BASE).dll + +install: all + if not exist $(PREFIX)\bin mkdir $(PREFIX)\bin + if not exist $(PREFIX)\include mkdir $(PREFIX)\include + if not exist $(PREFIX)\lib mkdir $(PREFIX)\lib + copy /Y sqlite3.h $(PREFIX)\include + copy /Y sqlite3ext.h $(PREFIX)\include + copy /Y $(SQLITE3_STATIC_BASE).lib $(PREFIX)\lib + copy /Y $(SQLITE3_STATIC_BASE).pdb $(PREFIX)\lib + copy /Y $(SQLITE3_DLL_BASE).lib $(PREFIX)\lib + copy /Y $(SQLITE3_DLL_BASE).pdb $(PREFIX)\bin + copy /Y $(SQLITE3_DLL_BASE).dll $(PREFIX)\bin + copy /Y $(SQLITE3_EXE_BASE).exe $(PREFIX)\bin + copy /Y $(SQLITE3_EXE_BASE).pdb $(PREFIX)\bin + +install-static: $(SQLITE3_STATIC_BASE).lib + if not exist $(PREFIX)\include mkdir $(PREFIX)\include + if not exist $(PREFIX)\lib mkdir $(PREFIX)\lib + copy /Y sqlite3.h $(PREFIX)\include + copy /Y sqlite3ext.h $(PREFIX)\include + copy /Y $(SQLITE3_STATIC_BASE).lib $(PREFIX)\lib + +clean: + del *.obj *.lib *.exe *.pdb *.dll *.exp + +$(SQLITE3_STATIC_BASE).lib: sqlite3.c sqlite3.h + $(CC) $(CFLAGS) /Fd$(SQLITE3_STATIC_BASE).pdb /c sqlite3.c + $(AR) sqlite3.obj /OUT:$(SQLITE3_STATIC_BASE).lib + +$(SQLITE3_EXE_BASE).exe: shell.c sqlite3.c sqlite3.h + $(CC) $(CFLAGS) shell.c sqlite3.c /Fd$(SQLITE3_EXE_BASE).pdb /Fe$(SQLITE3_EXE_BASE).exe + +$(SQLITE3_DLL_BASE).dll: sqlite3.c sqlite3.h + $(CC) $(CFLAGS) /DSQLITE_API=__declspec(dllexport) /Fd$(SQLITE3_DLL_BASE).pdb /c sqlite3.c $(LINK) /DLL /OUT:$(SQLITE3_DLL_BASE).dll sqlite3.obj \ No newline at end of file diff --git a/src/globals/extra/gd_config_80.w32 b/src/globals/extra/gd_config_80.w32 index ea7582b8..528c2113 100644 --- a/src/globals/extra/gd_config_80.w32 +++ b/src/globals/extra/gd_config_80.w32 @@ -1,86 +1,86 @@ -// 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 is not supported on php <= 8.0 - - 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_pixelate.c gd_rotate.c gd_color_match.c gd_webp.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"); -} +// 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 is not supported on php <= 8.0 + + 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_pixelate.c gd_rotate.c gd_color_match.c gd_webp.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/extra/gd_config_81.w32 b/src/globals/extra/gd_config_81.w32 index 528de319..5f07da8a 100644 --- a/src/globals/extra/gd_config_81.w32 +++ b/src/globals/extra/gd_config_81.w32 @@ -1,94 +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_pixelate.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"); -} +// 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_pixelate.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/extra/gd_config_85.w32 b/src/globals/extra/gd_config_85.w32 index e980b003..033a3857 100644 --- a/src/globals/extra/gd_config_85.w32 +++ b/src/globals/extra/gd_config_85.w32 @@ -1,94 +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"); -} +// 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/internal-env.php b/src/globals/internal-env.php index 97df8841..cd3f3e3b 100644 --- a/src/globals/internal-env.php +++ b/src/globals/internal-env.php @@ -3,22 +3,11 @@ declare(strict_types=1); // 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 @@ -75,31 +64,3 @@ putenv('CPU_COUNT=' . CPU_COUNT); 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/licenses/postgresql.txt b/src/globals/licenses/postgresql.txt index 2f33cebb..0a397648 100644 --- a/src/globals/licenses/postgresql.txt +++ b/src/globals/licenses/postgresql.txt @@ -1,23 +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. +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. 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. */ diff --git a/tests/GlobalsFunctionsTest.php b/tests/GlobalsFunctionsTest.php index b1b047db..c2d159c4 100644 --- a/tests/GlobalsFunctionsTest.php +++ b/tests/GlobalsFunctionsTest.php @@ -21,6 +21,8 @@ class GlobalsFunctionsTest extends TestCase protected function tearDown(): void { $GLOBALS['spc_log_filters'] = null; + // Restore logger level to avoid polluting other tests with DEBUG noise + logger()->setLevel(LogLevel::ERROR); } public function testAddLogFilterDeduplicates(): void diff --git a/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php b/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php index 919452d6..0b033b14 100644 --- a/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php +++ b/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php @@ -23,12 +23,10 @@ class ArtifactDownloaderTest extends TestCase // Reset ArtifactConfig and ArtifactLoader static state $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); } @@ -38,12 +36,10 @@ class ArtifactDownloaderTest extends TestCase $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); } @@ -343,7 +339,6 @@ class ArtifactDownloaderTest extends TestCase { $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 index d95275a6..c11031cd 100644 --- a/tests/StaticPHP/Artifact/ArtifactExtractorTest.php +++ b/tests/StaticPHP/Artifact/ArtifactExtractorTest.php @@ -31,12 +31,10 @@ class ArtifactExtractorTest extends TestCase $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(); @@ -51,12 +49,10 @@ class ArtifactExtractorTest extends TestCase $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(); @@ -157,7 +153,6 @@ class ArtifactExtractorTest extends TestCase // 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); @@ -181,7 +176,6 @@ class ArtifactExtractorTest extends TestCase // 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); @@ -204,7 +198,6 @@ class ArtifactExtractorTest extends TestCase { $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/ArtifactTest.php b/tests/StaticPHP/Artifact/ArtifactTest.php index 0effb109..1a852672 100644 --- a/tests/StaticPHP/Artifact/ArtifactTest.php +++ b/tests/StaticPHP/Artifact/ArtifactTest.php @@ -29,7 +29,6 @@ class ArtifactTest extends TestCase // Reset ArtifactConfig static state $reflection = new \ReflectionClass(ArtifactConfig::class); $property = $reflection->getProperty('artifact_configs'); - $property->setAccessible(true); $property->setValue(null, []); // Reset DI container @@ -45,7 +44,6 @@ class ArtifactTest extends TestCase $reflection = new \ReflectionClass(ArtifactConfig::class); $property = $reflection->getProperty('artifact_configs'); - $property->setAccessible(true); $property->setValue(null, []); ApplicationContext::reset(); @@ -715,7 +713,6 @@ class ArtifactTest extends TestCase { $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/Command/Dev/GenExtTestMatrixCommandTest.php b/tests/StaticPHP/Command/Dev/GenExtTestMatrixCommandTest.php new file mode 100644 index 00000000..4db6da59 --- /dev/null +++ b/tests/StaticPHP/Command/Dev/GenExtTestMatrixCommandTest.php @@ -0,0 +1,326 @@ +getProperty('package_configs'); + $prop->setValue(null, []); + + // Register fixture packages + PackageConfig::loadFromArray(self::buildFixture(), 'test'); + + // Set up Symfony Application with the command under test + $this->app = new Application(); + $this->app->add(new GenExtTestMatrixCommand()); + $this->app->setAutoExit(false); + } + + protected function tearDown(): void + { + parent::tearDown(); + + // Reset PackageConfig static state + $ref = new \ReflectionClass(PackageConfig::class); + $prop = $ref->getProperty('package_configs'); + $prop->setValue(null, []); + + // Restore logger level (BaseCommand::execute() may have changed it) + logger()->setLevel(LogLevel::ERROR); + } + + // ────────────────────────────────────────────────────────────────────────── + // Tests + // ────────────────────────────────────────────────────────────────────────── + + /** + * swoole entry must contain all swoole-hook-* virtuals and nothing else. + */ + public function testSwooleBundlesHookVirtuals(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + $swooleEntries = $this->findEntriesContaining($matrix, 'swoole'); + $this->assertCount(1, $swooleEntries, 'Expected exactly one entry containing swoole'); + + $parts = explode(',', $swooleEntries[0]['extension']); + sort($parts); + + $this->assertContains('swoole', $parts); + $this->assertContains('swoole-hook-mysql', $parts); + $this->assertContains('swoole-hook-pgsql', $parts); + } + + /** + * curl must NOT appear in the same entry as swoole, even though swoole depends on it. + */ + public function testCurlIsNotPulledIntoSwooleEntry(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + // The swoole entry must not contain 'curl' + $swooleEntries = $this->findEntriesContaining($matrix, 'swoole'); + $this->assertCount(1, $swooleEntries); + $parts = explode(',', $swooleEntries[0]['extension']); + $this->assertNotContains('curl', $parts, 'curl must not appear inside the swoole matrix entry'); + + // curl must appear in a separate entry + $curlEntries = $this->findEntriesContaining($matrix, 'curl'); + $this->assertNotEmpty($curlEntries, 'curl must have its own matrix entry'); + } + + /** + * swow must be fully isolated — its entry should only contain 'swow'. + */ + public function testSwowIsIsolated(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + $swowEntries = $this->findEntriesContaining($matrix, 'swow'); + $this->assertCount(1, $swowEntries, 'Expected exactly one entry containing swow'); + $this->assertSame('swow', $swowEntries[0]['extension'], 'swow entry must contain only swow'); + } + + /** + * dom and xml must appear in the same matrix entry (DFS chain). + */ + public function testDomXmlChain(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + $chainEntries = $this->findEntriesContaining($matrix, 'dom', 'xml'); + $this->assertNotEmpty($chainEntries, 'dom and xml must appear in the same matrix entry'); + } + + /** + * --os=Windows must exclude ext-linux-only. + */ + public function testOsFilterExcludesLinuxOnlyFromWindows(): void + { + $matrix = $this->runMatrix(['--os' => 'Windows']); + + $linuxOnlyEntries = $this->findEntriesContaining($matrix, 'linux-only'); + $this->assertEmpty($linuxOnlyEntries, 'ext-linux-only must not appear in the Windows matrix'); + } + + /** + * --os=Linux must include ext-linux-only. + */ + public function testOsFilterIncludesLinuxOnly(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + $linuxOnlyEntries = $this->findEntriesContaining($matrix, 'linux-only'); + $this->assertNotEmpty($linuxOnlyEntries, 'ext-linux-only must appear in the Linux matrix'); + } + + /** + * All returned entries must reference the requested OS runner when --os is specified. + */ + public function testOsFilterRestrictsRunners(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + foreach ($matrix as $entry) { + $this->assertSame('linux', $entry['os'], "Entry {$entry['extension']} must only target Linux"); + } + } + + /** + * --for-extensions=redis must return only entries that contain 'redis'. + */ + public function testForExtensionsFilter(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux', '--for-extensions' => 'redis']); + + $this->assertNotEmpty($matrix, '--for-extensions=redis must yield at least one entry'); + foreach ($matrix as $entry) { + $parts = explode(',', $entry['extension']); + $this->assertContains('redis', $parts, "Entry {$entry['extension']} does not contain redis"); + } + } + + /** + * --for-libs=libxml2 must return only entries whose extension(s) depend on libxml2. + */ + public function testForLibsFilter(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux', '--for-libs' => 'libxml2']); + + $this->assertNotEmpty($matrix, '--for-libs=libxml2 must yield at least one entry'); + foreach ($matrix as $entry) { + $parts = explode(',', $entry['extension']); + // xml depends on libxml2 directly; dom depends on xml (which depends on libxml2) + $match = count(array_intersect($parts, ['xml', 'dom'])) > 0; + $this->assertTrue($match, "Entry {$entry['extension']} should not appear in --for-libs=libxml2 results"); + } + } + + /** + * --for-libs must include extensions that depend on the library through other libraries. + */ + public function testForLibsFilterIncludesTransitiveLibraryDeps(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux', '--for-libs' => 'libde265']); + + $this->assertNotEmpty($matrix, '--for-libs=libde265 must yield at least one entry'); + foreach ($matrix as $entry) { + $parts = explode(',', $entry['extension']); + $this->assertContains('imagick', $parts, "Entry {$entry['extension']} should not appear in --for-libs=libde265 results"); + } + } + + /** + * Multiple filters should include entries matching any changed package. + */ + public function testExtensionAndLibraryFiltersAreCombinedAsUnion(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux', '--for-extensions' => 'simdjson', '--for-libs' => 'libde265']); + + $this->assertNotEmpty($this->findEntriesContaining($matrix, 'simdjson'), 'simdjson entry must be included'); + $this->assertNotEmpty($this->findEntriesContaining($matrix, 'imagick'), 'imagick entry must be included through libde265'); + } + + /** + * --tier2 must produce only Tier2 runners and no Windows entries. + */ + public function testTier2Flag(): void + { + $matrix = $this->runMatrix(['--tier2' => true]); + + $this->assertNotEmpty($matrix); + foreach ($matrix as $entry) { + $this->assertNotSame('windows', $entry['os'], '--tier2 must not include Windows entries'); + $this->assertContains( + $entry['runner'], + ['ubuntu-24.04-arm', 'macos-15-intel'], + "Runner {$entry['runner']} is not a valid Tier2 runner" + ); + } + } + + /** + * Each entry must have the mandatory keys and correct types. + */ + public function testEntryShape(): void + { + $matrix = $this->runMatrix(['--os' => 'Linux']); + + $this->assertNotEmpty($matrix); + foreach ($matrix as $entry) { + $this->assertArrayHasKey('runner', $entry); + $this->assertArrayHasKey('os', $entry); + $this->assertArrayHasKey('arch', $entry); + $this->assertArrayHasKey('extension', $entry); + $this->assertArrayHasKey('build-args', $entry); + $this->assertIsString($entry['extension']); + $this->assertIsString($entry['build-args']); + $this->assertStringContainsString($entry['extension'], $entry['build-args']); + } + } + + // ────────────────────────────────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────────────────────────────────── + + /** + * Run the command with the given options and return the parsed JSON matrix. + */ + private function runMatrix(array $options = []): array + { + $tester = new CommandTester($this->app->find('dev:gen-ext-test-matrix')); + $tester->execute($options, ['decorated' => false]); + $output = $tester->getDisplay(); + $matrix = json_decode($output, true); + $this->assertIsArray($matrix, "Command output is not valid JSON. Output:\n{$output}"); + return $matrix; + } + + /** + * Find matrix entries whose 'extension' field contains all of the given names. + * + * @return array[] matching entries + */ + private function findEntriesContaining(array $matrix, string ...$names): array + { + return array_values(array_filter($matrix, static function (array $entry) use ($names): bool { + $parts = explode(',', $entry['extension']); + foreach ($names as $name) { + if (!in_array($name, $parts, true)) { + return false; + } + } + return true; + })); + } + + /** + * Minimal valid php-extension fixture. + * + * Layout: + * - ext-swow standalone isolated, no ext deps + * - ext-swoole standalone isolated, depends on ext-curl + * - ext-swoole-hook-* virtual (arg-type: none) — must be bundled with swoole + * - ext-curl simple orphan, depended on by swoole but must NOT be pulled into swoole entry + * - ext-redis simple orphan + * - ext-simdjson simple orphan used for combined filter tests + * - ext-xml depends on lib 'libxml2' + * - ext-dom depends on ext-xml (DFS chain) + * - ext-imagick depends on imagemagick -> libheif -> libde265 + * - ext-linux-only restricted to Linux via os: [Linux] + */ + private static function buildFixture(): array + { + // php-extension must be a non-empty assoc array ([] fails is_assoc_array() check). + $ext = static fn (array $phpExt = ['arg-type' => 'standard'], array $topLevel = []): array => array_merge(['type' => 'php-extension', 'php-extension' => $phpExt], $topLevel); + $lib = static fn (array $topLevel = []): array => array_merge(['type' => 'library', 'artifact' => ['source' => 'custom']], $topLevel); + + return [ + // Isolated standalones + 'ext-swow' => $ext(), + 'ext-swoole' => $ext(['arg-type' => 'standard'], ['depends' => ['ext-curl']]), + + // Swoole hook virtuals (arg-type: none → virtual) + 'ext-swoole-hook-mysql' => $ext(['arg-type' => 'none']), + 'ext-swoole-hook-pgsql' => $ext(['arg-type' => 'none']), + + // Simple orphans + 'ext-curl' => $ext(), + 'ext-redis' => $ext(), + 'ext-simdjson' => $ext(), + + // DFS chain: dom depends on xml; xml depends on lib 'libxml2' + 'ext-xml' => $ext(['arg-type' => 'standard'], ['depends' => ['libxml2']]), + 'ext-dom' => $ext(['arg-type' => 'standard'], ['depends' => ['ext-xml']]), + + // Transitive library chain: imagick -> imagemagick -> libheif -> libde265 + 'ext-imagick' => $ext(['arg-type' => 'standard'], ['depends' => ['imagemagick']]), + 'imagemagick' => $lib(['depends' => ['libheif']]), + 'libheif' => $lib(['depends' => ['libde265']]), + 'libde265' => $lib(), + + // OS-restricted to Linux only + 'ext-linux-only' => $ext(['os' => ['Linux']]), + ]; + } +} diff --git a/tests/StaticPHP/Config/ArtifactConfigTest.php b/tests/StaticPHP/Config/ArtifactConfigTest.php index f9990365..ac726a9a 100644 --- a/tests/StaticPHP/Config/ArtifactConfigTest.php +++ b/tests/StaticPHP/Config/ArtifactConfigTest.php @@ -25,7 +25,6 @@ class ArtifactConfigTest extends TestCase // Reset static state $reflection = new \ReflectionClass(ArtifactConfig::class); $property = $reflection->getProperty('artifact_configs'); - $property->setAccessible(true); $property->setValue([]); } @@ -41,7 +40,6 @@ class ArtifactConfigTest extends TestCase // Reset static state $reflection = new \ReflectionClass(ArtifactConfig::class); $property = $reflection->getProperty('artifact_configs'); - $property->setAccessible(true); $property->setValue([]); } diff --git a/tests/StaticPHP/Config/ConfigTypeTest.php b/tests/StaticPHP/Config/ConfigTypeTest.php index 0990931e..bf3d1bc8 100644 --- a/tests/StaticPHP/Config/ConfigTypeTest.php +++ b/tests/StaticPHP/Config/ConfigTypeTest.php @@ -27,6 +27,7 @@ class ConfigTypeTest extends TestCase 'php-extension', 'target', 'virtual-target', + 'tool', ]; $this->assertEquals($expectedTypes, ConfigType::PACKAGE_TYPES); diff --git a/tests/StaticPHP/Config/PackageConfigTest.php b/tests/StaticPHP/Config/PackageConfigTest.php index ce28aebc..c243a99e 100644 --- a/tests/StaticPHP/Config/PackageConfigTest.php +++ b/tests/StaticPHP/Config/PackageConfigTest.php @@ -26,7 +26,6 @@ class PackageConfigTest extends TestCase // Reset static state $reflection = new \ReflectionClass(PackageConfig::class); $property = $reflection->getProperty('package_configs'); - $property->setAccessible(true); $property->setValue([]); } @@ -41,7 +40,6 @@ class PackageConfigTest extends TestCase // Reset static state $reflection = new \ReflectionClass(PackageConfig::class); $property = $reflection->getProperty('package_configs'); - $property->setAccessible(true); $property->setValue([]); } diff --git a/tests/StaticPHP/Registry/ArtifactLoaderTest.php b/tests/StaticPHP/Registry/ArtifactLoaderTest.php index 75370dfe..c56521c1 100644 --- a/tests/StaticPHP/Registry/ArtifactLoaderTest.php +++ b/tests/StaticPHP/Registry/ArtifactLoaderTest.php @@ -32,12 +32,10 @@ class ArtifactLoaderTest extends TestCase // 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, []); } @@ -52,12 +50,10 @@ class ArtifactLoaderTest extends TestCase // 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, []); } @@ -429,7 +425,6 @@ class TestArtifact1 { { $reflection = new \ReflectionClass(ArtifactConfig::class); $property = $reflection->getProperty('artifact_configs'); - $property->setAccessible(true); $configs = $property->getValue(); $configs[$name] = [ 'type' => 'source', diff --git a/tests/StaticPHP/Registry/DoctorLoaderTest.php b/tests/StaticPHP/Registry/DoctorLoaderTest.php index 7817a880..40ebaa7d 100644 --- a/tests/StaticPHP/Registry/DoctorLoaderTest.php +++ b/tests/StaticPHP/Registry/DoctorLoaderTest.php @@ -26,11 +26,9 @@ class DoctorLoaderTest extends TestCase // 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, []); } @@ -45,11 +43,9 @@ class DoctorLoaderTest extends TestCase // 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, []); } diff --git a/tests/StaticPHP/Registry/PackageLoaderTest.php b/tests/StaticPHP/Registry/PackageLoaderTest.php index a40c79fa..cb78cd18 100644 --- a/tests/StaticPHP/Registry/PackageLoaderTest.php +++ b/tests/StaticPHP/Registry/PackageLoaderTest.php @@ -33,25 +33,20 @@ class PackageLoaderTest extends TestCase $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, []); } @@ -67,25 +62,20 @@ class PackageLoaderTest extends TestCase $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, []); } @@ -359,7 +349,6 @@ class PackageLoaderTest extends TestCase // 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]], @@ -384,7 +373,6 @@ class PackageLoaderTest extends TestCase // 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]], @@ -408,7 +396,6 @@ class PackageLoaderTest extends TestCase // 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']], @@ -435,7 +422,6 @@ class PackageLoaderTest extends TestCase // 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]], @@ -458,7 +444,6 @@ class PackageLoaderTest extends TestCase $reflection = new \ReflectionClass(PackageLoader::class); $property = $reflection->getProperty('before_stages'); - $property->setAccessible(true); $property->setValue(null, [ 'test-package' => [ 'test-stage' => [ @@ -482,7 +467,6 @@ class PackageLoaderTest extends TestCase $reflection = new \ReflectionClass(PackageLoader::class); $property = $reflection->getProperty('after_stages'); - $property->setAccessible(true); $property->setValue(null, [ 'test-package' => [ 'test-stage' => [ @@ -533,13 +517,13 @@ class PackageLoaderTest extends TestCase mkdir($psr4Dir, 0755, true); // Create test class file - $classContent = 'getProperty('package_configs'); - $property->setAccessible(true); $configs = $property->getValue(); $configs[$name] = [ 'type' => $type, diff --git a/tests/StaticPHP/Util/DependencyResolverTest.php b/tests/StaticPHP/Util/DependencyResolverTest.php new file mode 100644 index 00000000..b9098c66 --- /dev/null +++ b/tests/StaticPHP/Util/DependencyResolverTest.php @@ -0,0 +1,531 @@ +resetPackageConfig(); + } + + protected function tearDown(): void + { + parent::tearDown(); + $this->resetPackageConfig(); + } + + // ────────────────────────────────────────────── + // Basic resolution + // ────────────────────────────────────────────── + + public function testResolveSinglePackageNoDependencies(): void + { + $this->loadConfig([ + 'zlib' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['zlib']); + + $this->assertSame(['zlib'], $result); + } + + public function testResolveLinearChain(): void + { + // a -> b -> c (a depends on b, b depends on c) + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + 'b' => ['type' => 'library', 'depends' => ['c']], + 'c' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['a']); + + // c must be first, then b, then a + $this->assertSame(['c', 'b', 'a'], $result); + } + + public function testResolveMultipleIndependentChains(): void + { + // a -> b, x -> y (two independent dependency chains) + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + 'b' => ['type' => 'library'], + 'x' => ['type' => 'library', 'depends' => ['y']], + 'y' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['a', 'x']); + + // Dependencies must come before their dependants + $posB = array_search('b', $result, true); + $posA = array_search('a', $result, true); + $posY = array_search('y', $result, true); + $posX = array_search('x', $result, true); + + $this->assertIsInt($posB); + $this->assertIsInt($posA); + $this->assertIsInt($posY); + $this->assertIsInt($posX); + $this->assertLessThan($posA, $posB, 'b should come before a'); + $this->assertLessThan($posX, $posY, 'y should come before x'); + } + + public function testResolveSharedDependency(): void + { + // a -> c, b -> c (c is shared) + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['c']], + 'b' => ['type' => 'library', 'depends' => ['c']], + 'c' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['a', 'b']); + + // c must appear exactly once and before both a and b + $cCount = count(array_keys($result, 'c', true)); + $this->assertSame(1, $cCount, 'Shared dependency c should appear exactly once'); + + $posC = array_search('c', $result, true); + $posA = array_search('a', $result, true); + $posB = array_search('b', $result, true); + + $this->assertLessThan($posA, $posC, 'c should come before a'); + $this->assertLessThan($posB, $posC, 'c should come before b'); + } + + public function testResolveDiamondDependency(): void + { + // a + // / \ + // b c + // \ / + // d + $this->loadConfig([ + 'a' => ['type' => 'target', 'depends' => ['b', 'c']], + 'b' => ['type' => 'library', 'depends' => ['d']], + 'c' => ['type' => 'library', 'depends' => ['d']], + 'd' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['a']); + + // d must appear exactly once and before everything + $dCount = count(array_keys($result, 'd', true)); + $this->assertSame(1, $dCount); + + $posD = array_search('d', $result, true); + $posB = array_search('b', $result, true); + $posC = array_search('c', $result, true); + $posA = array_search('a', $result, true); + + $this->assertLessThan($posB, $posD, 'd should come before b'); + $this->assertLessThan($posC, $posD, 'd should come before c'); + $this->assertLessThan($posA, $posB, 'b should come before a'); + $this->assertLessThan($posA, $posC, 'c should come before a'); + } + + // ────────────────────────────────────────────── + // Suggests (optional dependencies) + // ────────────────────────────────────────────── + + public function testResolveSuggestsAreExcludedByDefault(): void + { + // a depends on b, suggests c + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b'], 'suggests' => ['c']], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['a']); + + // c should NOT be in the resolved list (it's only suggested, not depended) + $this->assertNotContains('c', $result); + $this->assertSame(['b', 'a'], $result); + } + + public function testResolveSuggestsIncludedWhenFlagSet(): void + { + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b'], 'suggests' => ['c']], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library', 'depends' => ['b']], + ]); + + $result = DependencyResolver::resolve(['a'], include_suggests: true); + + // c IS a suggest of a and should be included when flag is set + $this->assertContains('c', $result); + $posB = array_search('b', $result, true); + $posC = array_search('c', $result, true); + $posA = array_search('a', $result, true); + $this->assertLessThan($posA, $posB, 'b should come before a'); + $this->assertLessThan($posA, $posC, 'c should come before a'); + } + + // ────────────────────────────────────────────── + // Virtual-target promotion + // ────────────────────────────────────────────── + + public function testResolveVirtualTargetPromotesDepsToParent(): void + { + // php-cli (virtual-target) depends on [php, ext-ctype] + // When php-cli is in the input, ext-ctype should be promoted to php's deps + $this->loadConfig([ + 'php-cli' => ['type' => 'virtual-target', 'depends' => ['php', 'ext-ctype']], + 'php' => ['type' => 'target', 'depends' => ['libxml2']], + 'ext-ctype' => ['type' => 'php-extension', 'depends' => []], + 'libxml2' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['php-cli']); + + $posPhp = array_search('php', $result, true); + $posCtype = array_search('ext-ctype', $result, true); + $posLibxml2 = array_search('libxml2', $result, true); + + $this->assertIsInt($posPhp); + $this->assertIsInt($posCtype); + $this->assertIsInt($posLibxml2); + + // ext-ctype was promoted to php's deps, so it must come before php + $this->assertLessThan($posPhp, $posCtype, 'ext-ctype should come before php (promoted dep)'); + // libxml2 is a native dep of php, so it must also come before php + $this->assertLessThan($posPhp, $posLibxml2, 'libxml2 should come before php'); + } + + public function testResolveVirtualTargetNotInInputDoesNotPromote(): void + { + // php-cli is a virtual-target but NOT in the input request, + // so its deps should NOT be injected into php + $this->loadConfig([ + 'php-cli' => ['type' => 'virtual-target', 'depends' => ['php', 'ext-ctype']], + 'php' => ['type' => 'target', 'depends' => ['libxml2']], + 'ext-ctype' => ['type' => 'php-extension'], + 'libxml2' => ['type' => 'library'], + ]); + + // Only php is requested, not php-cli + $result = DependencyResolver::resolve(['php']); + + // ext-ctype should NOT be in the result since php-cli was not requested + $this->assertNotContains('ext-ctype', $result); + $this->assertSame(['libxml2', 'php'], $result); + } + + // ────────────────────────────────────────────── + // Dependency overrides + // ────────────────────────────────────────────── + + public function testResolveDependencyOverridesAddDeps(): void + { + $this->loadConfig([ + 'a' => ['type' => 'library'], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library'], + ]); + + // Override: a now depends on b and c + $result = DependencyResolver::resolve(['a'], dependency_overrides: [ + 'a' => ['b', 'c'], + ]); + + $posA = array_search('a', $result, true); + $posB = array_search('b', $result, true); + $posC = array_search('c', $result, true); + + $this->assertLessThan($posA, $posB, 'b should come before a (override)'); + $this->assertLessThan($posA, $posC, 'c should come before a (override)'); + } + + public function testResolveDependencyOverridesMergeWithExisting(): void + { + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library'], + ]); + + // a natively depends on b, override adds c + $result = DependencyResolver::resolve(['a'], dependency_overrides: [ + 'a' => ['c'], + ]); + + $this->assertContains('b', $result); + $this->assertContains('c', $result); + $posA = array_search('a', $result, true); + $posB = array_search('b', $result, true); + $posC = array_search('c', $result, true); + $this->assertLessThan($posA, $posB, 'b should come before a'); + $this->assertLessThan($posA, $posC, 'c should come before a'); + } + + // ────────────────────────────────────────────── + // Error handling + // ────────────────────────────────────────────── + + public function testResolveUnknownPackageThrowsException(): void + { + $this->loadConfig([ + 'zlib' => ['type' => 'library'], + ]); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('does not exist in config'); + + DependencyResolver::resolve(['nonexistent']); + } + + public function testResolveUnregisteredDependencyThrowsException(): void + { + // a depends on b, but b is not in the config + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + ]); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('not exist'); + + DependencyResolver::resolve(['a']); + } + + // ────────────────────────────────────────────── + // Reverse dependency map ($why parameter) + // ────────────────────────────────────────────── + + public function testReverseDependencyMap(): void + { + // a -> b -> c + $this->loadConfig([ + 'a' => ['type' => 'target', 'depends' => ['b']], + 'b' => ['type' => 'library', 'depends' => ['c']], + 'c' => ['type' => 'library'], + ]); + + $why = []; + DependencyResolver::resolve(['a'], why: $why); + + $this->assertArrayHasKey('c', $why, 'c is depended upon'); + $this->assertContains('b', $why['c'], 'b depends on c'); + $this->assertArrayHasKey('b', $why, 'b is depended upon'); + $this->assertContains('a', $why['b'], 'a depends on b'); + } + + public function testReverseDependencyMapOnlyIncludesResolvedPackages(): void + { + // a -> b -> c, but only requesting a + // d is not in the resolved set + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + 'b' => ['type' => 'library', 'depends' => ['c']], + 'c' => ['type' => 'library'], + 'd' => ['type' => 'library', 'depends' => ['c']], // not in input + ]); + + $why = []; + DependencyResolver::resolve(['a'], why: $why); + + // d should NOT appear in the reverse map since it's not in the resolved set + $this->assertArrayNotHasKey('d', $why); + } + + // ────────────────────────────────────────────── + // getSubDependencies + // ────────────────────────────────────────────── + + public function testGetSubDependenciesLinearChain(): void + { + // a -> b -> c -> d + $this->loadConfig([ + 'a' => ['type' => 'target', 'depends' => ['b']], + 'b' => ['type' => 'library', 'depends' => ['c']], + 'c' => ['type' => 'library', 'depends' => ['d']], + 'd' => ['type' => 'library'], + ]); + + $subDeps = DependencyResolver::getSubDependencies('a', ['a', 'b', 'c', 'd']); + + // Should return [d, c, b] in dependency order (a not included) + $this->assertNotContains('a', $subDeps); + $this->assertSame(['d', 'c', 'b'], $subDeps); + } + + public function testGetSubDependenciesPackageNotInResolvedSet(): void + { + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + 'b' => ['type' => 'library'], + ]); + + $subDeps = DependencyResolver::getSubDependencies('nonexistent', ['a', 'b']); + + $this->assertSame([], $subDeps); + } + + public function testGetSubDependenciesWithSuggests(): void + { + $this->loadConfig([ + 'a' => ['type' => 'target', 'depends' => ['b'], 'suggests' => ['c']], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library'], + ]); + + // Without include_suggests: only b is a sub-dep + $without = DependencyResolver::getSubDependencies('a', ['a', 'b', 'c'], include_suggests: false); + $this->assertSame(['b'], $without); + + // With include_suggests: both b and c are sub-deps + $with = DependencyResolver::getSubDependencies('a', ['a', 'b', 'c'], include_suggests: true); + $this->assertContains('b', $with); + $this->assertContains('c', $with); + } + + public function testGetSubDependenciesOnlyIncludesResolvedDeps(): void + { + // a depends on b and c, but c is not in the resolved set + $this->loadConfig([ + 'a' => ['type' => 'target', 'depends' => ['b', 'c']], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library'], + ]); + + // c is NOT in the resolved set + $subDeps = DependencyResolver::getSubDependencies('a', ['a', 'b']); + + $this->assertContains('b', $subDeps); + $this->assertNotContains('c', $subDeps, 'c is not in the resolved set, should be excluded'); + } + + // ────────────────────────────────────────────── + // Edge cases & defensive + // ────────────────────────────────────────────── + + public function testResolveEmptyInput(): void + { + $this->loadConfig([ + 'zlib' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve([]); + + $this->assertSame([], $result); + } + + public function testResolveWithStringAndPackageInstanceMixed(): void + { + $this->loadConfig([ + 'a' => ['type' => 'library'], + 'b' => ['type' => 'library'], + ]); + + // Pass one as string, one as a mock Package + $mockPackage = $this->createMockPackage('a'); + + $result = DependencyResolver::resolve([$mockPackage, 'b']); + + $this->assertContains('a', $result); + $this->assertContains('b', $result); + } + + public function testResolveDuplicateInputPackages(): void + { + // Requesting the same package twice should not duplicate it in output + $this->loadConfig([ + 'zlib' => ['type' => 'library'], + ]); + + $result = DependencyResolver::resolve(['zlib', 'zlib']); + + $this->assertSame(['zlib'], $result); + } + + /** + * Documents the current behavior for circular dependencies. + * The algorithm does not detect cycles; it silently resolves them + * using the visited-set to break infinite recursion. This test + * locks in the current behavior so any intentional change is caught. + */ + public function testCircularDependencyDoesNotLoopInfinitely(): void + { + // a -> b -> a (circular) + $this->loadConfig([ + 'a' => ['type' => 'library', 'depends' => ['b']], + 'b' => ['type' => 'library', 'depends' => ['a']], + ]); + + // Must not hang — should complete and return both packages + $result = DependencyResolver::resolve(['a']); + + $this->assertCount(2, $result); + $this->assertContains('a', $result); + $this->assertContains('b', $result); + } + + // ────────────────────────────────────────────── + // Helpers + // ────────────────────────────────────────────── + + /** + * Load package configurations directly into PackageConfig. + * Uses reflection to inject fixture data without needing YAML files on disk. + * + * @param array $configs + */ + private function loadConfig(array $configs): void + { + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + + $existing = $property->getValue(); + if (!is_array($existing)) { + $existing = []; + } + + foreach ($configs as $name => $config) { + $existing[$name] = $config; + } + + $property->setValue(null, $existing); + } + + /** + * Reset PackageConfig to empty state. + */ + private function resetPackageConfig(): void + { + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + $property->setValue(null, []); + } + + /** + * Create a minimal mock Package object that returns a given name. + */ + private function createMockPackage(string $name): object + { + return new class($name) { + public function __construct(private string $name) {} + + public function getName(): string + { + return $this->name; + } + }; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 14397d03..f45c9f4a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,8 +2,10 @@ declare(strict_types=1); use Psr\Log\LogLevel; +use StaticPHP\Registry\Registry; require_once __DIR__ . '/../src/bootstrap.php'; -\StaticPHP\Registry\Registry::resolve(); logger()->setLevel(LogLevel::ERROR); + +Registry::resolve();