Skip to content
Closed
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
63 changes: 60 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@

## 📖 简介

`tinywan/webman-typephp` 是面向 Webman 2.x 的 [TypePHP AOT](https://swoole.com/aot/zh) 构建插件。它会从现有 Webman 项目生成 AOT 入口和 Linux 编译配置,再交给固定版本的 Docker builder 完成编译,最后整理出可以复制到目标服务器的 `dist/` 目录。
`tinywan/webman-typephp` 是面向 Webman 2.x 的 [TypePHP AOT](https://swoole.com/aot/zh) 构建插件。它会从现有 Webman 项目生成 AOT 入口和编译配置,可交给固定版本的 Docker builder 生成 Linux portable-dir,也可直接调用宿主机 TypePHP 工具链生成当前平台原生程序。

宿主机只需要 PHP、Composer 和 Docker,不需要安装 C++、Clang 或 TypePHP 编译工具链。
Linux portable-dir 模式下,宿主机只需要 PHP、Composer 和 Docker。原生模式不使用 Docker,但需要匹配 ABI 的 PHP embed SDK、PHPX、TypePHP 和 C++17 编译器。

## 🌟 核心特性

- ⚡ **一键构建**:自动生成 `main.php` 与 `project.linux.yml`,统一调度 Docker 编译环境。
- ⚡ **两种构建路径**:`typephp:package` 使用 Docker 生成 Linux portable-dir;`typephp:compile` 直接调用宿主机 TypePHP、PHPX 与 C++ 编译器生成当前平台程序。
- 🧩 **全版本 Webman 兼容**:自动把 `webman-framework` 的 `helpers.php` 与 `fast-route` 的 `functions.php` 平铺为 AOT 专用版本(`.typephp/build/`),规避新版框架顶层 `if` 守卫触发的 `Unsupported statement: Stmt_If` 编译错误或静默跳过,同时保证 `base_path()`、`config()`、`FastRoute\simpleDispatcher()` 等全局函数完整编译进二进制。
- 🩹 **协程静态属性补丁**:自动把 `workerman/coroutine` 的 `Context`/`WaitGroup`/`Barrier` 中未初始化的标量静态属性补成可空并默认 `null`(`.typephp/build/`),规避 TypePHP 编译产物把未初始化标量静态读作零值导致 `??=` 守卫失效、进而触发 `Invalid callback ::destroy` 崩溃循环的问题。
- 🔧 **可变参数闭包补丁**:自动把 `Worker`/`TcpConnection`/`AsyncTcpConnection`/`Select`/webman `File` 中签名不足的错误处理与信号闭包补成可变参数形态(`.typephp/build/`),规避 TypePHP 编译产物对闭包调用强制精确参数个数(PHP 语义允许多传忽略)导致每次 accept 抛 `ArgumentCountError`、worker 崩溃循环的问题。
Expand Down Expand Up @@ -52,12 +52,21 @@ composer require tinywan/webman-typephp --dev
php webman typephp:doctor
```

检查不使用 Docker 的宿主机原生工具链:

```bash
php webman typephp:doctor --target=native
```

### 3. 编译打包

```bash
# 默认输出到 dist/
php webman typephp:package

# SaiAdmin:自动发现核心、app、support 与已安装插件服务端业务代码
php webman typephp:package --profile=saiadmin

# dist/ 已存在时,显式确认覆盖
php webman typephp:package --force

Expand All @@ -67,6 +76,22 @@ php webman typephp:package --refresh-main

默认 builder 为 `tinywan/typephp-webman-builder:v0.1.3`。编译在 Docker 中完成,宿主机不需要 C++、Clang 或 TypePHP 编译器。

直接调用宿主机 TypePHP 和 clang 编译当前平台程序:

```bash
php webman typephp:doctor --target=native
php webman typephp:compile --profile=saiadmin
```

原生模式输出 `build/webman-server` 及
`.typephp/build/native-build-manifest.json`。它不会启动 Docker,也不会把当前平台程序包装成 Linux
portable-dir;macOS 构建结果是 Mach-O,只能在 ABI 兼容的 macOS 环境运行。
如果项目中已经存在旧版本插件生成的 `main.php`,升级后首次重建应增加
`--refresh-main`;命令会先保留 `main.php.bak`。

SaiAdmin 的支持矩阵、开发规范、存量迁移、配置样例、验收脚本和实跑证据见
[`docs/saiadmin-aot/`](docs/saiadmin-aot/README.md)。

### 4. 启动产物

将 `dist/` 复制到兼容的 Linux x86_64/glibc 服务器,在目录内启动:
Expand Down Expand Up @@ -106,6 +131,7 @@ dist/
├── lib/ # 随包发布的底层系统与扩展动态依赖库 (ldd 完整收集)
├── runtime/ # 运行时缓存与日志目录 (logs, views)
├── build-manifest.json # 输入、镜像与时间等构建元数据
├── source-coverage.json # SaiAdmin profile 的逐业务文件 AOT 覆盖清单
├── config/ # 项目运行时配置(若存在)
├── public/ # 静态资源(若存在)
└── app/view/ # 视图模板(若存在)
Expand All @@ -118,10 +144,13 @@ dist/
| 命令 | 说明 |
| --- | --- |
| `php webman typephp:package` | 使用默认 builder 构建 Linux portable-dir |
| `php webman typephp:package --profile=saiadmin` | 使用锁版本、失败关闭的 SaiAdmin 自动发现与兼容规则构建 |
| `php webman typephp:compile --profile=saiadmin` | 不使用 Docker,调用本机 TypePHP 工具链编译当前平台原生程序 |
| `php webman typephp:package --force` | 覆盖已有输出,并保留旧目录备份 |
| `php webman typephp:package --refresh-main` | 强制从最新官方 stub 刷新 `main.php`(旧文件自动备份) |
| `php webman typephp:package --image=...` | 使用指定且经过验证的 Docker 镜像 |
| `php webman typephp:doctor` | 检查 PHP、Docker 和构建前置条件 |
| `php webman typephp:doctor --target=native` | 检查 PHP embed、PHPX、TypePHP 和本机 C++ 编译器 |
| `php webman typephp:init-ci` | 生成 Linux amd64 GitHub Actions 工作流 |

## ⚙️ 配置
Expand All @@ -135,6 +164,13 @@ return [
'enabled' => true,
'image' => 'tinywan/typephp-webman-builder:v0.1.3',
],
'native' => [
'tpc' => null,
'php' => null,
'php_home' => null,
'phpx_home' => null,
'cxx' => null,
],
'build' => [
'output_name' => 'webman-server',
'dist_dir' => 'dist',
Expand All @@ -145,6 +181,27 @@ return [

`--image` 的优先级最高;未指定时使用上述 `docker.image`,配置缺失时才回退至 `tinywan/typephp-webman-builder:v0.1.3`。

原生工具链默认按环境变量和常见 Composer/Homebrew 路径发现。自动发现不适用时,可通过
`native.*`、`PHP_HOME`、`PHPX_HOME`、`TYPEPHP_TPC`、`CXX`,或
`typephp:compile` 的同名命令选项显式指定。PHP 可执行文件、`php-config`、头文件、
`libphp` 和 `libphpx` 必须来自同一 PHP 8.4/8.5 ABI;发现版本混用时命令会在编译前失败。

### SaiAdmin profile

`--profile=saiadmin` 要求存在 `plugin/saiadmin` 和有效的 `composer.lock`。profile
沿用本插件 Composer 对 Webman 的版本约束,不再额外设置 Webman、Workerman 或
SaiAdmin 版本白名单。安全边界由 Composer 可安装约束、SaiAdmin 安装源码一致性、
兼容规则预期命中数和完整编译共同保证;源码结构漂移会失败关闭。当前仍会精确校验
ThinkORM `v3.0.34` 和 Carbon `3.13.2`,因为相关兼容规则尚未完成跨版本验证。

已验证版本分层如下:SaiAdmin `6.1.1` 与 `6.1.5` 均已完成 Linux amd64
编译、打包和隔离 MySQL 业务验收。`6.1.5` 的普通 PHP 8.4 对照中,验证码、
登录和用户信息通过;权限异常路径仍受上游隐式 nullable deprecation 影响,具体
证据边界见 [支持矩阵](docs/saiadmin-aot/docs/compatibility-matrix.md) 与
[验证证据](docs/saiadmin-aot/docs/verification-evidence.md)。

profile 会自动发现根 `app/`、`support/`、SaiAdmin 核心和每个 `plugin/*/app/`,并编译完整 Composer 依赖树。兼容副本只写入 `.typephp/build/`,普通 PHP 源码不变。构建输出中的 `source-coverage.json` 逐项记录业务 PHP 是直接编译还是由哪个 AOT 副本替代;业务文件未分类、被排除却没有等价副本,或漂移到未知依赖版本时都会终止构建。

## 🎯 可信 MVP 边界

当前第一阶段只承诺已经验证的组合:
Expand Down
26 changes: 12 additions & 14 deletions docker/AssignOpTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,20 +287,18 @@ protected function parseAssignToList(Expr $left, Expr $right): string
continue;
}
if ($item instanceof ArrayItem) {
$key = $item->key ? $this->parseArrayKey($item->key) : (string) $k;
$value = new Expr\ArrayDimFetch(
new Variable($tmpVar),
$item->key ?? new Node\Scalar\Int_($k),
);
if ($item->value instanceof Expr\List_) {
$nestedTmp = $this->genTmpVarName();
$this->addLocalVar($nestedTmp, Type::ARRAY);
$code .= $this->getIndent() . "{$nestedTmp} = {$tmpVar}.item({$key});" . PHP_EOL;
$code .= $this->getIndent()
. $this->parseAssignToList($item->value, new Variable($nestedTmp))
. $this->parseAssignToList($item->value, $value)
. PHP_EOL;
} else {
$var = $this->parseWritableIdentifier($item->value);
if ($this->isVarExpr($item->value) and !$this->hasVar($var)) {
$this->addLocalVar($var, Type::VAR);
}
$code .= $this->getIndent() . "{$var} = {$tmpVar}.item({$key});" . PHP_EOL;
$code .= $this->getIndent()
. $this->parseAssignFinally($item->value, $value)
. ';' . PHP_EOL;
}
} else {
$this->unsupportedSyntax($item);
Expand Down Expand Up @@ -885,7 +883,7 @@ protected function parseAssignOp(Expr\AssignOp $node, string $op): string
return $pythonOperator;
}
$propertyWriteTarget = $this->preparePropertyWriteTarget($node->var);
$this->guardLiteralDivisionByZero($node->expr, $op);
$this->guardLiteralDivisionByZero($node->var, $node->expr, $op);

// A compound division/modulo on a NATIVE scalar slot with a proven
// zero divisor cannot fall through to the raw C++ operator (SIGFPE
Expand All @@ -894,7 +892,7 @@ protected function parseAssignOp(Expr\AssignOp $node, string $op): string
// lower the whole expression to the PHP-semantics binary operation
// and leave the target untouched.
if (($op === '/=' || $op === '%=')
&& !$this->nativeTypes
&& $this->varIntTypes
&& $this->isZeroLiteral($node->expr)
&& $this->isVarExpr($node->var)
&& $this->hasVar((string) $this->parseIdentifier($node->var))
Expand All @@ -903,7 +901,7 @@ protected function parseAssignOp(Expr\AssignOp $node, string $op): string
// std::int()/std::float() values are an explicit opt-in to native
// C++ arithmetic; changing them to PHP semantics here would be as
// wrong as the undefined raw operation. Keep the compile-time
// rejection native_types mode uses.
// rejection varint_types mode uses.
if ($this->isExplicitNativeArithmeticExpr($node->var)) {
$this->fatalError($node->expr, 'Cannot divide or modulo by zero');
}
Expand Down Expand Up @@ -1201,7 +1199,7 @@ protected function parseNativePropertyAssignOp(Expr\AssignOp $node, string $op):
// A direct zend_long reference would bypass that behavior completely.
// Native objects cannot cross the Variant boundary and retain their
// native C++ property access path.
if (!$this->nativeTypes
if ($this->varIntTypes
&& $def->type === Type::INT
&& !$this->isNativeObjectClass($this->detectClassOfExpr($node->var->var))
&& in_array($op, ['+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '|=', '^='], true)
Expand Down
Loading