Compare commits

...

42 Commits

Author SHA1 Message Date
crazywhalecc
71d20a8322 fix(docs): update Windows SAPI warning and download links in CliGenerator.vue 2026-07-14 23:27:46 +08:00
crazywhalecc
01a0b26a2a feat(docs): update package model documentation to include new package types and clarify definitions 2026-07-14 23:03:15 +08:00
crazywhalecc
abc6e3695f feat(docs): add staticphp-documentation-sync skill for bilingual documentation synchronization 2026-07-14 23:02:33 +08:00
crazywhalecc
a1ccaaca36 fix(docs): update build scripts and dependencies for VitePress 2026-07-14 23:00:56 +08:00
Jerry Ma
c1eefb8399 feat(manifest): implement BuildManifestDumper for generating build manifest (#1210) 2026-07-13 18:53:54 +08:00
crazywhalecc
4d00e338af feat(manifest): implement BuildManifestDumper for generating build manifest 2026-07-13 16:59:19 +08:00
Marc
422323a41d Fable v3 windows (#1207) 2026-07-10 23:25:39 +07:00
Jerry Ma
6144f7affb fix(windows): survive cmd.exe's 8191 character command line limit (#1209) 2026-07-10 21:02:19 +08:00
m-this
6d58d53f7d fix(imagemagick): build the VisualMagick tree under source/ on windows
- C:\im was outside the project so bin/spc reset could not clean it
- with the command line limit fixed a short base path is no longer
  needed; keep SPC_IMAGEMAGICK_BUILD_DIR as an override
2026-07-10 10:52:14 +02:00
m-this
c54fa78ad1 fix(windows): survive cmd.exe's 8191 character command line limit
- with big extension stacks (imagick) the link command lines exceed what
  cmd.exe allows and fail with 'The command line is too long'
- pass the collected LIBS_CLI/LIBS_CGI/LIBS_MICRO lib lists to link.exe
  through @response files, like php-src already does for its object lists
- compile the frankenphp -mthreads shim instead of generating a .bat:
  batch files run through cmd.exe and cap cgo link lines at 8191 chars,
  a real executable gets the full 32K and past that go writes a response
  file by itself; still probe-gated, still a stopgap for golang/go#80290
2026-07-10 08:04:10 +02:00
crazywhalecc
4d6f334cbb Refactor UPX patching logic for phpmicro binary 2026-07-09 16:52:39 +08:00
crazywhalecc
e99423c7d1 Unify GOPATH with go-win custom binary extraction method 2026-07-09 16:20:52 +09:00
crazywhalecc
44daaad548 Resolve xlswriter dllimport issue 2026-07-09 16:20:29 +09:00
crazywhalecc
4dac0d18b4 Fix windows file delete permissions 2026-07-09 16:20:10 +09:00
Jerry Ma
79db27da04 Merge branch 'v3' into fable-v3-windows 2026-07-09 12:31:39 +09:00
crazywhalecc
d04d82a1f1 Fix strawberry-perl detect time 2026-07-09 12:27:41 +09:00
crazywhalecc
dca24585d4 Add frankenphp test label 2026-07-09 11:56:29 +09:00
crazywhalecc
7edd8359d5 Enhance artifact extraction logic to check for existing files and handle hash storage for both file and directory targets 2026-07-09 11:55:54 +09:00
crazywhalecc
be8f239259 Merge remote-tracking branch 'origin/fable-v3-windows' into fable-v3-windows 2026-07-09 11:24:41 +09:00
crazywhalecc
6c7a47c9b5 Trigger full test when containing 'need-full-test' tag 2026-07-09 11:17:10 +09:00
Jerry Ma
859c06a470 Fix the Windows extension test failures on fable-v3-windows (#1208) 2026-07-09 11:09:33 +09:00
crazywhalecc
274706084a Fix frankenphp package path bug 2026-07-09 11:04:00 +09:00
crazywhalecc
0bd94b0ebd Lint 2026-07-09 11:03:39 +09:00
crazywhalecc
150998723d Fix windows captainhook 2026-07-09 11:03:32 +09:00
crazywhalecc
303dca5f59 Remove doctor item 2026-07-09 11:03:15 +09:00
m-this
d7902b9931 feat(tool): bundle python for the meson build
- add a python-win tool package that unzips the official python.org
  nuget package into the pkgroot (venv and ensurepip included)
- meson uses it to build its venv and only falls back to a system
  python, so no manual environment setup is needed
2026-07-08 14:17:36 +02:00
m-this
43b2da0511 fix(libiconv): only put the static lib on the windows link line
- static-libs@windows listed both libiconv.lib (the dll import lib) and
  libiconv_a.lib; the import lib came first, so php.exe ended up
  importing libiconv.dll and the cli smoke test died with
  STATUS_DLL_NOT_FOUND on machines without it
2026-07-08 11:15:11 +02:00
m-this
6dc5c5b770 fix(pgsql): link secur32 and crypt32 on windows
- libpq authenticates through SSPI and the static libcrypto behind it
  reads the system cert store; php.exe failed to link with unresolved
  Cert* and SSPI symbols when the openssl extension is not in the build
2026-07-08 11:15:11 +02:00
m-this
f3afd81c81 fix(php): pass /FORCE:MULTIPLE when linking php-cgi.exe and micro.sfx
- php.exe already links with /FORCE:MULTIPLE for extension-bundled
  duplicate symbols, cgi and micro did not
- imagick's MagickCore defines gettimeofday, also defined in php's
  time.obj, and both links failed with LNK2005
2026-07-08 11:15:11 +02:00
m-this
1dce506222 fix(postgresql): provision meson, ninja and winflexbison on windows
- the meson build assumed meson/ninja/win_flex/win_bison/perl on PATH,
  which only held on machines that had them installed
- add meson, ninja and winflexbison tool packages and declare them via
  tools@windows on postgresql, together with the existing strawberry-perl
- meson no longer ships a Windows MSI, so the wheel is installed offline
  into a venv: postgres re-invokes meson through find_program, which
  needs the real meson.exe launcher pip generates
2026-07-08 11:15:11 +02:00
crazywhalecc
323f21a6f2 Add gdi32.lib 2026-07-08 17:02:32 +09:00
crazywhalecc
ab30c9dc49 fix: update Visual Studio version handling in imagemagick configuration 2026-07-08 16:27:52 +09:00
Jerry Ma
996cca12c0 [v3] - fix(windows): resolve doctor-installed perl and quote perl path (#1174) 2026-07-08 13:59:05 +08:00
crazywhalecc
5451945972 Use escapeshellarg instead 2026-07-08 13:21:52 +08:00
crazywhalecc
fc1673e37f Merge branch 'refs/heads/v3' into pr/win-openssl-perl-path
# Conflicts:
#	src/Package/Library/openssl.php
2026-07-08 13:19:36 +08:00
Marc
f1722a0f3e Merge branch 'v3' into fable-v3-windows 2026-07-08 11:45:02 +07:00
Jerry Ma
5ffbf2d5fe fix(frankenphp): strip Go's -mthreads so the Windows build survives Clang >= 20 (#1201) 2026-07-08 11:47:02 +08:00
mathis
83ce54b62b docs(frankenphp): link golang/go#80290 so the -mthreads workaround can be removed later 2026-07-08 03:59:20 +02:00
m-this
5a5c784c4f fix(frankenphp): strip Go's -mthreads so the Windows build survives Clang >= 20
Go passes the MinGW-only -mthreads flag to the C compiler for cgo builds
(golang/go#16932); Clang >= 20 rejects it for the MSVC target, so the final
frankenphp go build fails on current windows-latest (VS 18 / LLVM 20+).

Wrap Clang with a generated .bat that drops the -mthreads token and forwards
the rest, used only when the detected Clang refuses the flag.
2026-07-07 15:30:52 +02:00
DubbleClick
f66e68754e fable output 2026-06-14 11:51:07 +07:00
Luther Monson
6c59456c02 fix(windows): consolidate to single canonical strawberry-perl path
Per review: spc's pkg config, doctor, and MSVCToolchain all use
PKG_ROOT_PATH\strawberry-perl. The arch-suffixed strawberry-perl-{arch}-win
path was stale and shouldn't be juggled alongside it. Validate against the
one canonical pkg-root path, falling back to WindowsUtil::findCommand.
2026-05-27 22:30:04 -07:00
Luther Monson
0e8a806640 fix(windows): resolve doctor-installed perl and quote perl path
Two related fixes for OpenSSL builds on Windows:

1. spc doctor installs Strawberry Perl under `pkgroot/strawberry-perl/`,
   but the validate() check only looks at the arch-specific native path
   (`strawberry-perl-x86_64-win`). When a user runs `spc doctor` to
   install perl, the subsequent build fails because the doctor's install
   layout isn't on the lookup list. Add it as a fallback before falling
   through to WindowsUtil::findCommand.

2. Wrap the resolved perl path in quote() when building the Configure
   command. WindowsUtil::findCommand can return a path containing spaces
   (e.g. `C:\Program Files\...`), which then splits the cmd.exe
   invocation. Quoting matches how every other interpolated path in this
   command is handled.
2026-05-27 22:07:57 -07:00
53 changed files with 2890 additions and 486 deletions

View File

@@ -10,6 +10,7 @@ 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-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. - `.github/skills/staticphp-build-troubleshooting`: diagnose build, download, doctor, shell, terminal, CI, smoke-test, or log failures.
- `.github/skills/staticphp-documentation-sync`: synchronize bilingual documentation (`docs/en/` and `docs/zh/`) when features, commands, configuration, APIs, or behavior change.
## Quick Reference ## Quick Reference

View File

@@ -0,0 +1,160 @@
---
name: staticphp-documentation-sync
description: Synchronize bilingual documentation when StaticPHP v3 user-facing or developer-facing documentation must change. Use when adding, modifying, deprecating, or removing documented features, CLI commands/options, environment variables, configuration formats, public APIs/extension points, workflows, installation requirements, migration guidance, or user-visible behavior. Do not use solely for internal implementation changes, package/library additions, or build bug fixes unless they change documented behavior or require updating existing docs.
---
# StaticPHP Documentation Sync
## Overview
StaticPHP maintains bilingual documentation under `docs/en/` (English) and `docs/zh/` (Chinese). User-facing or developer-facing documentation changes must be reflected in both languages. Use this skill to identify affected docs, update them in sync, and validate sidebar/config consistency.
## When to Use This Skill
Invoke this skill when the change involves:
- New or modified CLI commands, options, or arguments
- New or changed environment variables
- Package (extension/library/target/tool) additions, removals, or behavioral changes that affect documented support, dependencies, commands, or workflows
- Configuration format changes (YAML fields, artifact definitions, registry)
- Build lifecycle, doctor, or toolchain changes
- API or extension point changes (attributes, hooks, DI, custom artifacts)
- Workflow changes (installation, first build, migration)
- Deprecation or removal of any documented feature
Do not update docs just because code changed. Internal refactors, new libraries used only as dependencies, package build fixes, CI-only changes, and bug fixes that preserve documented behavior usually do not need documentation changes. If existing docs mention the changed behavior, command, package support, dependency, caveat, or workaround, update them.
## Documentation Map
```
docs/
├── en/ # English docs (canonical source)
│ ├── index.md # Home page hero/features
│ ├── guide/ # User-facing guides
│ │ ├── index.md # Overview
│ │ ├── installation.md # Installation instructions
│ │ ├── first-build.md # First build walkthrough
│ │ ├── sapi-reference.md # PHP SAPI options
│ │ ├── cli-reference.md # CLI commands and options
│ │ ├── cli-generator.md # Build command generator
│ │ ├── migrate-from-v2.md # v2 → v3 migration
│ │ ├── extensions.md # Supported extensions list
│ │ ├── extension-notes.md # Per-extension notes
│ │ ├── env-vars.md # Environment variables
│ │ ├── deps-map.md # Dependency map
│ │ └── troubleshooting.md # Troubleshooting guide
│ ├── develop/ # Developer docs
│ │ ├── index.md # Dev overview
│ │ ├── structure.md # Project structure
│ │ ├── registry.md # Registry model
│ │ ├── package-model.md # Package YAML model
│ │ ├── artifact-model.md # Artifact YAML model
│ │ ├── craft-yml.md # craft.yml reference
│ │ ├── build-lifecycle.md # Build stages/lifecycle
│ │ ├── system-build-tools.md # System tool requirements
│ │ ├── doctor-module.md # Doctor module
│ │ ├── php-src-changes.md # PHP source patches
│ │ └── extending/ # Extension authoring
│ │ ├── index.md
│ │ ├── package-classes.md
│ │ ├── annotations.md
│ │ ├── lifecycle-hooks.md
│ │ ├── dependency-injection.md
│ │ └── custom-artifact.md
│ ├── contributing/
│ │ └── index.md
│ └── faq/
│ └── index.md
├── zh/ # Chinese docs (mirrors en/ structure)
│ └── (same structure as en/)
├── .vitepress/
│ ├── sidebar.en.ts # English sidebar config
│ ├── sidebar.zh.ts # Chinese sidebar config
│ └── config.ts # VitePress site config
└── deps-craft-yml.md # Shared craft.yml include used by both languages
```
Other documentation files outside this tree may also matter, especially root-level `ext-support.md`.
**Key rule**: `docs/en/` and `docs/zh/` have identical file trees. Every `.md` file under `en/` must have a corresponding file under `zh/`.
## Workflow
### Step 1: Identify Impact Scope
Map the change to affected documentation:
| Change Type | Likely Affected Docs |
|---|---|
| New/removed CLI command | `guide/cli-reference.md`, `guide/cli-generator.md` |
| New/removed CLI option | `guide/cli-reference.md`, `guide/first-build.md` |
| New env var | `guide/env-vars.md` |
| New/removed documented extension package | `guide/extensions.md`, `guide/extension-notes.md`, `guide/deps-map.md`, root `ext-support.md` |
| Package config field change | `develop/package-model.md`, `develop/artifact-model.md` |
| Build lifecycle change | `develop/build-lifecycle.md` |
| Doctor change | `develop/doctor-module.md`, `develop/system-build-tools.md` |
| New attribute/hook/DI | `develop/extending/annotations.md`, `develop/extending/lifecycle-hooks.md`, `develop/extending/dependency-injection.md` |
| PHP source patch | `develop/php-src-changes.md` |
| Registry change | `develop/registry.md` |
| Install/setup change | `guide/installation.md`, `guide/first-build.md` |
| Deprecation/removal | All docs referencing the feature |
| New doc page | `sidebar.en.ts`, `sidebar.zh.ts` |
### Step 2: Read Current Docs
Before editing, read BOTH the English and Chinese versions of each affected file to understand the current content and existing translation patterns.
```bash
# Example: check both language versions of the CLI reference
cat docs/en/guide/cli-reference.md
cat docs/zh/guide/cli-reference.md
```
### Step 3: Update English Docs First
1. Update `docs/en/` files with accurate, complete English content.
2. Follow existing formatting conventions (headings, code blocks, tables, admonitions).
3. If adding a new page:
- Create the file under `docs/en/<category>/<name>.md`
- Add the corresponding entry in `docs/.vitepress/sidebar.en.ts`
- Ensure the file has proper VitePress frontmatter if needed
### Step 4: Sync Chinese Docs
1. Update `docs/zh/` files to match the English changes.
2. Translate all changed or added prose. Do not leave placeholder text or untranslated English in Chinese docs.
3. Match the structure and formatting of the English version exactly.
4. If a new page was added in English, create the corresponding `docs/zh/` file and update `docs/.vitepress/sidebar.zh.ts`.
5. Treat `docs/deps-craft-yml.md` as a special shared include for both languages. If it needs prose changes, either keep the shared content acceptable for both languages or split the included content before adding language-specific prose.
### Step 5: Validate
Run these checks before considering the task complete:
```bash
# Check that en/ and zh/ have matching file trees
diff <(find docs/en -name '*.md' | sed 's|docs/en/||' | sort) \
<(find docs/zh -name '*.md' | sed 's|docs/zh/||' | sort)
# Check that all sidebar links point to existing files
# (manual check: every 'link' in sidebar.*.ts must resolve to an actual .md file)
# Build docs when Node dependencies are available; this catches many broken links/config issues
npm run docs:build
```
## Translation Guidelines
- **Chinese docs must be complete**: Every section, paragraph, and table in the English docs must have a Chinese equivalent. Never skip sections.
- **Code blocks and command examples**: Keep executable code and commands identical in both languages. Translate surrounding prose; translate comments inside examples only when they are explanatory docs rather than copy-pasteable source/config.
- **Technical terms**: Use consistent translations. Refer to existing Chinese docs for established term translations. When in doubt, prefer the term used elsewhere in `docs/zh/`.
- **Links**: Internal links in Chinese docs should point to `/zh/...` paths (not `/en/...`).
- **YAML frontmatter**: Keep frontmatter identical between languages (layout, etc.) unless the title/tagline field is translated.
## Common Pitfalls
- **Don't update only one language**: This is the most common mistake. Always update both `en/` and `zh/` in the same change.
- **Don't forget sidebar configs**: When adding, renaming, or removing doc pages, update both `sidebar.en.ts` and `sidebar.zh.ts`.
- **Don't forget shared/root docs**: root `ext-support.md` and `docs/deps-craft-yml.md` are also documentation. Update them if relevant.
- **Don't leave TODO markers**: Do not commit TODO translation placeholders unless the user explicitly allows it or the task is blocked; if that happens, call it out in the final response.
- **Don't translate file paths or URLs**: Only translate human-readable content, not paths, URLs, command names, or code identifiers.

View File

@@ -331,5 +331,4 @@ jobs:
with: with:
name: build-meta-${{ inputs.php-version }}-${{ inputs.os }} name: build-meta-${{ inputs.php-version }}-${{ inputs.os }}
path: | path: |
buildroot/build-extensions.json buildroot/build-manifest.json
buildroot/build-libraries.json

View File

@@ -115,5 +115,4 @@ jobs:
with: with:
name: build-meta name: build-meta
path: | path: |
buildroot/build-extensions.json buildroot/build-manifest.json
buildroot/build-libraries.json

View File

@@ -2,13 +2,14 @@ name: "Extension Matrix Tests"
on: on:
workflow_dispatch: workflow_dispatch:
push: pull_request:
types: [opened, synchronize, labeled]
jobs: jobs:
test: test:
name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})"
runs-on: ${{ matrix.operating-system }} runs-on: ${{ matrix.operating-system }}
if: contains(github.event.head_commit.message, 'extension test') || contains(github.event.head_commit.message, 'test extensions') if: contains(github.event.pull_request.labels.*.name, 'need-full-test')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View File

@@ -227,7 +227,7 @@ jobs:
echo "matrix=$(echo "$FINAL" | jq -c '{"combo": .}')" >> "$GITHUB_OUTPUT" echo "matrix=$(echo "$FINAL" | jq -c '{"combo": .}')" >> "$GITHUB_OUTPUT"
ext-test: ext-test:
name: "Ext test: ${{ matrix.combo.extension }} (PHP ${{ matrix.combo.php-version }} · ${{ matrix.combo.os }}-${{ matrix.combo.arch }})" name: "Ext test: ${{ matrix.combo.extension }} [${{ matrix.combo.sapi }}] (PHP ${{ matrix.combo.php-version }} - ${{ matrix.combo.os }}-${{ matrix.combo.arch }})"
needs: gen-matrix needs: gen-matrix
runs-on: ${{ matrix.combo.runner }} runs-on: ${{ matrix.combo.runner }}
timeout-minutes: 120 timeout-minutes: 120
@@ -266,5 +266,5 @@ jobs:
if: always() && hashFiles('log/**') != '' if: always() && hashFiles('log/**') != ''
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: logs-${{ matrix.combo.os }}-${{ matrix.combo.arch }}-${{ matrix.combo.extension }}-php${{ matrix.combo.php-version }} name: logs-${{ matrix.combo.os }}-${{ matrix.combo.arch }}-${{ matrix.combo.sapi }}-${{ matrix.combo.extension }}-php${{ matrix.combo.php-version }}
path: log path: log

View File

@@ -27,9 +27,14 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: actions/setup-node@v3 - name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
- run: npm install - name: Install documentation dependencies
run: yarn install --frozen-lockfile
- name: "Install PHP for official runners" - name: "Install PHP for official runners"
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
@@ -57,7 +62,7 @@ jobs:
run: "composer install --no-interaction --no-progress" run: "composer install --no-interaction --no-progress"
- name: Build - name: Build
run: npm run docs:build run: yarn docs:build
# Deploy to GitHub Pages only when the workflow is triggered by a push to the v3 branch # Deploy to GitHub Pages only when the workflow is triggered by a push to the v3 branch
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages

1
.gitignore vendored
View File

@@ -52,6 +52,7 @@ packlib_files.txt
/node_modules/ /node_modules/
/docs/.vitepress/dist/ /docs/.vitepress/dist/
/docs/.vitepress/cache/ /docs/.vitepress/cache/
/docs/.vitepress/.temp/
/docs/.vitepress/ext-data.json /docs/.vitepress/ext-data.json
/docs/.vitepress/deps-data.json /docs/.vitepress/deps-data.json
package-lock.json package-lock.json

View File

@@ -10,6 +10,7 @@ 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-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. - `.github/skills/staticphp-build-troubleshooting`: diagnose build, download, doctor, shell, terminal, CI, smoke-test, or log failures.
- `.github/skills/staticphp-documentation-sync`: synchronize bilingual documentation (`docs/en/` and `docs/zh/`) when features, commands, configuration, APIs, or behavior change.
If your agent does not automatically discover skills from `.github/skills`, read the matching `SKILL.md` manually before working on that task. If your agent does not automatically discover skills from `.github/skills`, read the matching `SKILL.md` manually before working on that task.

View File

@@ -20,7 +20,7 @@
] ]
}, },
{ {
"action": "bin/spc dev:lint-config --check", "action": "php bin/spc dev:lint-config --check",
"conditions": [ "conditions": [
{ {
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\InDirectory", "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\InDirectory",

View File

@@ -13,4 +13,5 @@ ext-imagick:
os: os:
- Linux - Linux
- Darwin - Darwin
- Windows
arg-type: custom arg-type: custom

View File

@@ -16,13 +16,59 @@ imagemagick:
- libtiff - libtiff
- libheif - libheif
- bzip2 - bzip2
depends@windows:
- zlib
suggests: suggests:
- zstd - zstd
- xz - xz
- libzip - libzip
- libxml2 - libxml2
headers@windows:
- imagemagick/MagickWand/MagickWand.h
lang: cpp lang: cpp
pkg-configs: pkg-configs:
- Magick++-7.Q16HDRI - Magick++-7.Q16HDRI
- MagickCore-7.Q16HDRI - MagickCore-7.Q16HDRI
- MagickWand-7.Q16HDRI - MagickWand-7.Q16HDRI
static-libs@windows:
- CORE_RL_MagickWand_.lib
- CORE_RL_MagickCore_.lib
- CORE_RL_coders_.lib
- CORE_RL_filters_.lib
- CORE_RL_aom_.lib
- CORE_RL_brotli_.lib
- CORE_RL_bzip2_.lib
- CORE_RL_cairo_.lib
- CORE_RL_croco_.lib
- CORE_RL_de265_.lib
- CORE_RL_exr_.lib
- CORE_RL_ffi_.lib
- CORE_RL_freetype_.lib
- CORE_RL_fribidi_.lib
- CORE_RL_gdk-pixbuf_.lib
- CORE_RL_glib_.lib
- CORE_RL_harfbuzz_.lib
- CORE_RL_heif_.lib
- CORE_RL_highway_.lib
- CORE_RL_imath_.lib
- CORE_RL_jpeg-turbo-12_.lib
- CORE_RL_jpeg-turbo-16_.lib
- CORE_RL_jpeg-turbo_.lib
- CORE_RL_jpeg-xl_.lib
- CORE_RL_lcms_.lib
- CORE_RL_lqr_.lib
- CORE_RL_lzma_.lib
- CORE_RL_openh264_.lib
- CORE_RL_openjpeg_.lib
- CORE_RL_openjph_.lib
- CORE_RL_pango_.lib
- CORE_RL_pixman_.lib
- CORE_RL_png_.lib
- CORE_RL_raqm_.lib
- CORE_RL_raw_.lib
- CORE_RL_rsvg_.lib
- CORE_RL_tiff_.lib
- CORE_RL_webp_.lib
- CORE_RL_xml_.lib
- CORE_RL_zip_.lib
- CORE_RL_zlib_.lib

View File

@@ -9,5 +9,4 @@ libiconv-win:
license-files: [source/COPYING] license-files: [source/COPYING]
license: GPL-3.0-or-later license: GPL-3.0-or-later
static-libs@windows: static-libs@windows:
- libiconv.lib
- libiconv_a.lib - libiconv_a.lib

View File

@@ -11,6 +11,10 @@ libzip:
license: BSD-3-Clause license: BSD-3-Clause
depends: depends:
- zlib - zlib
depends@windows:
- zlib
- bzip2
- xz
suggests: suggests:
- bzip2 - bzip2
- xz - xz

View File

@@ -27,4 +27,5 @@ openssl:
- libcrypto.lib - libcrypto.lib
tools@windows: tools@windows:
- jom - jom
- nasm
- strawberry-perl - strawberry-perl

View File

@@ -5,8 +5,6 @@ postgresql:
type: ghtagtar type: ghtagtar
repo: postgres/postgres repo: postgres/postgres
match: REL_18_\d+ match: REL_18_\d+
binary:
windows-x86_64: { type: url, url: 'https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip', extract: { lib/libpq.lib: '{build_root_path}/lib/libpq.lib', lib/libpgport.lib: '{build_root_path}/lib/libpgport.lib', lib/libpgcommon.lib: '{build_root_path}/lib/libpgcommon.lib', include/libpq-fe.h: '{build_root_path}/include/libpq-fe.h', include/postgres_ext.h: '{build_root_path}/include/postgres_ext.h', include/pg_config_ext.h: '{build_root_path}/include/pg_config_ext.h', include/libpq/libpq-fs.h: '{build_root_path}/include/libpq/libpq-fs.h' } }
metadata: metadata:
license-files: ['@/postgresql.txt'] license-files: ['@/postgresql.txt']
license: PostgreSQL license: PostgreSQL
@@ -16,6 +14,9 @@ postgresql:
- openssl - openssl
- zlib - zlib
- libedit - libedit
depends@windows:
- openssl
- zlib
suggests@unix: suggests@unix:
- icu - icu
- libxslt - libxslt
@@ -27,3 +28,9 @@ postgresql:
- libpq.lib - libpq.lib
- libpgport.lib - libpgport.lib
- libpgcommon.lib - libpgcommon.lib
tools@windows:
- python-win
- meson
- ninja
- winflexbison
- strawberry-perl

View File

@@ -5,7 +5,7 @@ go-win:
env: env:
GOROOT: '{pkg_root_path}/go-win' GOROOT: '{pkg_root_path}/go-win'
GOBIN: '{pkg_root_path}/go-win/bin' GOBIN: '{pkg_root_path}/go-win/bin'
GOPATH: '{pkg_root_path}/go-win/go' GOPATH: '{pkg_root_path}/go-win/gopath'
path@windows: path@windows:
- '{pkg_root_path}/go-win/bin' - '{pkg_root_path}/go-win/bin'
tool: tool:

11
config/pkg/tool/meson.yml Normal file
View File

@@ -0,0 +1,11 @@
meson:
type: tool
artifact:
binary:
windows-x86_64: { type: url, url: 'https://github.com/mesonbuild/meson/releases/download/1.11.1/meson-1.11.1-py3-none-any.whl', extract: '{pkg_root_path}/meson/meson-1.11.1-py3-none-any.whl' }
path@windows:
- '{pkg_root_path}\meson'
tool:
provides:
- meson.bat
binary-subdir: meson

View File

@@ -0,0 +1,9 @@
ninja:
type: tool
artifact:
binary:
windows-x86_64: { type: url, url: 'https://github.com/ninja-build/ninja/releases/download/v1.13.2/ninja-win.zip', extract: { ninja.exe: '{pkg_root_path}/bin/ninja.exe' } }
tool:
provides:
- ninja.exe
binary-subdir: bin

View File

@@ -0,0 +1,8 @@
python-win:
type: tool
artifact:
binary: custom
tool:
provides:
- python.exe
binary-subdir: python-win/tools

View File

@@ -0,0 +1,12 @@
winflexbison:
type: tool
artifact:
binary:
windows-x86_64: { type: url, url: 'https://github.com/lexxmark/winflexbison/releases/download/v2.5.25/win_flex_bison-2.5.25.zip', extract: '{pkg_root_path}/winflexbison' }
path@windows:
- '{pkg_root_path}\winflexbison'
tool:
provides:
- win_flex.exe
- win_bison.exe
binary-subdir: winflexbison

View File

@@ -45,7 +45,7 @@
<label :for="'build_' + item">{{ item }}</label> <label :for="'build_' + item">{{ item }}</label>
</div> </div>
</div> </div>
<div v-if="selectedSystem === 'windows' && (checkedTargets.includes('fpm') || checkedTargets.includes('embed') || checkedTargets.includes('frankenphp'))" class="warning custom-block"> <div v-if="selectedSystem === 'windows' && checkedTargets.includes('fpm')" class="warning custom-block">
<p class="custom-block-title">WARNING</p> <p class="custom-block-title">WARNING</p>
<p>{{ I18N[lang].windowsSAPIUnavailable }}</p> <p>{{ I18N[lang].windowsSAPIUnavailable }}</p>
</div> </div>
@@ -157,7 +157,7 @@
<div class="warning custom-block"> <div class="warning custom-block">
<p class="custom-block-title">WARNING</p> <p class="custom-block-title">WARNING</p>
<p>{{ I18N[lang].windowsDownSPCWarning }}</p> <p>{{ I18N[lang].windowsDownSPCWarning }}</p>
<a href="https://dl.static-php.dev/v3/spc-bin/latest/spc-windows-x86_64.exe" target="_blank">https://dl.static-php.dev/v3/spc-bin/latest/spc-windows-x86_64.exe</a> <a href="https://dl.static-php.dev/v3/spc-bin/nightly/spc-windows-x86_64.exe" target="_blank">https://dl.static-php.dev/v3/spc-bin/nightly/spc-windows-x86_64.exe</a>
</div> </div>
</div> </div>
</div> </div>
@@ -278,7 +278,7 @@ const spcCommand = computed(() => {
const spcDownloadCommand = computed(() => { const spcDownloadCommand = computed(() => {
const os = selectedSystem.value === 'macos' ? 'macos' : 'linux'; const os = selectedSystem.value === 'macos' ? 'macos' : 'linux';
const arch = selectedArch.value; const arch = selectedArch.value;
return `curl -#fSL https://dl.static-php.dev/v3/spc-bin/latest/spc-${os}-${arch} -o spc && chmod +x spc`; return `curl -#fSL https://dl.static-php.dev/v3/spc-bin/nightly/spc-${os}-${arch} -o spc && chmod +x spc`;
}); });
const doctorCommandString = computed(() => `${spcCommand.value} doctor --auto-fix`); const doctorCommandString = computed(() => `${spcCommand.value} doctor --auto-fix`);
@@ -425,7 +425,7 @@ const I18N: Record<string, Record<string, string>> = {
downloadPhpVersion: '下载 PHP 版本', downloadPhpVersion: '下载 PHP 版本',
downloadSPCBinaryCommand: '下载 spc 二进制命令', downloadSPCBinaryCommand: '下载 spc 二进制命令',
selectedSystem: '选择操作系统', selectedSystem: '选择操作系统',
windowsSAPIUnavailable: 'Windows 目前不支持 fpm、embed、frankenphp 构建!', windowsSAPIUnavailable: 'Windows 目前不支持 fpm 构建!',
useUPX: '是否开启 UPX 压缩(减小二进制体积)', useUPX: '是否开启 UPX 压缩(减小二进制体积)',
windowsDownSPCWarning: 'Windows 下请手动下载 spc.exe 二进制文件!', windowsDownSPCWarning: 'Windows 下请手动下载 spc.exe 二进制文件!',
usePreBuilt: '如果可能,使用预编译的依赖库(减少编译时间)', usePreBuilt: '如果可能,使用预编译的依赖库(减少编译时间)',
@@ -459,7 +459,7 @@ const I18N: Record<string, Record<string, string>> = {
downloadPhpVersion: 'PHP version', downloadPhpVersion: 'PHP version',
downloadSPCBinaryCommand: 'Download spc binary', downloadSPCBinaryCommand: 'Download spc binary',
selectedSystem: 'Select OS', selectedSystem: 'Select OS',
windowsSAPIUnavailable: 'Windows does not support fpm, embed and frankenphp build!', windowsSAPIUnavailable: 'Windows does not support fpm build!',
useUPX: 'Enable UPX compression (reduce binary size)', useUPX: 'Enable UPX compression (reduce binary size)',
windowsDownSPCWarning: 'Please download the spc.exe binary manually on Windows!', windowsDownSPCWarning: 'Please download the spc.exe binary manually on Windows!',
usePreBuilt: 'Use pre-built dependencies where available (reduce compile time)', usePreBuilt: 'Use pre-built dependencies where available (reduce compile time)',

View File

@@ -4,14 +4,15 @@
A Package is the core concept in StaticPHP's build system, representing a buildable/installable unit such as a PHP extension, library, or build target. A Package is the core concept in StaticPHP's build system, representing a buildable/installable unit such as a PHP extension, library, or build target.
Each Package contains build information, dependencies, and build logic, forming StaticPHP's build model. Package definitions are primarily implemented through YAML/JSON configuration files. The package configuration files for the `core` registry are located in the `config/pkg/` directory, and the corresponding build classes are in the `src/Package/` directory. Each Package contains build information, dependencies, and build logic, forming StaticPHP's build model. Package definitions are primarily implemented through YAML/JSON configuration files. The package configuration files for the `core` registry are located in the `config/pkg/` directory, and optional recipe classes are in the corresponding subdirectories of `src/Package/`.
Packages are primarily divided into four types: Packages are divided into five types:
- **php-extension**: A PHP extension package containing build information and logic for a PHP extension. - **php-extension**: A PHP extension package containing build information and logic for a PHP extension.
- **library**: A library package containing build information and logic for build tools, dependency libraries, etc. - **library**: A dependency library package, usually installed into `buildroot/` for other packages to compile and link against.
- **target**: A build target package representing the final build artifact, such as a PHP binary or curl binary. Inherits from the `library` package type. - **target**: A build target package representing the final build artifact, such as a PHP binary or curl binary. Inherits from the `library` package type.
- **virtual-target**: A virtual build target package representing an abstract build target that doesn't directly correspond to a build artifact, primarily used for dependency management and build scheduling. - **virtual-target**: A virtual build target package representing an abstract build target that doesn't directly correspond to a build artifact, primarily used for dependency management and build scheduling.
- **tool**: A host-side build tool package, installed separately under `pkgroot/` and not treated as a link-time library dependency.
```yaml ```yaml
{pkg-name}: {pkg-name}:
@@ -21,7 +22,7 @@ Packages are primarily divided into four types:
## Artifact Definition ## Artifact Definition
An Artifact is a definition independent of Packages. It contains the source archive file or pre-built binary for building packages. Each Artifact defines download URLs, extraction methods, and build artifact file paths. Packages can reference one or more Artifacts via the `artifact` field to obtain the source or binaries needed for building. An Artifact is a definition independent of Packages. It contains the source archive file or pre-built binary for building packages. Each Artifact defines download URLs, extraction methods, and build artifact file paths. A Package can reference one Artifact via the `artifact` field to obtain the source or binary needed for building.
In simple terms, by default one Package corresponds to one Artifact; if multiple Packages share the same source, you can define a single Artifact for multiple Packages to reference. Artifact definitions are located in the `config/artifact/` directory, and the corresponding custom download/extract logic classes are in the `src/Package/Artifact/` directory. For special package types like virtual targets and PHP built-in extensions, a Package may also omit the Artifact field entirely. In simple terms, by default one Package corresponds to one Artifact; if multiple Packages share the same source, you can define a single Artifact for multiple Packages to reference. Artifact definitions are located in the `config/artifact/` directory, and the corresponding custom download/extract logic classes are in the `src/Package/Artifact/` directory. For special package types like virtual targets and PHP built-in extensions, a Package may also omit the Artifact field entirely.
@@ -40,7 +41,7 @@ For more on Artifact definitions, see the [Artifact Model](./artifact-model) cha
## php-extension Package Type ## php-extension Package Type
A php-extension package represents a PHP extension. Its configuration file is located in the `config/pkg/ext/` directory, and its build class inherits from `PhpExtensionPackage` in the `src/Package/Extension/` directory. PHP extension package configurations include extension name, version, dependencies, build options, and more. A php-extension package represents a PHP extension. Its configuration file is located in the `config/pkg/ext/` directory. Optional recipe classes are normally placed in `src/Package/Extension/`, registered with `#[Extension]`, and receive the corresponding `PhpExtensionPackage` through callback context. Inheriting from `PhpExtensionPackage` is supported but is not required by the current core recipes.
```yaml ```yaml
ext-lz4: ext-lz4:
@@ -57,7 +58,7 @@ ext-lz4:
depends: depends:
- liblz4 - liblz4
php-extension: php-extension:
arg-type@unix: '--enable-lz4=@shared_suffix@ --with-lz4-includedir=@build_root_path@' arg-type@unix: '--enable-lz4@shared_suffix@ --with-lz4-includedir=@build_root_path@'
arg-type@windows: '--enable-lz4' arg-type@windows: '--enable-lz4'
``` ```
@@ -72,16 +73,18 @@ ext-{ext-name}: # Package name must start with ext- prefix
lang: c # Optional, implementation language of the extension (c / c++ etc.) lang: c # Optional, implementation language of the extension (c / c++ etc.)
frameworks: [] # Optional, list of related macOS framework dependencies frameworks: [] # Optional, list of related macOS framework dependencies
artifact: '{artifact-name}' # Optional; when a string, references an Artifact definition artifact: '{artifact-name}' # Optional; when a string, references the named Artifact definition;
# with the same name; when an object, is an inline Artifact # when an object, it is an inline Artifact
# (built-in extensions don't need this field) # (built-in extensions don't need this field)
# depends / suggests support @windows / @unix / @linux / @macos suffixes # depends / suggests / tools support @windows / @unix / @linux / @macos suffixes
depends: [] # Optional, hard dependency list (library names as-is, PHP extensions need ext- prefix) depends: [] # Optional, hard dependency list (library names as-is, PHP extensions need ext- prefix)
depends@unix: [] # Optional, hard dependencies only effective on Unix platforms depends@unix: [] # Optional, hard dependencies only effective on Unix platforms
depends@windows: [] # Optional, hard dependencies only effective on Windows platforms depends@windows: [] # Optional, hard dependencies only effective on Windows platforms
suggests: [] # Optional, optional dependency list (same format as depends) suggests: [] # Optional, optional dependency list (same format as depends)
suggests@unix: [] suggests@unix: []
tools: [] # Optional, host-side build tool dependencies; resolved separately
tools@windows: [] # Optional, platform-specific tool dependencies
# ── php-extension Specific Fields (nested under php-extension: object) ──── # ── php-extension Specific Fields (nested under php-extension: object) ────
php-extension: php-extension:
@@ -100,7 +103,7 @@ ext-{ext-name}: # Package name must start with ext- prefix
# @shared_path_suffix@ → Expands to =shared,{buildroot} in shared builds, # @shared_path_suffix@ → Expands to =shared,{buildroot} in shared builds,
# expands to ={buildroot} in static builds # expands to ={buildroot} in static builds
arg-type: enable arg-type: enable
arg-type@unix: '--enable-{extname}=@shared_suffix@' arg-type@unix: '--enable-my-extension@shared_suffix@'
arg-type@windows: with-path arg-type@windows: with-path
zend-extension: false # Optional, true indicates this is a Zend extension (e.g., opcache, xdebug) zend-extension: false # Optional, true indicates this is a Zend extension (e.g., opcache, xdebug)
@@ -121,7 +124,7 @@ ext-{ext-name}: # Package name must start with ext- prefix
## library Package Type ## library Package Type
A library package represents a dependency library that needs to be compiled from source (such as openssl, zlib, etc.). Its configuration file is located in the `config/pkg/lib/` directory, and its build class inherits from `LibraryPackage` in the `src/Package/Library/` directory. A library package represents a dependency library, such as openssl or zlib, installed from source or a pre-built binary. Its configuration file is located in the `config/pkg/lib/` directory. Optional recipe classes are normally placed in `src/Package/Library/`, registered with `#[Library]`, and receive a `LibraryPackage` through callback context; they do not need to inherit from it.
Taking openssl as an example: Taking openssl as an example:
@@ -161,22 +164,27 @@ Allowed fields for `library`:
# ── Common Fields ───────────────────────────────────────────────────────── # ── Common Fields ─────────────────────────────────────────────────────────
description: '..' # Optional, human-readable package description description: '..' # Optional, human-readable package description
license: MIT # Optional, SPDX license identifier (for license export) license: # Optional, license material copied after a source build
type: file # type is file or text; a list of entries is also accepted
path: LICENSE
lang: c # Optional, implementation language of the library (c / c++ etc.) lang: c # Optional, implementation language of the library (c / c++ etc.)
frameworks: [] # Optional, list of related framework tags frameworks: [] # Optional, list of related framework tags
artifact: '{artifact-name}' # Required; when a string, references an Artifact definition artifact: '{artifact-name}' # Required; when a string, references the named Artifact definition;
# with the same name; when an object, is an inline Artifact # when an object, it is an inline Artifact
# depends / suggests support @windows / @unix / @linux / @macos suffixes # depends / suggests / tools support @windows / @unix / @linux / @macos suffixes
depends: [] # Optional, hard dependency list (library names or PHP extension names with ext- prefix) depends: [] # Optional, hard dependency list (library names or PHP extension names with ext- prefix)
depends@unix: [] depends@unix: []
depends@windows: [] depends@windows: []
suggests: [] # Optional, optional dependency list (same format as depends) suggests: [] # Optional, optional dependency list (same format as depends)
tools: [] # Optional, ToolPackage names required only while building
tools@windows: []
# ── library / target Specific Fields ─────────────────────────────────────── # ── library / target Specific Fields ───────────────────────────────────────
# The following fields are used to verify that artifacts have been correctly # The following fields are used to verify that artifacts have been correctly
# installed after the build. They support @unix / @windows / @linux / @macos suffixes. # installed after the build. headers, static-libs, and static-bins support
# @unix / @windows / @linux / @macos suffixes.
# Verify that specified header files or directories exist under buildroot/include/ # Verify that specified header files or directories exist under buildroot/include/
# Relative paths are based on buildroot/include/, absolute paths are used directly # Relative paths are based on buildroot/include/, absolute paths are used directly
@@ -231,17 +239,49 @@ The following path placeholders are supported in string values of the `path`, `e
| `{source_path}` | Extracted source directory (`source/`) | | `{source_path}` | Extracted source directory (`source/`) |
| `{spc_msys2_path}` | MSYS2 root directory (`msys64/`) — Windows only | | `{spc_msys2_path}` | MSYS2 root directory (`msys64/`) — Windows only |
## tool Package Type
A `tool` package represents an executable needed while building another package, rather than a library linked into the final target. Tool packages may use a pre-built binary or build from source, and install under `pkgroot/` by default, using either a shared `bin/` directory or a configured subdirectory. Packages request them through the top-level `tools` field; this dependency set is resolved independently of `depends` and `suggests`.
```yaml
nasm:
type: tool
artifact:
binary:
windows-x86_64:
type: url
url: 'https://example.com/nasm-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
min-version: '2.16'
```
The nested `tool` object supports:
| Field | Required | Meaning |
|---|---|---|
| `provides` | Yes | Executable filenames used to decide whether the tool is installed |
| `binary-subdir` | No | Directory below `install-root` containing the executables; defaults to the install root |
| `install-root` | No | Installation root; defaults to `PKG_ROOT_PATH` and supports path placeholders |
| `min-version` | No | Declared minimum version metadata exposed by `ToolPackage`; the installer does not currently enforce it |
Fields inside `tool` may use `@windows`, `@unix`, `@linux`, and `@macos` suffixes. For example, `provides@windows` can list `.exe` names while `provides@unix` lists Unix names.
## target Package Type ## target Package Type
A `target` package represents a final build artifact. It inherits from `library`, so it includes all definition fields of `library`. The configuration file for `target` packages is located in the `config/pkg/target/` directory, and its build class inherits from `TargetPackage` in the `src/Package/Target/` directory. A `target` package represents a final build artifact. It inherits from `library`, so it includes all definition fields of `library`. Its configuration file is located in `config/pkg/target/`. Optional recipe classes are normally placed in `src/Package/Target/`, registered with `#[Target]`, and receive a `TargetPackage`; inheriting from `TargetPackage` is optional.
The only difference from `library` is that a `target` package can be registered as a build target and automatically registers the build command `spc build:{target-name}`. The only difference from `library` is that a `target` package can be registered as a build target and automatically registers the build command `spc build:{target-name}`.
## virtual-target Package Type ## virtual-target Package Type
Unlike `target`, a `virtual-target` may not include an `artifact`, meaning it doesn't directly correspond to a buildable entity but is instead an abstract build target, primarily used for dependency management and build scheduling. The configuration file for `virtual-target` is located in the `config/pkg/target/` directory, and its build class inherits from `TargetPackage` in the `src/Package/Target/` directory. Its definition is essentially the same as `target`, but the `artifact` field is optional and typically not set. `virtual-target` is primarily used in the following scenarios: Unlike `target`, a `virtual-target` may omit `artifact`, meaning it doesn't directly correspond to a buildable entity but is instead an abstract build target, primarily used for dependency management and build scheduling. Its configuration and optional recipe classes use the same directories and `#[Target]` registration mechanism as `target`. Its definition is otherwise essentially the same, but the `artifact` field is optional and typically not set. `virtual-target` is primarily used in the following scenarios:
- Defining an abstract build target for other packages to depend on, without directly corresponding to a buildable entity. - Defining an abstract build target for other packages to depend on, without directly corresponding to a buildable entity.
- Serving as a common dependency for multiple `target` packages, simplifying dependency management. - Serving as a common dependency for multiple `target` packages, simplifying dependency management.
A typical example is the `php-cli`, `php-fpm` build targets for PHP. They have no independent source code and depend on `php-src`, with the final build outcome (CLI or FPM binary) determined through build scheduling. Typical examples are the `php-cli` and `php-fpm` build targets. They have no independent source and depend on the `php` target, whose Artifact is `php-src`; build scheduling determines whether the CLI or FPM binary is produced.

View File

@@ -1,20 +1,18 @@
# Package 模型 # Package 模型
<!-- TODO: 统一 Package 模型说明library / php-extension / target 类型。
config/pkg/ 下 per-package YAML 格式、depends 字段、平台覆盖(@windows / @unix 写法)。
artifact.source 和 artifact.binary 字段。附注释的 library 和 extension YAML 示例。 -->
## Package 定义 ## Package 定义
Package 是 StaticPHP 构建系统中的核心概念,代表一个可构建/可安装的单元,如 PHP 扩展、库、构建目标等。 Package 是 StaticPHP 构建系统中的核心概念,代表一个可构建/可安装的单元,如 PHP 扩展、库、构建目标等。
每个 Package 包含构建信息、依赖关系、构建逻辑等,构成了 StaticPHP 的构建模型。Package 的定义主要通过 YAML/JSON 配置文件来实现。`core` 注册表的包配置文件位于 `config/pkg/` 目录下,对应的构建类位于 `src/Package/` 目录 每个 Package 包含构建信息、依赖关系、构建逻辑等,构成了 StaticPHP 的构建模型。Package 的定义主要通过 YAML/JSON 配置文件来实现。`core` Registry 的 Package 配置位于 `config/pkg/` 目录下,可选 recipe class 位于 `src/Package/` 对应的子目录
Package 主要分为种类型: Package 分为种类型:
- **php-extension**PHP 扩展包,包含 PHP 扩展的构建信息和构建逻辑。 - **php-extension**PHP 扩展包,包含 PHP 扩展的构建信息和构建逻辑。
- **library**库包,包含构建工具链、依赖库等的构建信息和构建逻辑 - **library**依赖库 Package通常安装到 `buildroot/`,供其他 Package 编译和链接
- **target**:构建目标包,代表最终的构建产物,如 PHP 二进制、curl 二进制等,继承自 `library` 包类型。 - **target**:构建目标包,代表最终的构建产物,如 PHP 二进制、curl 二进制等,继承自 `library` 包类型。
- **virtual-target**:虚构建目标包,代表一个抽象的构建目标,不直接对应构建产物,主要用于依赖管理和构建调度。 - **virtual-target**:虚构建目标包,代表一个抽象的构建目标,不直接对应构建产物,主要用于依赖管理和构建调度。
- **tool**:宿主机侧构建工具 Package单独安装在 `pkgroot/` 下,不作为链接期库依赖处理。
```yaml ```yaml
{pkg-name}: {pkg-name}:
@@ -24,7 +22,7 @@ Package 主要分为四种类型:
## Artifact 定义 ## Artifact 定义
Artifact 是独立于 Package 的定义,它包含构建包的源码归档文件或预构建的二进制文件。每个 Artifact 定义了下载 URL、解压方式、构建产物的文件路径等信息。Package 可以通过 `artifact` 字段引用一个或多个 Artifact获取构建所需的源码或二进制文件。 Artifact 是独立于 Package 的定义,它包含构建包的源码归档文件或预构建的二进制文件。每个 Artifact 定义了下载 URL、解压方式、构建产物的文件路径等信息。一个 Package 可以通过 `artifact` 字段引用一个 Artifact,以获取构建所需的源码或二进制文件。
简单来说,默认情况下,一个 Package 对应一个 Artifact如果多个 Package 共用一份源码时,可以定义一个 Artifact 供多个 Package 引用。Artifact 的定义位于 `config/artifact/` 目录下,对应的自定义下载/解压逻辑类位于 `src/Package/Artifact/` 目录下对于虚拟目标、PHP 内置扩展等特殊包类型Package 也可以不设置 Artifact 字段。 简单来说,默认情况下,一个 Package 对应一个 Artifact如果多个 Package 共用一份源码时,可以定义一个 Artifact 供多个 Package 引用。Artifact 的定义位于 `config/artifact/` 目录下,对应的自定义下载/解压逻辑类位于 `src/Package/Artifact/` 目录下对于虚拟目标、PHP 内置扩展等特殊包类型Package 也可以不设置 Artifact 字段。
@@ -43,7 +41,7 @@ example-library-package:
## php-extension 包类型 ## php-extension 包类型
php-extension 一个包代表一个 PHP 扩展,它的配置文件位于 `config/pkg/ext/` 目录下,构建类继承自 `PhpExtensionPackage`,位于 `src/Package/Extension/` 目录下。PHP 扩展包的配置文件包含扩展名称、版本、依赖关系、构建选项等信息 php-extension Package 代表一个 PHP 扩展,它的配置文件位于 `config/pkg/ext/` 目录下。可选的 recipe class 通常放在 `src/Package/Extension/`,通过 `#[Extension]` 注册,并从 callback context 接收对应的 `PhpExtensionPackage`。当前 core recipe 支持继承 `PhpExtensionPackage`,但并不要求继承
```yaml ```yaml
ext-lz4: ext-lz4:
@@ -60,7 +58,7 @@ ext-lz4:
depends: depends:
- liblz4 - liblz4
php-extension: php-extension:
arg-type@unix: '--enable-lz4=@shared_suffix@ --with-lz4-includedir=@build_root_path@' arg-type@unix: '--enable-lz4@shared_suffix@ --with-lz4-includedir=@build_root_path@'
arg-type@windows: '--enable-lz4' arg-type@windows: '--enable-lz4'
``` ```
@@ -75,15 +73,17 @@ ext-{ext-name}: # 包名必须以 ext- 前缀开头
lang: c # 可选扩展的实现语言c / c++ 等) lang: c # 可选扩展的实现语言c / c++ 等)
frameworks: [] # 可选相关macOS框架依赖列表 frameworks: [] # 可选相关macOS框架依赖列表
artifact: '{artifact-name}' # 可选;字符串时引用同名 Artifact 定义, artifact: '{artifact-name}' # 可选;字符串时引用指定名称的 Artifact 定义,
# 对象时为内联 Artifact内置扩展无需此字段 # 对象时为内联 Artifact内置扩展无需此字段
# depends / suggests 支持 @windows / @unix / @linux / @macos 后缀 # depends / suggests / tools 支持 @windows / @unix / @linux / @macos 后缀
depends: [] # 可选硬依赖列表库名直接写PHP 扩展需加 ext- 前缀) depends: [] # 可选硬依赖列表库名直接写PHP 扩展需加 ext- 前缀)
depends@unix: [] # 可选,仅 Unix 平台生效的硬依赖 depends@unix: [] # 可选,仅 Unix 平台生效的硬依赖
depends@windows: [] # 可选,仅 Windows 平台生效的硬依赖 depends@windows: [] # 可选,仅 Windows 平台生效的硬依赖
suggests: [] # 可选,可选依赖列表(格式同 depends suggests: [] # 可选,可选依赖列表(格式同 depends
suggests@unix: [] suggests@unix: []
tools: [] # 可选,宿主机侧构建工具依赖,单独解析
tools@windows: [] # 可选,特定平台的工具依赖
# ── php-extension 专属字段(嵌套在 php-extension: 对象中)───────────────── # ── php-extension 专属字段(嵌套在 php-extension: 对象中)─────────────────
php-extension: php-extension:
@@ -101,7 +101,7 @@ ext-{ext-name}: # 包名必须以 ext- 前缀开头
# @shared_suffix@ → 共享构建时展开为 =shared静态构建时为空 # @shared_suffix@ → 共享构建时展开为 =shared静态构建时为空
# @shared_path_suffix@ → 共享构建时展开为 =shared,{buildroot},静态构建时为 ={buildroot} # @shared_path_suffix@ → 共享构建时展开为 =shared,{buildroot},静态构建时为 ={buildroot}
arg-type: enable arg-type: enable
arg-type@unix: '--enable-{extname}=@shared_suffix@' arg-type@unix: '--enable-my-extension@shared_suffix@'
arg-type@windows: with-path arg-type@windows: with-path
zend-extension: false # 可选true 表示这是 Zend 扩展(如 opcache、xdebug zend-extension: false # 可选true 表示这是 Zend 扩展(如 opcache、xdebug
@@ -120,7 +120,7 @@ ext-{ext-name}: # 包名必须以 ext- 前缀开头
## library 包类型 ## library 包类型
library 包代表一个需要从源码编译的依赖库(如 openssl、zlib 等),其配置文件位于 `config/pkg/lib/` 目录下,构建类继承自 `LibraryPackage`位于 `src/Package/Library/` 目录下 library Package 代表 openssl、zlib 等依赖库,可以从源码或预构建二进制安装。其配置文件位于 `config/pkg/lib/` 目录下。可选的 recipe class 通常放在 `src/Package/Library/`通过 `#[Library]` 注册,并从 callback context 接收 `LibraryPackage`;不要求继承该类
以 openssl 为例: 以 openssl 为例:
@@ -160,20 +160,25 @@ openssl:
# ── 通用字段 ───────────────────────────────────────────────────────────── # ── 通用字段 ─────────────────────────────────────────────────────────────
description: '...' # 可选,人类可读的包描述 description: '...' # 可选,人类可读的包描述
license: MIT # 可选,SPDX 许可证标识符(用于许可证导出) license: # 可选,从源码构建后复制的许可证内容
type: file # type 可为 file 或 text也可填写多个条目的列表
path: LICENSE
lang: c # 可选库的实现语言c / c++ 等) lang: c # 可选库的实现语言c / c++ 等)
frameworks: [] # 可选,相关框架标签列表 frameworks: [] # 可选,相关框架标签列表
artifact: '{artifact-name}' # 必填;字符串时引用同名 Artifact 定义,对象时为内联 Artifact artifact: '{artifact-name}' # 必填;字符串时引用指定名称的 Artifact 定义,对象时为内联 Artifact
# depends / suggests 支持 @windows / @unix / @linux / @macos 后缀 # depends / suggests / tools 支持 @windows / @unix / @linux / @macos 后缀
depends: [] # 可选,硬依赖列表(库名或 ext- 前缀的 PHP 扩展名) depends: [] # 可选,硬依赖列表(库名或 ext- 前缀的 PHP 扩展名)
depends@unix: [] depends@unix: []
depends@windows: [] depends@windows: []
suggests: [] # 可选,可选依赖列表(格式同 depends suggests: [] # 可选,可选依赖列表(格式同 depends
tools: [] # 可选,仅构建时需要的 ToolPackage 名称
tools@windows: []
# ── library / target 专属字段 ──────────────────────────────────────────── # ── library / target 专属字段 ────────────────────────────────────────────
# 以下字段用于构建完成后验证产物是否已正确安装,支持 @unix / @windows / @linux / @macos 后缀 # 以下字段用于构建完成后验证产物是否已正确安装headers、static-libs、static-bins
# 支持 @unix / @windows / @linux / @macos 后缀
# 验证 buildroot/include/ 下是否存在指定头文件或目录 # 验证 buildroot/include/ 下是否存在指定头文件或目录
# 相对路径基于 buildroot/include/,绝对路径直接使用 # 相对路径基于 buildroot/include/,绝对路径直接使用
@@ -225,17 +230,49 @@ openssl:
| `{source_path}` | 解压源码目录(`source/` | | `{source_path}` | 解压源码目录(`source/` |
| `{spc_msys2_path}` | MSYS2 根目录(`msys64/`)——仅 Windows | | `{spc_msys2_path}` | MSYS2 根目录(`msys64/`)——仅 Windows |
## tool 包类型
`tool` Package 表示构建其他 Package 时需要的可执行工具,而不是链接进最终 target 的库。Tool Package 可以使用预构建二进制,也可以从源码构建,默认安装在 `pkgroot/` 下的共享 `bin/` 或配置的子目录。其他 Package 通过顶层 `tools` 字段请求这些工具;该依赖集合与 `depends``suggests` 分开解析。
```yaml
nasm:
type: tool
artifact:
binary:
windows-x86_64:
type: url
url: 'https://example.com/nasm-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
min-version: '2.16'
```
嵌套的 `tool` 对象支持:
| 字段 | 是否必填 | 含义 |
|---|---|---|
| `provides` | 是 | 用于判断工具是否已经安装的可执行文件名 |
| `binary-subdir` | 否 | `install-root` 下存放可执行文件的目录;默认就是安装根目录 |
| `install-root` | 否 | 安装根目录;默认为 `PKG_ROOT_PATH`,支持路径占位符 |
| `min-version` | 否 | 由 `ToolPackage` 暴露的最低版本元数据installer 当前不会强制校验 |
`tool` 内的字段可以使用 `@windows``@unix``@linux``@macos` 后缀。例如,`provides@windows` 可以列出 `.exe` 名称,`provides@unix` 列出 Unix 名称。
## target 包类型 ## target 包类型
`target` 代表一个最终的构建产物,它继承于 `library`,所以包含 `library` 的所有定义字段。`target` 包的配置文件位于 `config/pkg/target/` 目录下,构建类继承自 `TargetPackage`,位于 `src/Package/Target/` 目录下 `target` Package 代表一个最终的构建产物,它继承于 `library`,所以包含 `library` 的所有定义字段。配置文件位于 `config/pkg/target/`。可选的 recipe class 通常放在 `src/Package/Target/`,通过 `#[Target]` 注册,并接收 `TargetPackage`;是否继承 `TargetPackage` 是可选的
`library` 的唯一区别是,`target` 包可以注册成为构建目标,且自动注册构建命令 `spc build:{target-name}` `library` 的唯一区别是,`target` 包可以注册成为构建目标,且自动注册构建命令 `spc build:{target-name}`
## virtual-target 包类型 ## virtual-target 包类型
`target` 不同的是`virtual-target` 可以不包含 `artifact`,即不直接对应一个可构建的实体,而是一个抽象的构建目标,主要用于依赖管理和构建调度。`virtual-target` 的配置文件位于 `config/pkg/target/` 目录下,构建类继承自 `TargetPackage`,位于 `src/Package/Target/` 目录下。它的定义与 `target` 基本相同,但 `artifact` 字段可选且通常不设置。`virtual-target` 主要用于以下场景: `target` 不同,`virtual-target` 可以省略 `artifact`,即不直接对应一个可构建的实体,而是用于依赖管理和构建调度的抽象构建目标。它与 `target` 使用相同的配置目录、可选 recipe class 目录和 `#[Target]` 注册方式。其余定义基本相同,但 `artifact` 字段可选且通常不设置。`virtual-target` 主要用于以下场景:
- 定义一个抽象的构建目标,供其他包依赖,但不直接对应一个可构建的实体。 - 定义一个抽象的构建目标,供其他包依赖,但不直接对应一个可构建的实体。
- 作为多个 `target` 包的公共依赖,简化依赖关系管理。 - 作为多个 `target` 包的公共依赖,简化依赖关系管理。
典型例子 PHP 包的 `php-cli``php-fpm` 构建目标,他们没有独立源码,依赖 `php-src`,通过构建调度决定最终构建成 CLI 还是 FPM 二进制。 典型例子是 `php-cli``php-fpm` 构建目标。它们没有独立源码,依赖 Artifact 为 `php-src``php` target构建调度决定最终成 CLI 还是 FPM 二进制。

View File

@@ -2,12 +2,12 @@
"scripts": { "scripts": {
"docs:dev": "node docs/.vitepress/gen-meta.js && vitepress dev docs", "docs:dev": "node docs/.vitepress/gen-meta.js && vitepress dev docs",
"docs:gen-meta": "node docs/.vitepress/gen-meta.js", "docs:gen-meta": "node docs/.vitepress/gen-meta.js",
"docs:build": "npm run docs:gen-meta && vitepress build docs", "docs:build": "node docs/.vitepress/gen-meta.js && vitepress build docs",
"docs:preview": "vitepress preview docs" "docs:preview": "vitepress preview docs"
}, },
"devDependencies": { "devDependencies": {
"mermaid": "^11.0.0", "mermaid": "^11.0.0",
"vitepress": "^2.0.0-alpha.5", "vitepress": "2.0.0-alpha.18",
"vue": "^3.2.47" "vue": "^3.2.47"
} }
} }

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Package\Artifact;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Util\FileSystem;
class meson_win
{
/**
* Meson 1.11 dropped the Windows MSI, so the artifact is the wheel, installed offline
* into a private venv. The venv is not just packaging hygiene: pip generates a real
* meson.exe launcher there, and PostgreSQL needs one because its build re-invokes meson
* through find_program(), which rejects non-executables like the meson.pyz zipapp.
*/
#[AfterBinaryExtract('meson', ['windows-x86_64'])]
public function afterExtract(string $target_path): void
{
$dir = dirname($target_path);
$venv = "{$dir}\\venv";
// Prefer the python-win tool package (installed alongside via tools@windows),
// fall back to whatever Python the machine already has.
$candidates = ['"' . PKG_ROOT_PATH . '\python-win\tools\python.exe"', 'python', 'py -3'];
$python = null;
foreach ($candidates as $candidate) {
[$code] = cmd()->execWithResult("{$candidate} --version", false);
if ($code === 0) {
$python = $candidate;
break;
}
}
if ($python === null) {
throw new EnvironmentException('meson needs Python 3; the python-win tool package did not install and no system Python was found.');
}
if (is_dir($venv)) {
FileSystem::removeDir($venv);
}
cmd()->exec("{$python} -m venv \"{$venv}\"")
->exec("\"{$venv}\\Scripts\\python.exe\" -m pip install --no-index --no-deps \"{$target_path}\"");
FileSystem::writeFile("{$dir}\\meson.bat", "@echo off\r\n\"%~dp0venv\\Scripts\\meson.exe\" %*\r\n");
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Package\Artifact;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
use StaticPHP\Exception\DownloaderException;
/**
* Full CPython for Windows from the official python.org nuget package. It is a plain zip
* (python.exe plus stdlib under tools/, venv and ensurepip included), so it installs into
* the pkgroot without touching the system. Used by the meson tool package.
*/
class python_win
{
#[CustomBinary('python-win', [
'windows-x86_64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$version = $this->getLatestVersion($downloader->getRetry());
$url = "https://api.nuget.org/v3-flatcontainer/python/{$version}/python.{$version}.nupkg";
// .nupkg is a zip; name the cache file .zip so the extractor treats it as one
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "python-win-{$version}.zip";
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: '{pkg_root_path}/python-win', version: $version);
}
#[CustomBinaryCheckUpdate('python-win', ['windows-x86_64'])]
public function checkUpdateBinary(?string $old_version): CheckUpdateResult
{
$version = $this->getLatestVersion();
return new CheckUpdateResult(
old: $old_version,
new: $version,
needUpdate: $old_version === null || $version !== $old_version,
);
}
#[AfterBinaryExtract('python-win', ['windows-x86_64'])]
public function afterExtract(string $target_path): void
{
if (!file_exists("{$target_path}\\tools\\python.exe")) {
throw new DownloaderException("Python installation appears incomplete: python.exe not found at {$target_path}\\tools\\python.exe");
}
}
private function getLatestVersion(int $retries = 0): string
{
$index = default_shell()->executeCurl('https://api.nuget.org/v3-flatcontainer/python/index.json', retries: $retries);
$versions = $index ? (json_decode($index, true)['versions'] ?? []) : [];
$stable = array_filter($versions, fn ($v) => preg_match('/^\d+\.\d+\.\d+$/', $v));
if ($stable === []) {
throw new DownloaderException('Failed to get Python versions from the nuget index');
}
return end($stable);
}
}

View File

@@ -4,12 +4,17 @@ declare(strict_types=1);
namespace Package\Extension; namespace Package\Extension;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\CustomPhpConfigureArg;
use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Util\FileSystem;
#[Extension('imagick')] #[Extension('imagick')]
class imagick class imagick extends PhpExtensionPackage
{ {
#[CustomPhpConfigureArg('Darwin')] #[CustomPhpConfigureArg('Darwin')]
#[CustomPhpConfigureArg('Linux')] #[CustomPhpConfigureArg('Linux')]
@@ -18,4 +23,35 @@ class imagick
$disable_omp = ' ac_cv_func_omp_pause_resource_all=no'; $disable_omp = ' ac_cv_func_omp_pause_resource_all=no';
return '--with-imagick=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . $disable_omp; return '--with-imagick=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . $disable_omp;
} }
#[CustomPhpConfigureArg('Windows')]
public function getWindowsConfigureArg(bool $shared): string
{
// config.w32 uses PHP_IMAGICK as an extra search path for CORE_RL_*.lib; the static
// ImageMagick libs are installed flat in buildroot/lib (headers in buildroot/include/imagemagick).
return '--with-imagick=' . BUILD_LIB_PATH;
}
#[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-imagick')]
#[PatchDescription('Add the Win32 system libraries the static ImageMagick stack needs')]
public function patchConfigW32ForWindows(): void
{
$config = $this->getSourceDir() . '/config.w32';
// Idempotency guard (the source dir may be patched in place and reused across builds).
if (str_contains(FileSystem::readFile($config), 'LIBS_IMAGICK')) {
return;
}
// The static ImageMagick stack needs several Win32 system libraries (GDI/GDI+, WIC, urlmon, ...)
// that aren't already pulled in by the other extensions. (imagick itself builds as plain C:
// ImageMagick is built with a 32-bit channel mask, see imagemagick.php buildWin, so the
// MagickCore headers don't require a C++ translation unit.)
FileSystem::replaceFileStr(
$config,
"AC_DEFINE('HAVE_IMAGICK', 1);",
'ADD_FLAG("LIBS_IMAGICK", "gdi32.lib gdiplus.lib urlmon.lib msimg32.lib oleaut32.lib windowscodecs.lib iphlpapi.lib");' . "\n\t\t" .
"AC_DEFINE('HAVE_IMAGICK', 1);"
);
}
} }

View File

@@ -5,11 +5,15 @@ declare(strict_types=1);
namespace Package\Extension; namespace Package\Extension;
use Package\Target\php; use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\CustomPhpConfigureArg;
use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\SPCConfigUtil; use StaticPHP\Util\SPCConfigUtil;
#[Extension('pgsql')] #[Extension('pgsql')]
@@ -39,6 +43,27 @@ class pgsql extends PhpExtensionPackage
return "--with-pgsql={$builder->getBuildRootPath()}"; return "--with-pgsql={$builder->getBuildRootPath()}";
} }
#[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-pgsql')]
#[PatchDescription('Link the Win32 system libraries the static libpq needs')]
public function patchConfigW32ForWindows(TargetPackage $package): void
{
$config = "{$package->getSourceDir()}\\ext\\pgsql\\config.w32";
if (str_contains(FileSystem::readFile($config), 'LIBS_PGSQL')) {
return;
}
// libpq uses SSPI for Windows auth (secur32) and the static libcrypto behind it
// reads the system cert store (crypt32). Nothing else puts these on the link line
// when the openssl extension itself is not part of the build.
FileSystem::replaceFileStr(
$config,
'EXTENSION("pgsql", "pgsql.c", PHP_PGSQL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1");',
'EXTENSION("pgsql", "pgsql.c", PHP_PGSQL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1");' . "\n\t\t" .
'ADD_FLAG("LIBS_PGSQL", "secur32.lib crypt32.lib");'
);
}
public function getSharedExtensionEnv(): array public function getSharedExtensionEnv(): array
{ {
$parent = parent::getSharedExtensionEnv(); $parent = parent::getSharedExtensionEnv();

View File

@@ -11,6 +11,7 @@ use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription; use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\SourcePatcher;
#[Extension('xlswriter')] #[Extension('xlswriter')]
@@ -28,6 +29,17 @@ class xlswriter extends PhpExtensionPackage
return $arg; return $arg;
} }
#[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-xlswriter')]
#[PatchDescription('Define XML_STATIC so bundled Expat uses plain symbols instead of __declspec(dllimport) in static builds')]
public function patchConfigForStaticExpat(): void
{
FileSystem::replaceFileStr(
"{$this->getSourceDir()}/config.w32",
"' /D USE_SYSTEM_MINIZIP",
"' /D XML_STATIC /D USE_SYSTEM_MINIZIP"
);
}
#[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-xlswriter')] #[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-xlswriter')]
#[PatchDescription('Fix Windows build: apply win32 patch and add UTF-8 BOM to theme.c')] #[PatchDescription('Fix Windows build: apply win32 patch and add UTF-8 BOM to theme.c')]
public function patchBeforeMakeForWindows(): void public function patchBeforeMakeForWindows(): void

View File

@@ -6,15 +6,86 @@ namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\Library;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Runtime\SystemTarget; use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\FileSystem; use StaticPHP\Util\FileSystem;
use StaticPHP\Util\System\WindowsUtil;
#[Library('imagemagick')] #[Library('imagemagick')]
class imagemagick class imagemagick
{ {
/**
* Build a fully static, self-contained ImageMagick 7 (Q16-HDRI, /MT) on Windows using the
* official VisualMagick build (the ImageMagick/Windows + Configure + Dependencies repos), which
* bundles every delegate. ImageMagick has no autoconf/CMake build on Windows, so this clones the
* VisualMagick tree, generates a static x64 solution via the Configure tool, and builds it with
* msbuild. The resulting CORE_RL_*.lib static libraries + MagickWand/MagickCore headers are
* installed into the build root for ext-imagick to link.
*
* The VisualMagick tree lives under source/ so bin/spc reset cleans it up like everything
* else; override the location with SPC_IMAGEMAGICK_BUILD_DIR.
*/
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void
{
$work = getenv('SPC_IMAGEMAGICK_BUILD_DIR') ?: SOURCE_PATH . '\imagemagick-win';
$configure_release = '2026.05.30.2033';
$configure_url = "https://github.com/ImageMagick/Configure/releases/download/{$configure_release}/Configure.Release.x64.exe";
FileSystem::createDir($work);
// Clone the VisualMagick repos (ImageMagick source + Configure + Dependencies + all delegates).
if (!is_dir("{$work}\\ImageMagick")) {
cmd()->cd($work)->exec(SPC_GIT_EXEC . ' clone --depth 1 https://github.com/ImageMagick/Windows.git .');
cmd()->cd($work)->exec('bash clone-repositories.sh --imagemagick7');
}
// Use the prebuilt Configure tool (building it from source needs the MFC components).
default_shell()->executeCurlDownload($configure_url, "{$work}\\Configure\\Configure.Release.x64.exe", retries: 2);
// Generate a static, /MT (linkRuntime), x64, Q16-HDRI solution with the configs embedded
// (zeroConfigurationSupport) and OpenMP off (no vcomp runtime dependency).
$ver = WindowsUtil::findVisualStudio();
$vs_major = is_array($ver) ? $ver['major_version'] : 'unknown';
$vs_arg = match ($vs_major) {
'18',
'17' => '/VS2022',
'16' => '/VS2019',
default => throw new EnvironmentException("Current VS version {$vs_major} is not supported yet!"),
};
cmd()->cd("{$work}\\Configure")
->exec("Configure.Release.x64.exe /noWizard {$vs_arg} /x64 /static /linkRuntime /noOpenMP /zeroConfigurationSupport");
// x64 IM7 defaults to a 64-bit channel mask, whose magick-baseconfig.h #errors unless the
// consuming translation unit is C++. ext-imagick is plain C, so force a 32-bit channel mask
// (ample: 32 channels >> RGBA/CMYK) before building, keeping libs and the installed header in sync.
FileSystem::replaceFileStr(
"{$work}\\ImageMagick\\MagickCore\\magick-baseconfig.h",
'#define MAGICKCORE_CHANNEL_MASK_DEPTH 64',
'#define MAGICKCORE_CHANNEL_MASK_DEPTH 32'
);
cmd()->cd($work)
->exec('msbuild IM7.Static.x64.sln /m /t:Rebuild /nologo /p:Configuration=Release /p:Platform=x64');
$artifacts = "{$work}\\Artifacts\\lib";
if (!is_dir($artifacts)) {
throw new EnvironmentException('ImageMagick VisualMagick build produced no Artifacts/lib; build failed.');
}
// Install the static libs (flat, onto the build-root lib path) and the public headers.
FileSystem::createDir($lib->getLibDir());
foreach (glob("{$artifacts}\\CORE_RL_*.lib") as $f) {
FileSystem::copy($f, $lib->getLibDir() . '\\' . basename($f));
}
foreach (['MagickWand', 'MagickCore'] as $dir) {
FileSystem::createDir($lib->getIncludeDir() . "\\imagemagick\\{$dir}");
foreach (glob("{$work}\\ImageMagick\\{$dir}\\*.h") as $h) {
FileSystem::copy($h, $lib->getIncludeDir() . "\\imagemagick\\{$dir}\\" . basename($h));
}
}
}
#[BuildFor('Darwin')] #[BuildFor('Darwin')]
#[BuildFor('Linux')] #[BuildFor('Linux')]
public function buildUnix(LibraryPackage $lib, ToolchainInterface $toolchain): void public function buildUnix(LibraryPackage $lib, ToolchainInterface $toolchain): void

View File

@@ -6,43 +6,24 @@ namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\Package\Validate;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\EnvironmentException; use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageBuilder;
use StaticPHP\Runtime\SystemTarget; use StaticPHP\Package\ToolPackage;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\FileSystem; use StaticPHP\Util\FileSystem;
use StaticPHP\Util\System\LinuxUtil; use StaticPHP\Util\System\LinuxUtil;
use StaticPHP\Util\System\WindowsUtil;
#[Library('openssl')] #[Library('openssl')]
class openssl class openssl
{ {
#[Validate]
public function validate(): void
{
if (SystemTarget::getTargetOS() === 'Windows') {
global $argv;
$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(
'You need to install perl first!',
"Please run \"{$argv[0]} doctor\" to fix the environment.",
);
}
ApplicationContext::set('perl', $perl);
}
}
#[BuildFor('Windows')] #[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib, PackageBuilder $builder): void public function buildWin(LibraryPackage $lib, PackageBuilder $builder): void
{ {
$perl = ApplicationContext::get('perl'); $perl = $this->resolveWindowsPerl();
$cmd = cmd()->cd($lib->getSourceDir()) $cmd = cmd()->cd($lib->getSourceDir())
->exec( ->exec(
"{$perl} Configure zlib VC-WIN64A " . escapeshellarg($perl) . ' Configure zlib VC-WIN64A ' .
'no-shared ' . 'no-shared ' .
'--prefix=' . quote($lib->getBuildRootPath()) . ' ' . '--prefix=' . quote($lib->getBuildRootPath()) . ' ' .
'--with-zlib-lib=' . quote($lib->getLibDir()) . ' ' . '--with-zlib-lib=' . quote($lib->getLibDir()) . ' ' .
@@ -145,4 +126,22 @@ class openssl
FileSystem::replaceFileRegex("{$pkg->getLibDir()}/pkgconfig/libcrypto.pc", '/Libs.private:.*/m', 'Requires.private: zlib'); FileSystem::replaceFileRegex("{$pkg->getLibDir()}/pkgconfig/libcrypto.pc", '/Libs.private:.*/m', 'Requires.private: zlib');
FileSystem::replaceFileRegex("{$pkg->getLibDir()}/cmake/OpenSSL/OpenSSLConfig.cmake", '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")'); FileSystem::replaceFileRegex("{$pkg->getLibDir()}/cmake/OpenSSL/OpenSSLConfig.cmake", '/set\(OPENSSL_LIBCRYPTO_DEPENDENCIES .*\)/m', 'set(OPENSSL_LIBCRYPTO_DEPENDENCIES "${OPENSSL_LIBRARY_DIR}/libz.a")');
} }
private function resolveWindowsPerl(): string
{
global $argv;
$tool = PackageLoader::getPackage('strawberry-perl');
if ($tool instanceof ToolPackage) {
$perl = $tool->getBinary('perl.exe');
if (file_exists($perl)) {
return $perl;
}
}
throw new EnvironmentException(
'OpenSSL requires strawberry-perl, but it is not installed.',
"Please run \"{$argv[0]} install-pkg strawberry-perl\" or retry the build with downloads enabled.",
);
}
} }

View File

@@ -37,6 +37,10 @@ class postgresql extends LibraryPackage
#[PatchDescription('Various patches before building PostgreSQL')] #[PatchDescription('Various patches before building PostgreSQL')]
public function patchBeforeBuild(): bool public function patchBeforeBuild(): bool
{ {
// These patches target the autoconf/Make build; the Windows build uses Meson (see buildWin).
if (SystemTarget::getTargetOS() === 'Windows') {
return true;
}
// skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries
FileSystem::replaceFileStr("{$this->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); FileSystem::replaceFileStr("{$this->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';');
// disable shared libs build // disable shared libs build
@@ -53,6 +57,73 @@ class postgresql extends LibraryPackage
return true; return true;
} }
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void
{
$src = $lib->getSourceDir();
$build_root = $lib->getBuildRootPath();
$lib_dir = $lib->getLibDir();
$inc_dir = $lib->getIncludeDir();
$build = "{$src}\\build";
// Export the public pg_char_to_encoding()/pg_encoding_to_char() from libpgcommon.a so a
// statically-linked libpq.a resolves them (PHP's ext/pgsql relies on them too). This mirrors
// the Unix build's -UUSE_PRIVATE_ENCODING_FUNCS patch, but for the Meson build.
FileSystem::replaceFileStr(
"{$src}\\src\\common\\meson.build",
"'c_args': ['-DUSE_PRIVATE_ENCODING_FUNCS'],",
"'c_args': [],"
);
// Fresh Meson build dir (Meson refuses to reuse a dir configured differently).
if (is_dir($build)) {
FileSystem::removeDir($build);
}
// Meson's OpenSSL detection link-tests CRYPTO_new_ex_data; our static libcrypto needs its
// Win32 deps (and zlib, since OpenSSL was built with zlib) on the link line to succeed.
$ld = 'ws2_32.lib gdi32.lib advapi32.lib crypt32.lib user32.lib secur32.lib zlibstatic.lib';
$configure = 'meson setup build'
. ' --prefix=' . escapeshellarg($build_root)
. ' -Ddefault_library=static' // static libpq.a / libpgcommon.a / libpgport.a
. ' -Db_vscrt=mt' // /MT static CRT, matching the rest of the build
. ' -Dssl=openssl'
// Everything libpq doesn't need: keeps deps minimal and avoids server-only detection.
. ' -Dzlib=disabled -Dnls=disabled -Dreadline=disabled -Dicu=disabled'
. ' -Dlz4=disabled -Dzstd=disabled -Dtap_tests=disabled'
. ' -Dplperl=disabled -Dplpython=disabled -Dpltcl=disabled'
. ' -Dgssapi=disabled -Dldap=disabled -Dlibxml=disabled -Dlibxslt=disabled'
. ' -Dextra_include_dirs=' . escapeshellarg("{$build_root}\\include")
. ' -Dextra_lib_dirs=' . escapeshellarg($lib_dir);
// Build only the three frontend static libs (not the server) — keeps it fast and avoids
// needing every backend dependency. meson/ninja/win_bison/win_flex/perl come from the
// tool packages declared in the package config (tools@windows).
$targets = 'src/interfaces/libpq/libpq.a src/common/libpgcommon.a src/port/libpgport.a';
cmd()->cd($src)
->setEnv([
'LIB' => $lib_dir . ';' . (getenv('LIB') ?: ''),
'LDFLAGS' => $ld,
])
->exec($configure)
->exec("ninja -C build {$targets}");
// Install the static libs under the names PHP's ext/pgsql + frankenphp expect (.lib).
FileSystem::createDir($lib_dir);
FileSystem::createDir($inc_dir);
FileSystem::copy("{$build}\\src\\interfaces\\libpq\\libpq.a", "{$lib_dir}\\libpq.lib");
FileSystem::copy("{$build}\\src\\common\\libpgcommon.a", "{$lib_dir}\\libpgcommon.lib");
FileSystem::copy("{$build}\\src\\port\\libpgport.a", "{$lib_dir}\\libpgport.lib");
// Install the public libpq headers (PG18 no longer ships pg_config_ext.h).
FileSystem::copy("{$src}\\src\\interfaces\\libpq\\libpq-fe.h", "{$inc_dir}\\libpq-fe.h");
FileSystem::copy("{$src}\\src\\include\\postgres_ext.h", "{$inc_dir}\\postgres_ext.h");
FileSystem::createDir("{$inc_dir}\\libpq");
FileSystem::copy("{$src}\\src\\include\\libpq\\libpq-fs.h", "{$inc_dir}\\libpq\\libpq-fs.h");
}
#[BuildFor('Darwin')] #[BuildFor('Darwin')]
#[BuildFor('Linux')] #[BuildFor('Linux')]
public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void

View File

@@ -35,12 +35,16 @@ class curl
#[BuildFor('Windows')] #[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void public function buildWin(LibraryPackage $lib): void
{ {
$lib_dir = str_replace('\\', '/', $lib->getLibDir());
$zstd_lib = "{$lib_dir}/zstd_static.lib";
$extra_libs = "{$lib_dir}/libcrypto.lib {$lib_dir}/libssl.lib ws2_32.lib gdi32.lib advapi32.lib crypt32.lib user32.lib";
WindowsCMakeExecutor::create($lib) WindowsCMakeExecutor::create($lib)
->optionalPackage('zstd', ...cmake_boolean_args('CURL_ZSTD')) ->optionalPackage('zstd', ...cmake_boolean_args('CURL_ZSTD'))
->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI')) ->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI'))
->addConfigureArgs( ->addConfigureArgs(
'-DBUILD_CURL_EXE=ON', '-DBUILD_CURL_EXE=ON',
'-DZSTD_LIBRARY=' . BUILD_LIB_PATH . '/zstd_static.lib', '-DCMAKE_C_STANDARD_LIBRARIES=' . escapeshellarg($extra_libs),
'-DZSTD_LIBRARY=' . escapeshellarg($zstd_lib),
'-DBUILD_TESTING=OFF', '-DBUILD_TESTING=OFF',
'-DBUILD_EXAMPLES=OFF', '-DBUILD_EXAMPLES=OFF',
'-DUSE_LIBIDN2=OFF', '-DUSE_LIBIDN2=OFF',

View File

@@ -328,6 +328,49 @@ class php extends TargetPackage
return $info; return $info;
} }
public function getBuildManifestData(PackageInstaller $installer): array
{
if ($this->getName() !== 'php') {
return [];
}
$all_extensions = [];
$static_extensions = [];
$shared_extensions = [];
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $extension) {
$extension_name = $extension->getExtensionName();
if ($extension->isBuildStatic() || $extension->isBuildShared()) {
$all_extensions[] = $extension_name;
}
if ($extension->isBuildStatic()) {
$static_extensions[] = $extension_name;
}
if ($extension->isBuildShared()) {
$shared_extensions[] = $extension_name;
}
}
$sapis = array_values(array_filter([
$installer->isPackageResolved('php-cli') ? 'cli' : null,
$installer->isPackageResolved('php-fpm') ? 'fpm' : null,
$installer->isPackageResolved('php-micro') ? 'micro' : null,
$installer->isPackageResolved('php-cgi') ? 'cgi' : null,
$installer->isPackageResolved('php-embed') ? 'embed' : null,
$installer->isPackageResolved('frankenphp') ? 'frankenphp' : null,
]));
return [
'version' => static::getPHPVersion(return_null_if_failed: true),
'thread-safety' => $this->getBuildOption('enable-zts', false) ? 'zts' : 'nts',
'sapis' => $sapis,
'extensions' => [
'all' => $all_extensions,
'static' => $static_extensions,
'shared' => $shared_extensions,
],
];
}
#[BeforeStage('php', 'build')] #[BeforeStage('php', 'build')]
public function beforeBuild(PackageBuilder $builder, Package $package): void public function beforeBuild(PackageBuilder $builder, Package $package): void
{ {

View File

@@ -26,6 +26,55 @@ use ZM\Logger\ConsoleColor;
trait frankenphp trait frankenphp
{ {
/**
* Source of the -mthreads stripping shim, see windowsCgoCompilers(). The %s
* placeholder receives the wrapped compiler path as a C string literal.
*/
private const string MTHREADS_SHIM_C = <<<'C'
// Generated by static-php-cli. Drops the MinGW-only -mthreads flag Go passes to
// cgo (golang/go#80290) and forwards everything else to clang.
#include <windows.h>
#include <wchar.h>
#include <stdlib.h>
static const wchar_t compiler[] = L"%s";
int main(void)
{
const wchar_t *args = GetCommandLineW();
// skip our own (possibly quoted) argv[0]
if (*args == L'"') {
args++;
while (*args && *args != L'"') args++;
if (*args) args++;
} else {
while (*args && *args != L' ' && *args != L'\t') args++;
}
// "<compiler>" + args + trailing space so a flag at the very end still matches
size_t len = wcslen(compiler) + wcslen(args) + 8;
wchar_t *cmd = calloc(len, sizeof(wchar_t));
if (cmd == NULL) return 1;
swprintf(cmd, len, L"\"%%ls\"%%ls ", compiler, args);
// remove every space-delimited -mthreads token
wchar_t *p;
while ((p = wcsstr(cmd, L" -mthreads ")) != NULL) {
wmemmove(p, p + 10, wcslen(p + 10) + 1);
}
STARTUPINFOW si = {sizeof(si)};
PROCESS_INFORMATION pi;
if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
return 127;
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
return (int) code;
}
C;
#[Stage] #[Stage]
public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void
{ {
@@ -233,10 +282,21 @@ trait frankenphp
$dep_libs = array_unique($dep_libs); $dep_libs = array_unique($dep_libs);
$lib_dir = str_replace('\\', '/', BUILD_LIB_PATH); $lib_dir = str_replace('\\', '/', BUILD_LIB_PATH);
$php_embed_lib = "-lphp{$major}embed"; $php_embed_lib = "-lphp{$major}embed";
$win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt'; // pathcch: PathCchCanonicalizeEx etc. used by frankenphp/caddy path handling.
// secur32: InitSecurityInterfaceA (curl Schannel/SSPI). crypt32/gdi32: OpenSSL + Schannel.
$win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt -lpathcch -lsecur32 -lcrypt32 -lgdi32';
$cgo_ldflags = clean_spaces(implode(' ', array_filter([ $cgo_ldflags = clean_spaces(implode(' ', array_filter([
"-L{$lib_dir}", "-L{$lib_dir}",
$php_embed_lib, $php_embed_lib,
// FrankenPHP's cgo code references PHP/lexbor/zend symbols via __declspec(dllimport).
// Their definitions live in php{N}embed.lib but are only pulled in if the plain symbol
// is referenced, so the __imp_ refs go unresolved. Force-include one symbol from each
// defining object (zend_atomic.obj, lexbor url.obj, lexbor idna.obj) to pull them in;
// lld then auto-imports the __imp_ refs. (/WHOLEARCHIVE would also drag in libxml2.res,
// which collides with Go's own resource object: "more than one resource obj file".)
'-Wl,/INCLUDE:zend_atomic_bool_store',
'-Wl,/INCLUDE:lxb_url_parse',
'-Wl,/INCLUDE:lxb_unicode_idna_init',
implode(' ', $dep_libs), implode(' ', $dep_libs),
$win_sys_libs, $win_sys_libs,
'-llibcmt', '-llibcmt',
@@ -262,10 +322,11 @@ trait frankenphp
// Fix: prepend clang's directory to PATH and use plain executable names instead, // Fix: prepend clang's directory to PATH and use plain executable names instead,
// which matches FrankenPHP's official CI approach (CC=clang, CXX=clang++). // which matches FrankenPHP's official CI approach (CC=clang, CXX=clang++).
$clang_dir = dirname($clang_info['clang']); $clang_dir = dirname($clang_info['clang']);
[$cc, $cxx] = $this->windowsCgoCompilers($package, $clang_info['clang']);
$env = [ $env = [
'CGO_ENABLED' => '1', 'CGO_ENABLED' => '1',
'CC' => 'clang.exe', 'CC' => $cc,
'CXX' => 'clang++.exe', 'CXX' => $cxx,
'PATH' => $clang_dir . ';' . getenv('PATH'), 'PATH' => $clang_dir . ';' . getenv('PATH'),
'CGO_CFLAGS' => clean_spaces($cgo_cflags), 'CGO_CFLAGS' => clean_spaces($cgo_cflags),
'CGO_LDFLAGS' => $cgo_ldflags, 'CGO_LDFLAGS' => $cgo_ldflags,
@@ -366,6 +427,48 @@ trait frankenphp
} }
} }
/**
* Return the [CC, CXX] cgo should use to build FrankenPHP on Windows.
*
* Go passes the MinGW-only `-mthreads` flag to the C compiler for cgo builds;
* Clang >= 20 rejects it for the MSVC target. When it does, wrap Clang with a
* small compiled shim that strips the flag before forwarding. An executable and
* not a .bat on purpose: batch files run through cmd.exe, which caps the command
* line at 8191 characters, and cgo link steps with big extension stacks (imagick)
* blow past that. A real process gets the full ~32K limit, and beyond that Go
* switches to a response file by itself.
*
* Workaround for https://github.com/golang/go/issues/80290, remove once Go
* stops passing the flag.
*
* @return array{0: string, 1: string}
*/
protected function windowsCgoCompilers(TargetPackage $package, string $clang): array
{
$probe = $package->getSourceDir() . '\mthreads-probe.c';
file_put_contents($probe, "int main(void){return 0;}\n");
[$ret] = cmd()->execWithResult('"' . $clang . '" -mthreads -c ' . escapeshellarg($probe) . ' -o ' . escapeshellarg($probe . '.o'), false);
FileSystem::removeFileIfExists($probe);
FileSystem::removeFileIfExists($probe . '.o');
if ($ret === 0) {
return ['clang.exe', 'clang++.exe'];
}
logger()->info('Clang rejects -mthreads; wrapping it to strip the flag (golang/go#80290)');
$dir = dirname($clang);
$wrappers = [];
foreach (['cc-nothreads' => 'clang.exe', 'cxx-nothreads' => 'clang++.exe'] as $name => $compiler) {
$src = $package->getSourceDir() . "\\{$name}.c";
$exe = $package->getSourceDir() . "\\{$name}.exe";
file_put_contents($src, sprintf(self::MTHREADS_SHIM_C, str_replace('\\', '\\\\', "{$dir}\\{$compiler}")));
cmd()->cd($package->getSourceDir())
->exec('"' . $clang . '" -O2 ' . escapeshellarg($src) . ' -o ' . escapeshellarg($exe));
$wrappers[] = $exe;
}
return $wrappers;
}
protected function getFrankenPHPVersion(TargetPackage $package): string protected function getFrankenPHPVersion(TargetPackage $package): string
{ {
if ($version = getenv('FRANKENPHP_VERSION')) { if ($version = getenv('FRANKENPHP_VERSION')) {

View File

@@ -59,7 +59,7 @@ trait unix
} }
if (self::getPHPVersionID() >= 80300 && self::getPHPVersionID() < 80400) { if (self::getPHPVersionID() >= 80300 && self::getPHPVersionID() < 80400) {
SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', $this->getSourceDir()); SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', $package->getSourceDir());
} }
} }
@@ -289,16 +289,7 @@ trait unix
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst);
// patch after UPX-ed micro.sfx (Linux only) // patch after UPX-ed micro.sfx (Linux only)
if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) {
// cut binary with readelf to remove UPX extra segment self::patchMicroUpxPackedBinary($dst);
[$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'");
$out[1] = explode(' ', $out[1]);
$offset = $out[1][0];
if ($ret !== 0 || !str_starts_with($offset, '0x')) {
throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output');
}
$offset = hexdec($offset);
// remove upx extra wastes
file_put_contents($dst, substr(file_get_contents($dst), 0, $offset));
} }
$package->setOutput('Binary path for micro SAPI', $dst); $package->setOutput('Binary path for micro SAPI', $dst);
} }
@@ -680,6 +671,43 @@ trait unix
return $php; return $php;
} }
/**
* Remove the extra segment appended by UPX when packing phpmicro.
*/
private static function patchMicroUpxPackedBinary(string $binaryPath): void
{
[$ret, $out] = shell()->execWithResult('readelf -W -l ' . escapeshellarg($binaryPath));
if ($ret !== 0) {
throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output');
}
$fileSizes = [];
foreach ($out as $index => $line) {
$line = trim($line);
if (!preg_match('/^(LOAD|GNU_STACK)\s+/', $line)) {
continue;
}
$columns = preg_split('/\s+/', $line);
if (isset($columns[4]) && preg_match('/^0x[[:xdigit:]]+$/', $columns[4]) === 1) {
$fileSizes[] = $columns[4];
continue;
}
$next = $out[$index + 1] ?? '';
if (preg_match('/^\s*(0x[[:xdigit:]]+)\s+0x[[:xdigit:]]+/', $next, $matches)) {
$fileSizes[] = $matches[1];
}
}
$offset = $fileSizes[1] ?? null;
if ($offset === null) {
throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output');
}
file_put_contents($binaryPath, substr(file_get_contents($binaryPath), 0, intval(hexdec($offset))));
}
/** /**
* Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS. * Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS.
*/ */

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Package\Target\php; namespace Package\Target\php;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Stage;
@@ -126,7 +127,10 @@ trait windows
throw new PatchException('Windows Makefile patching for php.exe target', 'Cannot patch windows CLI Makefile, Makefile does not contain "$(BUILD_DIR)\php.exe:" line'); throw new PatchException('Windows Makefile patching for php.exe target', 'Cannot patch windows CLI Makefile, Makefile does not contain "$(BUILD_DIR)\php.exe:" line');
} }
$lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest'; $lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest';
$lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; // /FORCE:MULTIPLE: extensions may bundle their own static copies of common libraries (e.g.
// imagick's ImageMagick ships its own zlib/png/jpeg, duplicating gd's); let the first
// definition win instead of failing with LNK2005. /ignore:4006 silences the resulting noise.
$lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286 /FORCE:MULTIPLE /ignore:4006';
FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines));
} }
@@ -171,8 +175,9 @@ trait windows
} }
} }
$libs_cli = $this->makeLibsResponseFile($package, 'cli', "ws2_32.lib shell32.lib {$extra_libs}");
cmd()->cd($package->getSourceDir()) cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php.exe"); ->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"{$libs_cli}\" EXTRA_LD_FLAGS_PROGRAM= php.exe");
$this->deployWindowsBinary($builder, $package, 'php-cli'); $this->deployWindowsBinary($builder, $package, 'php-cli');
} }
@@ -197,7 +202,10 @@ trait windows
throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line');
} }
$lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest';
$lines[$line_num + 1] = "\t" . '@"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; // /FORCE:MULTIPLE /ignore:4006: same reason as the php.exe target above. Without it the
// CGI link fails with LNK2005 (e.g. imagick's MagickCore defines gettimeofday, which
// php's time.obj also defines).
$lines[$line_num + 1] = "\t" . '@"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286 /FORCE:MULTIPLE /ignore:4006';
FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines));
// Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE()
@@ -238,8 +246,9 @@ trait windows
} }
} }
$libs_cgi = $this->makeLibsResponseFile($package, 'cgi', "ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}");
cmd()->cd($package->getSourceDir()) cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe"); ->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"{$libs_cgi}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe");
$this->deployWindowsBinary($builder, $package, 'php-cgi'); $this->deployWindowsBinary($builder, $package, 'php-cgi');
} }
@@ -264,6 +273,19 @@ trait windows
} }
} }
#[BeforeStage('php', [self::class, 'makeMicroForWindows'])]
#[PatchDescription('Add /FORCE:MULTIPLE to the micro.sfx link, matching the CLI and CGI targets')]
public function patchMicroTarget(TargetPackage $package): void
{
$makefile = "{$package->getSourceDir()}\\Makefile";
if (str_contains(FileSystem::readFile($makefile), 'LDFLAGS_MICRO=/FORCE:MULTIPLE')) {
return;
}
// Same reason as the php.exe and php-cgi.exe targets: extension-bundled libs duplicate
// symbols from php's own objects and the link fails with LNK2005.
FileSystem::replaceFileStr($makefile, "\r\nLDFLAGS_MICRO=", "\r\nLDFLAGS_MICRO=/FORCE:MULTIPLE /ignore:4006 ");
}
#[Stage] #[Stage]
public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void public function makeMicroForWindows(TargetPackage $package, PackageBuilder $builder, PackageInstaller $installer): void
{ {
@@ -311,8 +333,9 @@ trait windows
$fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : '';
$libs_micro = $this->makeLibsResponseFile($package, 'micro', "ws2_32.lib shell32.lib {$extra_libs}");
cmd()->cd($package->getSourceDir()) cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"{$libs_micro}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro");
$this->deployWindowsBinary($builder, $package, 'php-micro'); $this->deployWindowsBinary($builder, $package, 'php-micro');
} }
@@ -714,12 +737,18 @@ C_CODE;
// MSVC cl.exe format: compiler flags must come before /link, linker flags after // MSVC cl.exe format: compiler flags must come before /link, linker flags after
// ldflags contains /LIBPATH which must be after /link // ldflags contains /LIBPATH which must be after /link
// /FORCE:MULTIPLE: in ZTS mode both zend.obj and php_embed.obj (both packed into the fat php8embed.lib) define _tsrm_ls_cache as a __declspec(thread) variable. // /FORCE:MULTIPLE: in ZTS mode both zend.obj and php_embed.obj (both packed into the fat php8embed.lib) define _tsrm_ls_cache as a __declspec(thread) variable.
// /INCLUDE: php8embed.lib's ext/uri (uri_parser_whatwg.obj) references lexbor lxb_url_*/
// lxb_unicode_idna_* via __declspec(dllimport); their definitions live in url.obj/idna.obj
// but are only pulled in if the plain symbol is referenced. Force-include one symbol from
// each so the objects are linked and the __imp_ refs auto-import. (FrankenPHP needs the same.)
// System libs add pathcch (PathCchCanonicalizeEx), secur32 (curl Schannel InitSecurityInterface),
// crypt32/gdi32 (OpenSSL + Schannel) on top of the Makefile LIBS set.
$compile_cmd = sprintf( $compile_cmd = sprintf(
'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /LIBPATH:"%s\lib" %s %s', 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /INCLUDE:lxb_url_parse /INCLUDE:lxb_unicode_idna_init /LIBPATH:"%s\lib" %s %s',
$include_flags, $include_flags,
BUILD_ROOT_PATH, BUILD_ROOT_PATH,
$config['libs'], $config['libs'],
'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS) 'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib pathcch.lib secur32.lib crypt32.lib gdi32.lib' // Windows system libs (match Makefile LIBS) + curl/openssl deps
); );
// Log command explicitly (workaround for cmd() not logging complex commands properly) // Log command explicitly (workaround for cmd() not logging complex commands properly)
@@ -733,9 +762,11 @@ C_CODE;
); );
} }
// Run the embed test // Run the embed test. Use a ".\" prefix: cmd.exe does not resolve a bare "embed.exe" from
// the current directory, while the cwd must remain $test_dir so the script's relative
// "embed.php" is found.
InteractiveTerm::setMessage('Running php-embed run smoke test'); InteractiveTerm::setMessage('Running php-embed run smoke test');
[$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe'); [$ret, $output] = cmd()->cd($test_dir)->execWithResult('.\embed.exe');
$raw_output = implode('', $output); $raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') { if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new ValidationException( throw new ValidationException(
@@ -856,4 +887,17 @@ C_CODE;
$package->setOutput('PHP headers path for embed SAPI', $php_include_dir); $package->setOutput('PHP headers path for embed SAPI', $php_include_dir);
} }
/**
* Write the collected lib list into a linker response file and return the @file
* token for the nmake command line. With big extension stacks (imagick alone pulls
* in ~60 static libs) the inline list can push the whole command past cmd.exe's
* 8191 character limit; link.exe reads @file without any length limit.
*/
private function makeLibsResponseFile(TargetPackage $package, string $sapi, string $libs): string
{
$file = "{$package->getSourceDir()}\\spc-libs-{$sapi}.rsp";
FileSystem::writeFile($file, $libs);
return "@{$file}";
}
} }

View File

@@ -228,6 +228,27 @@ class ArtifactExtractor
$mode = $extract_config['mode']; $mode = $extract_config['mode'];
if ($mode === 'selective') { if ($mode === 'selective') {
// Check if all destination files already exist before extracting
$all_files_exist = true;
foreach ($extract_config['files'] as $src_pattern => $dst_path) {
$resolved = $this->replacePathVariables($dst_path);
// For glob patterns, check if at least the parent directory exists
if (str_contains($src_pattern, '*')) {
$parent = dirname($resolved);
if (!is_dir($parent)) {
$all_files_exist = false;
break;
}
} elseif (!file_exists($resolved) && !is_dir($resolved)) {
$all_files_exist = false;
break;
}
}
if ($all_files_exist) {
logger()->debug("Binary [{$name}] already extracted (selective), skip.");
return SPC_STATUS_ALREADY_EXTRACTED;
}
$this->doSelectiveExtract($name, $cache_info, $extract_config['files']); $this->doSelectiveExtract($name, $cache_info, $extract_config['files']);
$artifact->emitAfterBinaryExtract($target_path, $platform); $artifact->emitAfterBinaryExtract($target_path, $platform);
logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); logger()->debug("Emitted after-binary-extract hooks for [{$name}]");
@@ -251,8 +272,14 @@ class ArtifactExtractor
logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); logger()->debug("Emitted after-binary-extract hooks for [{$name}]");
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
if ($hash !== null && $cache_info['cache_type'] !== 'file') { if ($hash !== null) {
FileSystem::writeFile("{$target_path}/.spc-hash", $hash); // Directory targets: store hash inside the directory
if (is_dir($target_path)) {
FileSystem::writeFile("{$target_path}/.spc-hash", $hash);
} elseif (is_file($target_path)) {
// File targets: store hash as a sidecar file (e.g., meson .whl)
FileSystem::writeFile("{$target_path}.spc-hash", $hash);
}
} }
return SPC_STATUS_EXTRACTED; return SPC_STATUS_EXTRACTED;
} }
@@ -332,9 +359,24 @@ class ArtifactExtractor
/** /**
* Check if artifact is already extracted with correct hash. * Check if artifact is already extracted with correct hash.
*
* Handles both directory targets (hash stored in $path/.spc-hash) and
* file targets (hash stored in $path.spc-hash sidecar).
*/ */
protected function isAlreadyExtracted(string $path, ?string $expected_hash): bool protected function isAlreadyExtracted(string $path, ?string $expected_hash): bool
{ {
// File targets: check sidecar hash file (e.g., meson .whl)
if (is_file($path)) {
if ($expected_hash === null) {
return false;
}
$hash_file = "{$path}.spc-hash";
if (!file_exists($hash_file)) {
return false;
}
return FileSystem::readFile($hash_file) === $expected_hash;
}
if (!is_dir($path)) { if (!is_dir($path)) {
return false; return false;
} }

View File

@@ -14,6 +14,12 @@ class GenExtTestMatrixCommand extends BaseCommand
{ {
private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro --with-suggests -vvv'; private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro --with-suggests -vvv';
private const string FRANKENPHP_BUILD_TARGETS = '--build-cli --build-frankenphp --enable-zts --with-suggests -vvv';
private const array SUPPORTED_SAPIS = [
'frankenphp',
];
private const array OS_RUNNERS = [ private const array OS_RUNNERS = [
'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'],
'windows' => ['arch' => 'x86_64', 'runner' => 'windows-2025', 'os_key' => 'Windows'], 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-2025', 'os_key' => 'Windows'],
@@ -84,6 +90,7 @@ class GenExtTestMatrixCommand extends BaseCommand
$this->addOption('for-extensions', null, InputOption::VALUE_OPTIONAL, 'Filter by extension display names, comma-separated', '') $this->addOption('for-extensions', null, InputOption::VALUE_OPTIONAL, 'Filter by extension display names, comma-separated', '')
->addOption('for-libs', null, InputOption::VALUE_OPTIONAL, 'Filter by lib names (depends+suggests), comma-separated', '') ->addOption('for-libs', null, InputOption::VALUE_OPTIONAL, 'Filter by lib names (depends+suggests), comma-separated', '')
->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Filter by OS (Linux/Darwin/Windows), comma-separated', '') ->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Filter by OS (Linux/Darwin/Windows), comma-separated', '')
->addOption('sapi', null, InputOption::VALUE_OPTIONAL, 'Add extra SAPI build tests, comma-separated (supported: frankenphp)', '')
->addOption('tier2', null, InputOption::VALUE_NONE, 'Use Tier 2 runners (Linux aarch64 + macOS x86_64, no Windows)'); ->addOption('tier2', null, InputOption::VALUE_NONE, 'Use Tier 2 runners (Linux aarch64 + macOS x86_64, no Windows)');
} }
@@ -98,8 +105,15 @@ class GenExtTestMatrixCommand extends BaseCommand
$filter_extensions = $parse_option('for-extensions'); $filter_extensions = $parse_option('for-extensions');
$filter_libs = $parse_option('for-libs'); $filter_libs = $parse_option('for-libs');
$filter_os_keys = $parse_option('os'); $filter_os_keys = $parse_option('os');
$filter_sapis = array_unique($parse_option('sapi'));
$tier2 = (bool) $this->input->getOption('tier2'); $tier2 = (bool) $this->input->getOption('tier2');
$unknown_sapis = array_values(array_diff($filter_sapis, self::SUPPORTED_SAPIS));
if (!empty($unknown_sapis)) {
$this->output->writeln('<error>Unsupported SAPI(s): ' . implode(', ', $unknown_sapis) . '</error>');
return static::USER_ERROR;
}
$base_runners = $tier2 ? self::OS_RUNNERS_TIER2 : self::OS_RUNNERS; $base_runners = $tier2 ? self::OS_RUNNERS_TIER2 : self::OS_RUNNERS;
$all = PackageConfig::getAll(); $all = PackageConfig::getAll();
@@ -126,6 +140,67 @@ class GenExtTestMatrixCommand extends BaseCommand
} }
} }
[$entries, $all_ext_lib_deps] = $this->buildEntriesForRunners(
$base_runners,
$filter_os_keys,
$all_regular,
$all_virtual,
$all_libraries,
self::BUILD_TARGETS,
'default',
);
if (in_array('frankenphp', $filter_sapis, true)) {
[$frankenphp_entries, $frankenphp_ext_lib_deps] = $this->buildEntriesForRunners(
self::OS_RUNNERS,
[],
$all_regular,
$all_virtual,
$all_libraries,
self::FRANKENPHP_BUILD_TARGETS,
'frankenphp',
);
$entries = array_merge($entries, $frankenphp_entries);
$all_ext_lib_deps = array_replace_recursive($all_ext_lib_deps, $frankenphp_ext_lib_deps);
}
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']);
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;
}));
}
$this->output->write(json_encode($entries, JSON_UNESCAPED_SLASHES));
return static::SUCCESS;
}
/**
* @return array{array<int, array<string, string>>, array<string, array<string, string[]>>}
*/
private function buildEntriesForRunners(
array $base_runners,
array $filter_os_keys,
array $all_regular,
array $all_virtual,
array $all_libraries,
string $build_targets,
string $sapi,
): array {
$os_runners = empty($filter_os_keys) $os_runners = empty($filter_os_keys)
? $base_runners ? $base_runners
: array_filter($base_runners, fn ($info) => in_array($info['os_key'], $filter_os_keys, true)); : array_filter($base_runners, fn ($info) => in_array($info['os_key'], $filter_os_keys, true));
@@ -237,40 +312,18 @@ class GenExtTestMatrixCommand extends BaseCommand
sort($groups); sort($groups);
foreach ($groups as $group) { foreach ($groups as $group) {
$extra = $this->extraBuildFlags($group);
$entries[] = [ $entries[] = [
'runner' => $os_info['runner'], 'runner' => $os_info['runner'],
'os' => $os, 'os' => $os,
'arch' => $os_info['arch'], 'arch' => $os_info['arch'],
'sapi' => $sapi,
'extension' => $group, 'extension' => $group,
'build-args' => './bin/spc build "' . $group . '" ' . self::BUILD_TARGETS . ($extra !== '' ? ' ' . $extra : ''), 'build-args' => './bin/spc build "' . $group . '" ' . $this->buildTargetsWithExtraFlags($build_targets, $group),
]; ];
} }
} }
if (!empty($filter_extensions) || !empty($filter_libs)) { return [$entries, $all_ext_lib_deps];
$entries = array_values(array_filter($entries, function (array $entry) use ($filter_extensions, $filter_libs, $all_ext_lib_deps): bool {
$names = explode(',', $entry['extension']);
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;
}));
}
$this->output->write(json_encode($entries, JSON_UNESCAPED_SLASHES));
return static::SUCCESS;
} }
/** /**
@@ -397,6 +450,18 @@ class GenExtTestMatrixCommand extends BaseCommand
return implode(' ', $flags); return implode(' ', $flags);
} }
private function buildTargetsWithExtraFlags(string $build_targets, string $group): string
{
$flags = explode(' ', $build_targets);
foreach (explode(' ', $this->extraBuildFlags($group)) as $extra) {
if ($extra === '' || in_array($extra, $flags, true)) {
continue;
}
$flags[] = $extra;
}
return implode(' ', $flags);
}
/** /**
* Resolve the value of a platform-specific array field, applying the suffix fallback chain. * Resolve the value of a platform-specific array field, applying the suffix fallback chain.
* *

View File

@@ -25,6 +25,10 @@ class TestBotCommand extends BaseCommand
private const string TIER2_LABEL = 'test/tier2'; private const string TIER2_LABEL = 'test/tier2';
private const array SAPI_LABELS = [
'sapi/frankenphp' => 'frankenphp',
];
/** PHP version labels → version string (8.5 is always included as default) */ /** PHP version labels → version string (8.5 is always included as default) */
private const array PHP_VERSION_LABELS = [ private const array PHP_VERSION_LABELS = [
'test/php-83' => '8.3', 'test/php-83' => '8.3',
@@ -93,6 +97,12 @@ class TestBotCommand extends BaseCommand
} }
$tier2 = in_array(self::TIER2_LABEL, $label_names, true); $tier2 = in_array(self::TIER2_LABEL, $label_names, true);
$need_test = in_array('need-test', $label_names, true); $need_test = in_array('need-test', $label_names, true);
$sapis = [];
foreach (self::SAPI_LABELS as $label => $sapi) {
if (in_array($label, $label_names, true)) {
$sapis[] = $sapi;
}
}
// Resolve PHP versions (default always included) // Resolve PHP versions (default always included)
$php_versions = [self::DEFAULT_PHP_VERSION]; $php_versions = [self::DEFAULT_PHP_VERSION];
@@ -119,6 +129,9 @@ class TestBotCommand extends BaseCommand
if (!empty($os_keys)) { if (!empty($os_keys)) {
$flag_parts[] = '--os=' . implode(',', $os_keys); $flag_parts[] = '--os=' . implode(',', $os_keys);
} }
if (!empty($sapis)) {
$flag_parts[] = '--sapi=' . implode(',', $sapis);
}
$gen_matrix_args = implode(' ', $flag_parts); $gen_matrix_args = implode(' ', $flag_parts);
if ($tier2) { if ($tier2) {
@@ -128,7 +141,7 @@ class TestBotCommand extends BaseCommand
fn ($k) => $k !== 'Windows' fn ($k) => $k !== 'Windows'
)); ));
if (!empty($tier2_os)) { if (!empty($tier2_os)) {
$tier2_parts = array_values(array_filter($flag_parts, fn ($f) => !str_starts_with($f, '--os='))); $tier2_parts = array_values(array_filter($flag_parts, fn ($f) => !str_starts_with($f, '--os=') && !str_starts_with($f, '--sapi=')));
$tier2_parts[] = '--os=' . implode(',', $tier2_os); $tier2_parts[] = '--os=' . implode(',', $tier2_os);
$tier2_parts[] = '--tier2'; $tier2_parts[] = '--tier2';
$gen_matrix_args_tier2 = implode(' ', $tier2_parts); $gen_matrix_args_tier2 = implode(' ', $tier2_parts);
@@ -143,6 +156,7 @@ class TestBotCommand extends BaseCommand
$label_names, $label_names,
$os_keys, $os_keys,
$tier2, $tier2,
$sapis,
$php_versions, $php_versions,
$need_test, $need_test,
); );
@@ -156,6 +170,7 @@ class TestBotCommand extends BaseCommand
'gen_matrix_args_tier2' => $gen_matrix_args_tier2, 'gen_matrix_args_tier2' => $gen_matrix_args_tier2,
'php_versions' => $php_versions, 'php_versions' => $php_versions,
'tier2' => $tier2, 'tier2' => $tier2,
'sapis' => $sapis,
'comment_body' => $comment_body, 'comment_body' => $comment_body,
]; ];
@@ -239,6 +254,7 @@ class TestBotCommand extends BaseCommand
array $label_names, array $label_names,
array $os_keys, array $os_keys,
bool $tier2, bool $tier2,
array $sapis,
array $php_versions, array $php_versions,
bool $need_test, bool $need_test,
): string { ): string {
@@ -258,6 +274,7 @@ class TestBotCommand extends BaseCommand
'`test/linux` `test/windows` `test/macos` (platform)', '`test/linux` `test/windows` `test/macos` (platform)',
'`test/tier2` (extra arch)', '`test/tier2` (extra arch)',
'`test/php-83` `test/php-84` (PHP version)', '`test/php-83` `test/php-84` (PHP version)',
'`sapi/frankenphp` (extra FrankenPHP SAPI build)',
]); ]);
// Case 1: need-test absent → invite the author to add it // Case 1: need-test absent → invite the author to add it
@@ -303,7 +320,8 @@ class TestBotCommand extends BaseCommand
} }
$php_str = implode(', ', array_map(fn ($v) => "PHP {$v}", $php_versions)) . ' NTS'; $php_str = implode(', ', array_map(fn ($v) => "PHP {$v}", $php_versions)) . ' NTS';
$active_test_labels = array_values(array_filter($label_names, fn ($l) => str_starts_with($l, 'test/'))); $sapi_str = !empty($sapis) ? ' + SAPI: ' . implode(', ', $sapis) . ' (Tier1 Linux/macOS/Windows)' : '';
$active_test_labels = array_values(array_filter($label_names, fn ($l) => str_starts_with($l, 'test/') || str_starts_with($l, 'sapi/')));
$labels_str = !empty($active_test_labels) ? '`' . implode('`, `', $active_test_labels) . '`' : '_none_'; $labels_str = !empty($active_test_labels) ? '`' . implode('`, `', $active_test_labels) . '`' : '_none_';
return implode("\n", [ return implode("\n", [
@@ -313,7 +331,7 @@ class TestBotCommand extends BaseCommand
$detected, $detected,
'**Active labels**: ' . $labels_str, '**Active labels**: ' . $labels_str,
'**Available labels**: ' . $available_labels, '**Available labels**: ' . $available_labels,
'**Config**: ' . implode(' + ', $platform_parts) . ' | ' . $php_str, '**Config**: ' . implode(' + ', $platform_parts) . ' | ' . $php_str . $sapi_str,
]); ]);
} }
} }

View File

@@ -12,7 +12,6 @@ use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\SystemTarget; use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\ToolchainManager; use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Util\FileSystem; use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\System\WindowsUtil; use StaticPHP\Util\System\WindowsUtil;
#[OptionalCheck([self::class, 'optional'])] #[OptionalCheck([self::class, 'optional'])]
@@ -74,27 +73,6 @@ class WindowsToolCheck
return CheckResult::ok($path); return CheckResult::ok($path);
} }
#[CheckItem('if nasm installed', level: 995)]
public function checkNasm(): ?CheckResult
{
if (($a = WindowsUtil::findCommand('nasm.exe')) === null) {
return CheckResult::fail('nasm.exe not found in path.', 'install-nasm');
}
return CheckResult::ok($a);
}
#[CheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)]
public function checkPerl(): ?CheckResult
{
if (($path = WindowsUtil::findCommand('perl.exe')) === null) {
return CheckResult::fail('perl not found in path.', 'install-perl');
}
if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) {
return CheckResult::fail($path . ' is not built for msvc.', 'install-perl');
}
return CheckResult::ok($path);
}
#[CheckItem('if environment is properly set up', level: 1)] #[CheckItem('if environment is properly set up', level: 1)]
public function checkenv(): ?CheckResult public function checkenv(): ?CheckResult
{ {
@@ -113,16 +91,6 @@ class WindowsToolCheck
return CheckResult::ok(); return CheckResult::ok();
} }
#[FixItem('install-perl')]
public function installPerl(): bool
{
$installer = new PackageInstaller();
$installer->addInstallPackage('strawberry-perl');
$installer->run(true);
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl');
return true;
}
#[FixItem('install-msys2-build-essentials')] #[FixItem('install-msys2-build-essentials')]
public function installMsys2(): bool public function installMsys2(): bool
{ {
@@ -141,15 +109,6 @@ class WindowsToolCheck
return true; return true;
} }
#[FixItem('install-nasm')]
public function installNasm(): bool
{
$installer = new PackageInstaller(interactive: false);
$installer->addInstallPackage('nasm');
$installer->run(true);
return true;
}
#[FixItem('install-vswhere')] #[FixItem('install-vswhere')]
public function installVSWhere(): bool public function installVSWhere(): bool
{ {

View File

@@ -111,7 +111,7 @@ class ExceptionHandler
private static function logError($message, int $indent_space = 0, bool $output_log = true, string $color = 'red'): void private static function logError($message, int $indent_space = 0, bool $output_log = true, string $color = 'red'): void
{ {
$spc_log = fopen(SPC_OUTPUT_LOG, 'a'); $spc_log = spc_log_stream(SPC_OUTPUT_LOG);
$msg = explode("\n", (string) $message); $msg = explode("\n", (string) $message);
foreach ($msg as $v) { foreach ($msg as $v) {
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);

View File

@@ -15,6 +15,7 @@ use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Exception\WrongUsageException; use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\PackageLoader; use StaticPHP\Registry\PackageLoader;
use StaticPHP\Runtime\SystemTarget; use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\BuildManifestDumper;
use StaticPHP\Util\BuildRootTracker; use StaticPHP\Util\BuildRootTracker;
use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\DirDiff; use StaticPHP\Util\DirDiff;
@@ -282,6 +283,11 @@ class PackageInstaller
// pipeline above (they were merged into $this->packages in resolvePackages()). This only // 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). // throws if a tool genuinely failed to become available (e.g. no artifact for this platform).
$this->ensureRequiredTools(); $this->ensureRequiredTools();
// Pure install operations must not overwrite the manifest of an existing build.
if ($this->build_packages !== []) {
new BuildManifestDumper()->dump($this->packages, $this->build_packages, $this->install_packages, $this);
}
} }
public function isBuildPackage(Package|string $package): bool public function isBuildPackage(Package|string $package): bool

View File

@@ -117,6 +117,19 @@ class TargetPackage extends LibraryPackage
return null; return null;
} }
/**
* Get target-specific structured data for the build manifest.
*
* Target implementations may override this method to expose build facts that
* cannot be represented by the generic resolved package list.
*
* @return array<string, mixed>
*/
public function getBuildManifestData(PackageInstaller $installer): array
{
return [];
}
/** /**
* Gets all build options for the target package. * Gets all build options for the target package.
* *

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace StaticPHP\Runtime\Shell; namespace StaticPHP\Runtime\Shell;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\InterruptException; use StaticPHP\Exception\InterruptException;
use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Runtime\SystemTarget; use StaticPHP\Runtime\SystemTarget;
@@ -153,7 +154,7 @@ class DefaultShell extends Shell
$this->logCommandInfo($cmd); $this->logCommandInfo($cmd);
logger()->debug("[TAR EXTRACT] {$cmd}"); logger()->debug("[TAR EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput); $this->passthruTolerateSymlinks($cmd);
return true; return true;
} }
@@ -200,7 +201,7 @@ class DefaultShell extends Shell
$run = function ($cmd) { $run = function ($cmd) {
$this->logCommandInfo($cmd); $this->logCommandInfo($cmd);
logger()->debug("[7Z EXTRACT] {$cmd}"); logger()->debug("[7Z EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput); $this->passthruTolerateSymlinks($cmd);
}; };
$extname = FileSystem::extname($archive_path); $extname = FileSystem::extname($archive_path);
@@ -214,4 +215,67 @@ class DefaultShell extends Shell
return true; return true;
} }
/**
* Run an extraction command, tolerating symbolic links that the host cannot create.
*
* Windows tar.exe (bsdtar) cannot create the symbolic links some archives ship (e.g. zstd's
* tests/cli-tests/bin/unzstd -> zstd), failing each with "Can't create '...': Invalid argument"
* and exiting non-zero. Those entries are never needed to build, so on Windows we swallow a
* failure whose only errors are such symlink creations and continue. Any other failure still throws.
*/
private function passthruTolerateSymlinks(string $cmd): void
{
// Symlink creation only fails on a Windows host; elsewhere extraction handles symlinks fine.
if (PHP_OS_FAMILY !== 'Windows') {
$this->passthru($cmd, $this->console_putput);
return;
}
$result = $this->passthru($cmd, $this->console_putput, capture_output: true, throw_on_error: false);
if ($result['code'] === 0) {
return;
}
if ($this->isSymlinkOnlyExtractFailure($result['output'])) {
logger()->warning('Some symbolic links could not be created during extraction and were skipped (not supported on this Windows host). This is harmless for building.');
return;
}
throw new ExecutionException(
cmd: $cmd,
message: "Command exited with non-zero code: {$result['code']}",
code: $result['code'],
cd: $this->cd,
env: $this->env,
);
}
/**
* Decide whether an extraction failure was caused solely by symbolic links that could not be
* created on Windows. Returns true only when at least one such error is present and no other
* error-looking output is found, so genuine extraction failures still propagate.
*/
private function isSymlinkOnlyExtractFailure(string $output): bool
{
$saw_symlink_error = false;
foreach (preg_split('/\r\n|\r|\n/', $output) ?: [] as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
// bsdtar's trailing summary line; not an error on its own.
if (str_contains($line, 'Error exit delayed from previous errors')) {
continue;
}
// The symlink (or other unsupported special file) that Windows refused to create.
if (str_contains($line, "Can't create") && str_contains($line, 'Invalid argument')) {
$saw_symlink_error = true;
continue;
}
// Any other error-looking line means this was not a clean symlink-only failure.
if (preg_match('/\berror\b|cannot|can\'t|failed|denied|no space|not permitted/i', $line)) {
return false;
}
}
return $saw_symlink_error;
}
} }

View File

@@ -114,8 +114,8 @@ abstract class Shell
if (!$this->enable_log_file) { if (!$this->enable_log_file) {
return; return;
} }
// write executed command to log file using spc_write_log // write executed command to log file using spc_write_log (shared handle, see spc_log_stream)
$log_file = fopen(SPC_SHELL_LOG, 'a'); $log_file = spc_log_stream(SPC_SHELL_LOG);
spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n");
spc_write_log($log_file, "> Executing command: {$cmd}\n"); spc_write_log($log_file, "> Executing command: {$cmd}\n");
// get the backtrace to find the file and line number // get the backtrace to find the file and line number
@@ -154,8 +154,8 @@ abstract class Shell
): array { ): array {
$file_res = null; $file_res = null;
if ($this->enable_log_file) { if ($this->enable_log_file) {
// write executed command to the log file using spc_write_log // write executed command to the log file using spc_write_log (shared handle, see spc_log_stream)
$file_res = fopen(SPC_SHELL_LOG, 'a'); $file_res = spc_log_stream(SPC_SHELL_LOG);
} }
if ($console_output) { if ($console_output) {
$console_res = STDOUT; $console_res = STDOUT;
@@ -263,9 +263,7 @@ abstract class Shell
} }
fclose($pipes[1]); fclose($pipes[1]);
fclose($pipes[2]); fclose($pipes[2]);
if ($file_res !== null) { // $file_res is a shared, process-wide handle (see spc_log_stream); do not close it here.
fclose($file_res);
}
proc_close($process); proc_close($process);
} }
} }

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\ConsoleApplication;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\SystemTarget;
final class BuildManifestDumper
{
public const int SCHEMA_VERSION = 1;
/**
* @param array<string, Package> $packages Resolved packages in dependency order
* @param array<string, Package> $buildPackages Packages explicitly scheduled for building
* @param array<string, Package> $installPackages Packages explicitly scheduled for installation
*/
public function dump(
array $packages,
array $buildPackages,
array $installPackages,
PackageInstaller $installer,
string $outputPath = BUILD_ROOT_PATH . '/build-manifest.json',
): void {
$targets = [];
foreach ($packages as $package) {
if (!$package instanceof TargetPackage) {
continue;
}
$data = $package->getBuildManifestData($installer);
if ($data !== []) {
$targets[$package->getName()] = $data;
}
}
$manifest = [
'schema-version' => self::SCHEMA_VERSION,
'spc-version' => ConsoleApplication::VERSION,
'target' => [
'platform' => SystemTarget::getCurrentPlatformString(),
'os' => SystemTarget::getTargetOS(),
'architecture' => SystemTarget::getTargetArch(),
'libc' => SystemTarget::getLibc(),
'libc-version' => SystemTarget::getLibcVersion(),
'spc-target' => getenv('SPC_TARGET') ?: null,
'toolchain' => getenv('SPC_TOOLCHAIN') ?: null,
],
'requested-packages' => [
'build' => array_keys($buildPackages),
'install' => array_keys($installPackages),
],
'packages' => array_values(array_map(
static function (Package $package): array {
$artifact = $package->getArtifact();
return [
'name' => $package->getName(),
'type' => $package->getType(),
'artifact' => $artifact?->getName(),
];
},
$packages,
)),
'targets' => $targets,
];
$json = json_encode(
$manifest,
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
);
if (FileSystem::writeFile($outputPath, $json . PHP_EOL) === false) {
throw new SPCInternalException("Failed to write build manifest: {$outputPath}");
}
logger()->info('Generated build manifest with ' . count($packages) . " package(s): {$outputPath}");
}
}

View File

@@ -322,7 +322,16 @@ class FileSystem
} }
$sub_file = self::convertPath($dir . '/' . $v); $sub_file = self::convertPath($dir . '/' . $v);
if (is_link($sub_file) || is_file($sub_file)) { if (is_link($sub_file) || is_file($sub_file)) {
if (!unlink($sub_file)) { if (!@unlink($sub_file)) {
$last_error = error_get_last();
logger()->debug("unlink failed for '{$sub_file}': " . ($last_error['message'] ?? 'unknown error') . ', attempting chmod + unlink');
// Go module cache files are often read-only; make writable and retry
if (@chmod($sub_file, 0666) && @unlink($sub_file)) {
continue;
}
// Shell fallback: del /f /q forces deletion of read-only files on Windows
$cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f'; $cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f';
f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret); f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret);
if ($ret !== 0) { if ($ret !== 0) {

View File

@@ -66,11 +66,10 @@ if (filter_var(getenv('SPC_ENABLE_LOG_FILE'), FILTER_VALIDATE_BOOLEAN)) {
} }
} }
$log_file_fd = fopen(SPC_OUTPUT_LOG, 'a'); // Use a single shared handle (see spc_log_stream) so the file is opened exactly once;
$ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) { // on Windows a second concurrent open fails while child processes hold an inherited handle.
if ($log_file_fd) { $ob_logger->addLogCallback(function ($level, $output) {
spc_write_log($log_file_fd, strip_ansi_colors($output) . "\n"); spc_write_log(spc_log_stream(SPC_OUTPUT_LOG), strip_ansi_colors($output) . "\n");
}
return true; return true;
}); });
} }

View File

@@ -137,6 +137,11 @@ function spc_add_log_filter(array|string $filter): void
function spc_write_log(mixed $stream, string $data): false|int function spc_write_log(mixed $stream, string $data): false|int
{ {
// Defensive: a log stream may be false/null when its file could not be opened
// (e.g. transient sharing violations on Windows). Never let logging crash the run.
if (!is_resource($stream)) {
return false;
}
// get filter // get filter
global $spc_log_filters; global $spc_log_filters;
if (is_array($spc_log_filters)) { if (is_array($spc_log_filters)) {
@@ -145,6 +150,29 @@ function spc_write_log(mixed $stream, string $data): false|int
return fwrite($stream, $data); return fwrite($stream, $data);
} }
/**
* Return a single, process-wide shared append handle for the given log file.
*
* The handle is opened lazily once and reused for the lifetime of the process. This is
* important on Windows: every time a log file is opened with fopen() its handle is inherited
* by child processes spawned via proc_open() (curl, git, tar, ...). While such a child is
* alive it keeps the file open, and any *additional* open of the same file fails with a
* sharing violation ("The process cannot access the file because it is being used by another
* process."). During parallel downloads many children run at once, so opening a fresh handle
* per log line crashes. Keeping exactly one handle per file means there is never a second open
* to violate. Returns null when the file cannot be opened.
*
* @internal
*/
function spc_log_stream(string $file): mixed
{
static $streams = [];
if (!isset($streams[$file]) || !is_resource($streams[$file])) {
$streams[$file] = @fopen($file, 'a') ?: null;
}
return $streams[$file];
}
// ------- function f_* part ------- // ------- function f_* part -------
// f_ means standard function wrapper // f_ means standard function wrapper

View File

@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Util;
use Package\Target\php as PhpTargetPackage;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\ConsoleApplication;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\BuildManifestDumper;
/**
* @internal
*/
class BuildManifestDumperTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/build_manifest_dumper_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
}
protected function tearDown(): void
{
parent::tearDown();
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
}
public function testDumpWritesResolvedPackagesAndBuildContext(): void
{
$phpArtifact = new Artifact('php-src', ['source' => ['type' => 'url', 'url' => 'https://example.com/php.tar.gz']]);
$phpManifestData = [
'thread-safety' => 'nts',
'extensions' => ['all' => ['curl'], 'static' => ['curl'], 'shared' => []],
];
$php = $this->createTargetPackage('php', $phpArtifact, $phpManifestData);
$openssl = $this->createPackage('openssl', 'library');
$curl = $this->createPackage('ext-curl', 'php-extension');
$cli = $this->createPackage('php-cli', 'target');
$installer = $this->createMock(PackageInstaller::class);
$outputPath = $this->tempDir . '/nested/build-manifest.json';
(new BuildManifestDumper())->dump(
[
'openssl' => $openssl,
'ext-curl' => $curl,
'php' => $php,
'php-cli' => $cli,
],
['php' => $php],
['php-cli' => $cli],
$installer,
$outputPath,
);
$this->assertFileExists($outputPath);
$manifest = json_decode((string) file_get_contents($outputPath), true, flags: JSON_THROW_ON_ERROR);
$this->assertSame(BuildManifestDumper::SCHEMA_VERSION, $manifest['schema-version']);
$this->assertSame(ConsoleApplication::VERSION, $manifest['spc-version']);
$this->assertSame(SystemTarget::getCurrentPlatformString(), $manifest['target']['platform']);
$this->assertSame(SystemTarget::getTargetOS(), $manifest['target']['os']);
$this->assertSame(SystemTarget::getTargetArch(), $manifest['target']['architecture']);
$this->assertSame(['php'], $manifest['requested-packages']['build']);
$this->assertSame(['php-cli'], $manifest['requested-packages']['install']);
$this->assertSame([
['name' => 'openssl', 'type' => 'library', 'artifact' => null],
['name' => 'ext-curl', 'type' => 'php-extension', 'artifact' => null],
['name' => 'php', 'type' => 'target', 'artifact' => 'php-src'],
['name' => 'php-cli', 'type' => 'target', 'artifact' => null],
], $manifest['packages']);
$this->assertSame(['php' => $phpManifestData], $manifest['targets']);
}
public function testPhpTargetExportsPhpSpecificBuildFacts(): void
{
$curl = new PhpExtensionPackage('curl', extension_config: ['arg-type' => 'with']);
$curl->setBuildStatic();
$pdo = new PhpExtensionPackage('pdo', extension_config: ['arg-type' => 'enable']);
$pdo->setBuildStatic();
$pdo->setBuildShared();
$xdebug = new PhpExtensionPackage('xdebug', extension_config: ['arg-type' => 'enable']);
$xdebug->setBuildShared();
$unused = new PhpExtensionPackage('unused', extension_config: ['arg-type' => 'enable']);
$installer = $this->createMock(PackageInstaller::class);
$installer->method('getResolvedPackages')->willReturn([$curl, $pdo, $xdebug, $unused]);
$installer->method('isPackageResolved')->willReturnCallback(
static fn (string $name): bool => in_array($name, ['php-cli', 'php-micro', 'php-embed'], true),
);
$php = new class('php', 'target') extends PhpTargetPackage {
public static function getPHPVersion(?string $from_custom_source = null, bool $return_null_if_failed = false): ?string
{
return '8.4.10';
}
public function getBuildOption(string $key, mixed $default = null): mixed
{
return $key === 'enable-zts' ? true : $default;
}
};
$data = $php->getBuildManifestData($installer);
$this->assertSame('8.4.10', $data['version']);
$this->assertSame('zts', $data['thread-safety']);
$this->assertSame(['cli', 'micro', 'embed'], $data['sapis']);
$this->assertSame([
'all' => ['curl', 'pdo', 'xdebug'],
'static' => ['curl', 'pdo'],
'shared' => ['pdo', 'xdebug'],
], $data['extensions']);
}
private function createPackage(string $name, string $type, ?Artifact $artifact = null): Package
{
return new class($name, $type, $artifact) extends Package {
public function __construct(string $name, string $type, private readonly ?Artifact $artifact)
{
parent::__construct($name, $type);
}
public function getArtifact(): ?Artifact
{
return $this->artifact;
}
};
}
/** @param array<string, mixed> $manifestData */
private function createTargetPackage(string $name, ?Artifact $artifact, array $manifestData): TargetPackage
{
return new class($name, 'target', $artifact, $manifestData) extends TargetPackage {
/** @param array<string, mixed> $manifestData */
public function __construct(
string $name,
string $type,
private readonly ?Artifact $artifact,
private readonly array $manifestData,
) {
parent::__construct($name, $type);
}
public function getArtifact(): ?Artifact
{
return $this->artifact;
}
public function getBuildManifestData(PackageInstaller $installer): array
{
return $this->manifestData;
}
};
}
private function removeDirectory(string $directory): void
{
$items = scandir($directory);
if ($items === false) {
return;
}
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $directory . DIRECTORY_SEPARATOR . $item;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($directory);
}
}

1620
yarn.lock

File diff suppressed because it is too large Load Diff