Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/sidebar.en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default {
{ text: 'Build Lifecycle', link: '/en/develop/build-lifecycle' },
{ text: 'Compilation Tools', link: '/en/develop/system-build-tools' },
{ text: 'Doctor', link: '/en/develop/doctor-module' },
{ text: 'Performance Engineering', link: '/en/develop/performance' },
{ text: 'PHP Source Modifications', link: '/en/develop/php-src-changes' },
],
},
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/sidebar.zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default {
{ text: '构建生命周期', link: '/zh/develop/build-lifecycle' },
{ text: '编译工具', link: '/zh/develop/system-build-tools' },
{ text: 'Doctor 环境检查', link: '/zh/develop/doctor-module' },
{ text: '性能工程', link: '/zh/develop/performance' },
{ text: '对 PHP 源码的修改', link: '/zh/develop/php-src-changes' },
],
},
Expand Down
133 changes: 130 additions & 3 deletions docs/en/contributing/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,132 @@
# Contributing

<!-- TODO: v3 contribution guide.
Sections: code style (php-cs-fixer, phpstan), adding a new library/extension,
adding a doctor check, submitting PRs, security disclosures. -->
StaticPHP welcomes fixes, package definitions, platform support, tests, and documentation. v3 is still defining parts of its extension API, so distinguish between a contribution to the built-in `core` Registry and an external Registry that needs a long-term compatibility contract.

## Development Setup

A source checkout requires PHP 8.4 or later and Composer. The CLI also needs the PHP extensions listed in the [source installation guide](/en/guide/installation). Install the development dependencies from the repository root:

```bash
composer install
bin/spc --version
```

Node.js and npm are only required when changing the VitePress documentation:

```bash
npm install
npm run docs:build
```

Run `bin/spc doctor` before attempting a real build. Full static builds are platform-sensitive and can be slow, so they are not a prerequisite for every documentation, test, or configuration-only change.

## Find the Correct Layer

Keep a change in the narrowest layer that can express it:

| Area | Purpose |
|---|---|
| `config/pkg/ext/` | PHP extension Package definitions |
| `config/pkg/lib/` | Link-time dependency library definitions |
| `config/pkg/target/` | Final and virtual build targets |
| `config/pkg/tool/` | Host-side build tool Packages |
| `config/artifact/` | Shared or complex source/binary Artifact definitions |
| `src/Package/` | Package-specific recipes, patches, hooks, and custom Artifact behavior |
| `src/StaticPHP/` | Framework, resolver, runtime, Doctor, Registry, and command implementation |
| `tests/` | PHPUnit tests and fixtures |
| `docs/en/`, `docs/zh/` | Canonical English documentation and its synchronized Chinese version |

Prefer declarative YAML fields over PHP conditions. Add a recipe class only when the package needs build commands, source rewrites, validation, custom configure arguments, or lifecycle hooks.

Do not fix a problem by editing generated `buildroot/`, `source/`, `downloads/`, or `pkgroot/` content. Changes there are discarded and do not describe how another user can reproduce the build.

## Adding or Updating a Package

Before writing a new definition, search for a package with the same build system and artifact type. Follow its structure, then verify these points:

1. Choose the correct type: `php-extension`, `library`, `target`, `virtual-target`, or `tool`.
2. Use the exact package name. PHP extension dependencies and configs use the `ext-` prefix.
3. Put hard dependencies in `depends`, optional relationships in `suggests`, and host build tools in `tools`.
4. Express platform differences with `@windows`, `@unix`, `@linux`, or `@macos` fields when the schema supports them.
5. Prefer an inline Artifact for a simple one-package source; use `config/artifact/` when multiple Packages share it or custom behavior makes a standalone definition clearer.
6. Record license identifiers and files in Artifact metadata. Add structured Package `license` entries only for material that must be copied after a source build.
7. Add or update the relevant PHP recipe only when configuration alone is insufficient.

See [Package Model](/en/develop/package-model) and [Artifact Model](/en/develop/artifact-model) for the current schemas.

Configuration linting is strict about unknown fields but cannot prove that a URL exists, a patch still applies, or a library links on every platform. When practical, test the oldest and newest affected PHP branches and the platforms touched by the change.

## PHP Code and Tests

StaticPHP follows the existing code style and favors existing package/build patterns over new abstractions. For framework code:

- Keep internal orchestration details internal; PHP `public` visibility alone does not make a symbol part of the supported extension API.
- Add a focused PHPUnit test for resolver, config, Registry, command, or utility behavior.
- Include the failing input or regression case in the test instead of relying only on a manual build.
- Avoid unrelated formatting or mechanical changes in the same pull request.

Tests live under `tests/StaticPHP/` and use the `Tests\StaticPHP\` namespace. Run the smallest relevant test during development, then the project checks before submitting.

## Doctor Changes

Built-in Doctor checks currently live under `src/StaticPHP/Doctor/Item/`. A check should observe the environment without changing it; installation, downloads, and file changes belong in a separate fix callback.

The current Attributes and callback signatures are internal and have known limitations, including raw fix parameters and no automatic re-check after a fix. Follow an existing core check when maintaining the built-in set, but do not present that pattern as a stable third-party API. Update [Doctor Module](/en/develop/doctor-module) and [Compilation Tools](/en/develop/system-build-tools) when user-visible checks, fixes, prerequisites, or lock behavior change.

## Documentation

English under `docs/en/` is canonical. Every English Markdown file must have a corresponding file under `docs/zh/`, and both versions must have the same headings, examples, tables, admonitions, and links to the appropriate language path.

When behavior changes, update all pages that describe it—not only the closest reference page. Common examples include CLI options, environment variables, Package/Artifact fields, Doctor checks, and migration guidance.

Validate matching file trees and build the site:

```bash
diff <(find docs/en -name '*.md' | sed 's|docs/en/||' | sort) \
<(find docs/zh -name '*.md' | sed 's|docs/zh/||' | sort)
npm run docs:build
```

New pages must also be added to both VitePress sidebars.

## Validation Checklist

Use checks proportional to the change:

| Change | Required checks |
|---|---|
| Package, Artifact, target, extension, library, or tool config | `bin/spc dev:lint-config`, `composer cs-fix` |
| PHP framework or recipe code | `composer cs-fix`, `composer test`, `composer analyse` |
| Documentation | English/Chinese tree and structure checks, `npm run docs:build` |
| Platform build fix | The checks above plus the narrowest affected build or CI matrix when available |

Useful commands:

```bash
bin/spc dev:lint-config
composer cs-fix
composer test
composer analyse
```

If a full build cannot be run locally, state which platform/PHP combinations remain untested and include the relevant logs or CI result. Build and shell logs live under `log/` by default, but generated logs should not normally be committed.

## Pull Requests

Keep each pull request focused and explain:

- The problem and why the selected layer is appropriate.
- The affected Package/Artifact, target OS, architecture, toolchain, and PHP versions.
- User-visible behavior or compatibility impact.
- Tests and builds that were run, including anything not tested.
- Documentation updated for both languages.

Complete the repository pull-request checklist. Do not commit generated build directories, downloaded archives, caches, local environment files, or unrelated editor/OS metadata.

For a new dependency or bundled patch, include its upstream source, license, and the condition that requires it. Prefer an upstream fix and keep downstream patches scoped to the affected versions.

## Security Reports

Do not publish exploit details, credentials, signing material, or a reproducible vulnerability in a normal issue. Use GitHub's private vulnerability reporting for the repository when available, or contact the maintainers through an existing project channel to arrange a private disclosure.

The repository does not currently contain a dedicated `SECURITY.md`; adding a formal supported-version and disclosure policy remains a project maintenance task.
117 changes: 115 additions & 2 deletions docs/en/develop/doctor-module.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,117 @@
# Doctor Module

<!-- TODO: Migrate and update from v2 doctor-module.md.
Cover v3 changes: --auto-fix, .spc-doctor.lock, new check items for v3 toolchain. -->
Doctor diagnoses whether the current host, selected target, and compiler toolchain can run StaticPHP's build pipeline. It checks common host commands, platform-specific SDKs, managed tool packages, and target-aware requirements such as Zig or musl support.

This page documents the current v3 behavior. The built-in check/fix loader is also described for maintainers, but its PHP Attributes, callback signatures, and loader classes are not yet a stable third-party extension API.

## Running Doctor

Run Doctor explicitly after installing StaticPHP, changing the target/toolchain, or updating system build tools:

```bash
# Pre-built binary
./spc doctor

# Source installation
bin/spc doctor
```

The fix policy is selected with `--auto-fix` (`-y`):

| Invocation | Current behavior |
|---|---|
| `spc doctor` | Ask before each available automatic fix |
| `spc doctor --auto-fix` | Apply available fixes without prompting |
| `spc doctor --auto-fix=never` | Report the first unresolved failure and do not attempt a fix |

Doctor exits successfully only when every applicable check either passes, is skipped, or reports a fix that the current implementation considers successful. It stops at the first unresolved failure.

The `craft` command also runs Doctor by default. It uses the automatic-fix policy without prompting; set `craft-options.doctor: false` in `craft.yml` to disable that step.

## Execution Model

The current execution sequence is:

1. `DoctorLoader` collects built-in and Registry-provided classes and scans their public methods for Doctor Attributes.
2. Checks are sorted by descending `level`.
3. `#[OptionalCheck]`, `limit_os`, and `SPC_SKIP_DOCTOR_CHECK_ITEMS` remove checks that do not apply.
4. The check callback is called directly.
5. `null` means skipped; `CheckResult::ok()` means passed; `CheckResult::fail()` describes a failure and may name a fix.
6. Depending on the selected fix policy, Doctor rejects, prompts for, or immediately invokes the named fix through `ApplicationContext`.

`checkAll()` is fail-fast: it does not collect all failures into a final report. A successful fixer is currently accepted without re-running its original check.

::: warning
An invalid check return value is currently printed as “Skipped due to invalid return value” and treated as successful. This is an implementation limitation, not behavior extension authors should rely on.
:::

## Built-in Check Groups

The exact list is target-aware. The current core Registry provides these groups:

| Scope | Checks |
|---|---|
| Common | At least one Registry is loaded; the host OS is Linux, macOS, or Windows |
| Linux | Distribution build-tool baseline, CMake 3.22+, `re2c` 1.0.3+, and Linux headers on musl distributions |
| macOS | Homebrew or MacPorts, required build commands, GNU Bison 3+, and the selected Homebrew/MacPorts LLVM variant |
| Windows target | `vswhere`, Visual Studio C++ tools, Git `patch.exe`, managed MSYS2, `7za.exe`, and the initialized MSVC command environment |
| Unix target | StaticPHP-managed and functional `pkg-config` |
| Zig toolchain | Installed StaticPHP `zig` tool package |
| musl target/toolchain | Required musl wrapper and, for the legacy `MuslToolchain`, the cross toolchain under `/usr/local/musl` |

Package-specific tools declared through a package's `tools` field are resolved by `PackageInstaller`; they are not all duplicated as baseline Doctor checks. See [Compilation Tools](./system-build-tools) for the platform package lists and toolchain details.

## Automatic Fixes

Depending on the failed item and platform, a fix may:

- Invoke `apt`, `apk`, `dnf`, `yum`, `pacman`, Homebrew, or MacPorts.
- Use `sudo` for system-level installation.
- Download and install a StaticPHP Tool or Artifact package.
- Build a replacement tool such as `re2c`.
- Install musl runtime or cross-toolchain files outside the workspace.

Automatic fixes can therefore use the network and change the host system. The default interactive policy asks first; CI commonly uses either `--auto-fix` in a disposable environment or `--auto-fix=never` in a pre-provisioned image.

Fixers do not share a transaction or rollback mechanism. A fixer returning `true` is currently considered sufficient, even if the environment still fails the original condition. Re-run `spc doctor --auto-fix=never` after a repair when verification matters.

## Doctor Lock

After all checks are considered successful, Doctor writes `.spc-doctor.lock` containing the current StaticPHP version. `craft` writes the same lock after its Doctor step succeeds.

The preferred locations are:

| Platform | Preferred path | Fallbacks |
|---|---|---|
| Unix | `$XDG_CACHE_HOME/.spc-doctor.lock`, or `$HOME/.cache/.spc-doctor.lock` | System temporary directory, then the working directory |
| Windows | `%LOCALAPPDATA%\.spc-doctor.lock` | System temporary directory, then the working directory |

Build/download commands that call `checkDoctorCache()` only use this lock to decide whether to show a warning. The lock does not skip an explicit `spc doctor` run and does not block a build.

The current fingerprint is only the StaticPHP version. A target, toolchain, Registry set, PATH, or operating-system change does not invalidate it automatically. Delete the lock or run Doctor explicitly after such a change.

## Skipping Checks and Warnings

Two environment variables have different purposes:

```ini
# Skip named Doctor items during an actual Doctor run (exact item names, comma-separated)
SPC_SKIP_DOCTOR_CHECK_ITEMS="if cmake version >= 3.22,if re2c version >= 1.0.3"

# Suppress the pre-build warning when the Doctor lock is missing or stale
SPC_SKIP_DOCTOR_CHECK=1
```

`SPC_SKIP_DOCTOR_CHECK` does not remove checks from an explicit `spc doctor` invocation. `SPC_SKIP_DOCTOR_CHECK_ITEMS` does, and a skipped required check can allow an unusable environment to appear healthy, so it should be limited to controlled CI or debugging cases.

## Current Internal Registration Model

A Registry can currently point `doctor.psr-4` or `doctor.classes` at PHP classes. `DoctorLoader` constructs each class without constructor arguments and recognizes:

| Attribute | Current role |
|---|---|
| `#[CheckItem]` | Registers a check name, optional OS limit, and numeric level; higher levels run first |
| `#[OptionalCheck]` | Supplies a class- or method-level callable that decides whether the check is present |
| `#[FixItem]` | Registers a string fix name used by a failed `CheckResult` |

Check callbacks currently receive no DI context and return `null` or `CheckResult`. Fix callbacks are invoked through `ApplicationContext`, with `CheckResult`'s raw `fix_params` array used as callback context. The `manual` property on `CheckItem` is not consumed by the current executor.
Loading
Loading