Skip to content

Commit d486d09

Browse files
author
Tinywan
committed
feat: add --static support for fully-static single binary packaging
1 parent 521e1f6 commit d486d09

7 files changed

Lines changed: 177 additions & 77 deletions

File tree

‎.github/workflows/docker-publish.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,22 @@ jobs:
5757
cache-to: type=gha,mode=max
5858
provenance: mode=max
5959
sbom: true
60+
- uses: docker/metadata-action@v6
61+
id: meta-static
62+
with:
63+
images: tinywan/typephp-webman-builder-static
64+
tags: |
65+
type=ref,event=tag
66+
type=raw,value=${{ inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
67+
- uses: docker/build-push-action@v7
68+
with:
69+
context: ./docker
70+
file: ./docker/Dockerfile.static
71+
platforms: linux/amd64
72+
push: ${{ steps.version.outputs.push }}
73+
tags: ${{ steps.meta-static.outputs.tags }}
74+
labels: ${{ steps.meta-static.outputs.labels }}
75+
cache-from: type=gha
76+
cache-to: type=gha,mode=max
77+
provenance: mode=max
78+
sbom: true

‎README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,12 @@ php webman typephp:package --force
4949

5050
# 强制使用最新 TypePHP stub 重置 main.php 入口(原有 main.php 会自动备份为 main.php.bak)
5151
php webman typephp:package --refresh-main
52+
53+
# 构建全静态单文件可执行产物 (零动态库依赖,单二进制)
54+
php webman typephp:package --static
5255
```
5356

54-
默认 builder 为 `tinywan/typephp-webman-builder:v0.2.1`。编译在 Docker 中完成,宿主机不需要 C++、Clang 或 TypePHP 编译器。
57+
默认 builder 为 `tinywan/typephp-webman-builder:v0.2.1`(动态便携包);全静态模式使用 `tinywan/typephp-webman-builder-static:v0.2.1`。编译在 Docker 中完成,宿主机不需要 C++、Clang 或 TypePHP 编译器。
5558

5659
### 4. 启动产物
5760

@@ -104,6 +107,7 @@ dist/
104107
| 命令 | 说明 |
105108
| --- | --- |
106109
| `php webman typephp:package` | 使用默认 builder 构建 Linux portable-dir |
110+
| `php webman typephp:package --static` | 构建全静态单文件可执行二进制(零外部动态库依赖) |
107111
| `php webman typephp:package --force` | 覆盖已有输出,并保留旧目录备份 |
108112
| `php webman typephp:package --refresh-main` | 强制从最新官方 stub 刷新 `main.php`(旧文件自动备份) |
109113
| `php webman typephp:package --image=...` | 使用指定且经过验证的 Docker 镜像 |
@@ -120,6 +124,7 @@ return [
120124
'docker' => [
121125
'enabled' => true,
122126
'image' => 'tinywan/typephp-webman-builder:v0.2.1',
127+
'static_image' => 'tinywan/typephp-webman-builder-static:v0.2.1',
123128
],
124129
'build' => [
125130
'output_name' => 'webman-server',

‎docker/Dockerfile.static‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM tinywan/typephp-linux-x64-static:v0.9.0@sha256:9f87e319a4f0a32d72c7204850a0629c961c561152145fc547c2a66d754e2dfb
2+
3+
LABEL org.opencontainers.image.title="TypePHP Webman Static Builder"
4+
LABEL org.opencontainers.image.description="Linux x86_64 Alpine musl full-static single binary builder"
5+
6+
COPY entrypoint.sh /entrypoint.sh
7+
RUN chmod 0755 /entrypoint.sh
8+
9+
WORKDIR /workspace
10+
ENTRYPOINT ["/entrypoint.sh"]

‎docker/entrypoint.sh‎

Lines changed: 105 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ readonly real_workspace=/workspace
55
readonly output_dir="${TYPEPHP_OUTPUT_DIR:-dist}"
66
readonly output_name="${TYPEPHP_OUTPUT_NAME:-webman-server}"
77
readonly force="${TYPEPHP_FORCE:-0}"
8+
readonly is_static="${TYPEPHP_STATIC:-0}"
89
readonly build_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
910

1011
validate_relative_path() {
@@ -120,8 +121,13 @@ jobs="$(nproc 2>/dev/null || echo 2)"
120121
[[ "$jobs" -lt 1 ]] && jobs=1
121122
sed -i "s/^job:.*/job: ${jobs}/" "$project_file"
122123

123-
echo "[INFO] Compiling Linux x86_64 glibc binary (job=${jobs})..."
124-
"${tpc[@]}" "$project_file" --no-progress
124+
if [[ "$is_static" = "1" ]]; then
125+
echo "[INFO] Compiling Linux x86_64 full-static binary with Clang (job=${jobs})..."
126+
"${tpc[@]}" "$project_file" --full-static --compiler=clang --no-progress
127+
else
128+
echo "[INFO] Compiling Linux x86_64 glibc binary (job=${jobs})..."
129+
"${tpc[@]}" "$project_file" --no-progress
130+
fi
125131

126132
normalized_output="$(dirname -- "$project_output")/$(basename -- "$project_output" | tr '-' '_')"
127133
if ! validate_relative_path "$normalized_output"; then
@@ -139,73 +145,103 @@ if [[ ! -f "$compiled_bin" ]]; then
139145
exit 1
140146
fi
141147

142-
if ! ldd_output="$(LD_LIBRARY_PATH=/opt/typephp/vendor/swoole/phpx/lib:/usr/lib ldd "$compiled_bin" 2>&1)"; then
143-
echo "[ERROR] ldd failed for '$compiled_bin': $ldd_output" >&2
144-
exit 1
145-
fi
146-
if grep -q 'not found' <<<"$ldd_output"; then
147-
echo "[ERROR] Unresolved runtime library: $ldd_output" >&2
148-
exit 1
149-
fi
148+
if [[ "$is_static" = "1" ]]; then
149+
echo "[INFO] Packaging full-static single binary..."
150+
trap cleanup_stage_on_failure EXIT
151+
install -m 0755 "$compiled_bin" "$stage_dir/webman-server"
152+
if command -v strip &>/dev/null; then
153+
strip --strip-all "$stage_dir/webman-server" 2>/dev/null || true
154+
fi
150155

151-
mkdir -p "$stage_dir/lib"
152-
trap cleanup_stage_on_failure EXIT
153-
install -m 0755 "$compiled_bin" "$stage_dir/webman-server.bin"
156+
# 生成全静态标准启动脚本 start.sh
157+
install -m 0755 /dev/stdin "$stage_dir/start.sh" <<'SCRIPT'
158+
#!/usr/bin/env bash
159+
set -e
160+
161+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
162+
cd "$SCRIPT_DIR"
154163
155-
# 1. 自动扫描并打包 PHP 扩展模块 (.so) 到 dist/ext
156-
mkdir -p "$stage_dir/ext"
157-
php_ext_dir="$(php-config --extension-dir 2>/dev/null || echo '/usr/lib/php/20240924')"
158-
if [[ -d "$php_ext_dir" ]]; then
159-
echo "[INFO] Copying PHP extensions from $php_ext_dir to dist/ext/ ..."
160-
cp -f "$php_ext_dir"/*.so "$stage_dir/ext/" 2>/dev/null || true
164+
if [ ! -f "$SCRIPT_DIR/webman-server" ]; then
165+
echo "[ERROR] webman-server executable not found in $SCRIPT_DIR!"
166+
exit 1
161167
fi
162168
163-
# 2. 自动通过 ldd 探测并打包所有程序、PHP 核心库与全部扩展模块的底层共享库
164-
mkdir -p "$stage_dir/lib"
165-
for bin_or_lib in "$stage_dir/webman-server.bin" "$stage_dir/lib"/*.so* "$stage_dir/ext"/*.so; do
166-
if [[ -f "$bin_or_lib" ]]; then
167-
mapfile -t found_libs < <(ldd "$bin_or_lib" 2>/dev/null | awk '/=> \// { print $3 }')
168-
for libpath in "${found_libs[@]}"; do
169-
if [[ -f "$libpath" ]]; then
170-
libname="$(basename "$libpath")"
171-
if is_platform_library "$libname"; then
172-
continue
173-
fi
174-
if [[ ! -f "$stage_dir/lib/$libname" ]]; then
175-
cp -L "$libpath" "$stage_dir/lib/$libname" || true
176-
fi
177-
fi
178-
done
179-
fi
180-
done
169+
chmod +x "$SCRIPT_DIR/webman-server" 2>/dev/null || true
170+
exec "$SCRIPT_DIR/webman-server" "$@"
171+
SCRIPT
181172

182-
# 确保核心运行时动态库存在,并复制到根目录和 lib/ 目录
183-
for required_library in 'libphpx.so*' 'libphp.so*'; do
184-
found_lib="$(find /opt /usr "$stage_dir/lib" -name "$required_library" -type f 2>/dev/null | head -n 1 || true)"
185-
if [[ -n "$found_lib" && -f "$found_lib" ]]; then
186-
libname="$(basename "$found_lib")"
187-
clean_libname="${libname%%.*}.so"
188-
cp -L "$found_lib" "$stage_dir/$clean_libname" || true
189-
cp -L "$found_lib" "$stage_dir/lib/$libname" || true
190-
else
191-
echo "[ERROR] Required library $required_library not found in /opt or /usr!" >&2
173+
if command -v file &>/dev/null; then
174+
echo "[INFO] Verifying static binary ELF format:"
175+
file "$stage_dir/webman-server" || true
176+
fi
177+
else
178+
if ! ldd_output="$(LD_LIBRARY_PATH=/opt/typephp/vendor/swoole/phpx/lib:/usr/lib ldd "$compiled_bin" 2>&1)"; then
179+
echo "[ERROR] ldd failed for '$compiled_bin': $ldd_output" >&2
180+
exit 1
181+
fi
182+
if grep -q 'not found' <<<"$ldd_output"; then
183+
echo "[ERROR] Unresolved runtime library: $ldd_output" >&2
192184
exit 1
193185
fi
194-
done
195186

196-
# 3. 修复可执行程序和扩展的 RPATH(如果有 patchelf)
197-
if command -v patchelf &> /dev/null; then
198-
patchelf --set-rpath '$ORIGIN:$ORIGIN/lib' "$stage_dir/webman-server.bin" 2>/dev/null || true
199-
for solib in "$stage_dir"/*.so "$stage_dir"/lib/*.so*; do
200-
[[ -f "$solib" ]] && patchelf --set-rpath '$ORIGIN:$ORIGIN/lib' "$solib" 2>/dev/null || true
187+
mkdir -p "$stage_dir/lib"
188+
trap cleanup_stage_on_failure EXIT
189+
install -m 0755 "$compiled_bin" "$stage_dir/webman-server.bin"
190+
191+
# 1. 自动扫描并打包 PHP 扩展模块 (.so) 到 dist/ext
192+
mkdir -p "$stage_dir/ext"
193+
php_ext_dir="$(php-config --extension-dir 2>/dev/null || echo '/usr/lib/php/20240924')"
194+
if [[ -d "$php_ext_dir" ]]; then
195+
echo "[INFO] Copying PHP extensions from $php_ext_dir to dist/ext/ ..."
196+
cp -f "$php_ext_dir"/*.so "$stage_dir/ext/" 2>/dev/null || true
197+
fi
198+
199+
# 2. 自动通过 ldd 探测并打包所有程序、PHP 核心库与全部扩展模块的底层共享库
200+
mkdir -p "$stage_dir/lib"
201+
for bin_or_lib in "$stage_dir/webman-server.bin" "$stage_dir/lib"/*.so* "$stage_dir/ext"/*.so; do
202+
if [[ -f "$bin_or_lib" ]]; then
203+
mapfile -t found_libs < <(ldd "$bin_or_lib" 2>/dev/null | awk '/=> \// { print $3 }')
204+
for libpath in "${found_libs[@]}"; do
205+
if [[ -f "$libpath" ]]; then
206+
libname="$(basename "$libpath")"
207+
if is_platform_library "$libname"; then
208+
continue
209+
fi
210+
if [[ ! -f "$stage_dir/lib/$libname" ]]; then
211+
cp -L "$libpath" "$stage_dir/lib/$libname" || true
212+
fi
213+
fi
214+
done
215+
fi
201216
done
202-
for extsolib in "$stage_dir"/ext/*.so; do
203-
[[ -f "$extsolib" ]] && patchelf --set-rpath '$ORIGIN/../lib:$ORIGIN:$ORIGIN/..' "$extsolib" 2>/dev/null || true
217+
218+
# 确保核心运行时动态库存在,并复制到根目录和 lib/ 目录
219+
for required_library in 'libphpx.so*' 'libphp.so*'; do
220+
found_lib="$(find /opt /usr "$stage_dir/lib" -name "$required_library" -type f 2>/dev/null | head -n 1 || true)"
221+
if [[ -n "$found_lib" && -f "$found_lib" ]]; then
222+
libname="$(basename "$found_lib")"
223+
clean_libname="${libname%%.*}.so"
224+
cp -L "$found_lib" "$stage_dir/$clean_libname" || true
225+
cp -L "$found_lib" "$stage_dir/lib/$libname" || true
226+
else
227+
echo "[ERROR] Required library $required_library not found in /opt or /usr!" >&2
228+
exit 1
229+
fi
204230
done
205-
fi
206231

207-
# 4. 生成自包含纯净 php.ini
208-
cat > "$stage_dir/php.ini" << 'EOF'
232+
# 3. 修复可执行程序和扩展的 RPATH(如果有 patchelf)
233+
if command -v patchelf &> /dev/null; then
234+
patchelf --set-rpath '$ORIGIN:$ORIGIN/lib' "$stage_dir/webman-server.bin" 2>/dev/null || true
235+
for solib in "$stage_dir"/*.so "$stage_dir"/lib/*.so*; do
236+
[[ -f "$solib" ]] && patchelf --set-rpath '$ORIGIN:$ORIGIN/lib' "$solib" 2>/dev/null || true
237+
done
238+
for extsolib in "$stage_dir"/ext/*.so; do
239+
[[ -f "$extsolib" ]] && patchelf --set-rpath '$ORIGIN/../lib:$ORIGIN:$ORIGIN/..' "$extsolib" 2>/dev/null || true
240+
done
241+
fi
242+
243+
# 4. 生成自包含纯净 php.ini
244+
cat > "$stage_dir/php.ini" << 'EOF'
209245
output_buffering=0
210246
implicit_flush=1
211247
memory_limit=4G
@@ -231,18 +267,18 @@ extension=sockets.so
231267
extension=event.so
232268
EOF
233269

234-
# 若 ext/ 目录下没有对应扩展 .so,则注释该行以防产生 Warning
235-
while IFS= read -r line || [[ -n "$line" ]]; do
236-
if echo "$line" | grep -q "^extension="; then
237-
ext_file=$(echo "$line" | cut -d'=' -f2)
238-
if [[ ! -f "$stage_dir/ext/$ext_file" ]]; then
239-
sed -i "s/^extension=$ext_file$/; extension=$ext_file/" "$stage_dir/php.ini"
270+
# 若 ext/ 目录下没有对应扩展 .so,则注释该行以防产生 Warning
271+
while IFS= read -r line || [[ -n "$line" ]]; do
272+
if echo "$line" | grep -q "^extension="; then
273+
ext_file=$(echo "$line" | cut -d'=' -f2)
274+
if [[ ! -f "$stage_dir/ext/$ext_file" ]]; then
275+
sed -i "s/^extension=$ext_file$/; extension=$ext_file/" "$stage_dir/php.ini"
276+
fi
240277
fi
241-
fi
242-
done < "$stage_dir/php.ini"
278+
done < "$stage_dir/php.ini"
243279

244-
# 5. 生成标准可移植启动包装脚本 webman-server 与 start.sh
245-
install -m 0755 /dev/stdin "$stage_dir/webman-server" <<'SCRIPT'
280+
# 5. 生成标准可移植启动包装脚本 webman-server 与 start.sh
281+
install -m 0755 /dev/stdin "$stage_dir/webman-server" <<'SCRIPT'
246282
#!/usr/bin/env sh
247283
set -eu
248284
@@ -254,7 +290,7 @@ export LD_LIBRARY_PATH="$SCRIPT_DIR:$SCRIPT_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRA
254290
exec "$SCRIPT_DIR/webman-server.bin" "$@"
255291
SCRIPT
256292

257-
install -m 0755 /dev/stdin "$stage_dir/start.sh" <<'SCRIPT'
293+
install -m 0755 /dev/stdin "$stage_dir/start.sh" <<'SCRIPT'
258294
#!/usr/bin/env bash
259295
set -e
260296

‎src/Commands/PackageCommand.php‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
class PackageCommand extends Command
2121
{
2222
private const DEFAULT_BUILDER_IMAGE = 'tinywan/typephp-webman-builder:v0.2.1';
23+
private const DEFAULT_STATIC_BUILDER_IMAGE = 'tinywan/typephp-webman-builder-static:v0.2.1';
2324

2425
protected static $defaultName = 'typephp:package';
2526
protected static $defaultDescription = 'Build Webman project into a Linux x86_64 portable-dir using TypePHP Docker builder.';
@@ -30,6 +31,12 @@ protected function configure(): void
3031
->setName('typephp:package')
3132
->setDescription('Build Webman project into a Linux x86_64 portable-dir using TypePHP Docker builder')
3233
->addOption('image', null, InputOption::VALUE_REQUIRED, 'Docker builder image reference', null)
34+
->addOption(
35+
'static',
36+
's',
37+
InputOption::VALUE_NONE,
38+
'Build fully-static single binary (zero shared library dependencies)',
39+
)
3340
->addOption(
3441
'output-dir',
3542
null,
@@ -62,8 +69,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6269
}
6370
}
6471

72+
$isStatic = (bool) $input->getOption('static');
6573
$optionImage = $input->getOption('image');
66-
$image = $this->resolveBuilderImage(is_string($optionImage) ? $optionImage : null, $pluginConfig);
74+
$image = $this->resolveBuilderImage(
75+
is_string($optionImage) ? $optionImage : null,
76+
$pluginConfig,
77+
$isStatic,
78+
);
6779
$outputDir = trim((string) $input->getOption('output-dir'), '/\\');
6880
$outputName = (string) $input->getOption('output-name');
6981
$force = (bool) $input->getOption('force');
@@ -211,6 +223,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
211223
'TYPEPHP_OUTPUT_NAME=' . $outputName,
212224
'-e',
213225
'TYPEPHP_FORCE=' . ($force ? '1' : '0'),
226+
'-e',
227+
'TYPEPHP_STATIC=' . ($isStatic ? '1' : '0'),
214228
$image,
215229
];
216230

@@ -220,7 +234,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
220234
});
221235

222236
if ($process->isSuccessful()) {
223-
$output->writeln("<info>🎉 Successfully built portable-dir: {$outputDir}/{$outputName}</info>");
237+
$productType = $isStatic ? 'full-static binary' : 'portable-dir';
238+
$output->writeln("<info>🎉 Successfully built {$productType}: {$outputDir}/{$outputName}</info>");
224239
$output->writeln("<info>Run command: cd $outputDir && ./start.sh start</info>");
225240

226241
// 净化 dist/config:把 `Phar::CONST` 类常量替换为等值整数,避免宿主运行时
@@ -267,19 +282,24 @@ private function collectGeneratedHashes(string $basePath, array $targets): array
267282
/**
268283
* @param array<array-key, mixed> $pluginConfig
269284
*/
270-
private function resolveBuilderImage(?string $optionImage, array $pluginConfig): string
285+
private function resolveBuilderImage(?string $optionImage, array $pluginConfig, bool $isStatic = false): string
271286
{
272287
if ($optionImage !== null) {
273288
return $optionImage;
274289
}
275290

276291
$dockerConfig = $pluginConfig['docker'] ?? null;
277292
if (!is_array($dockerConfig)) {
278-
return self::DEFAULT_BUILDER_IMAGE;
293+
return $isStatic ? self::DEFAULT_STATIC_BUILDER_IMAGE : self::DEFAULT_BUILDER_IMAGE;
294+
}
295+
296+
$key = $isStatic ? 'static_image' : 'image';
297+
$configuredImage = $dockerConfig[$key] ?? null;
298+
if (is_string($configuredImage)) {
299+
return $configuredImage;
279300
}
280301

281-
$configuredImage = $dockerConfig['image'] ?? null;
282-
return is_string($configuredImage) ? $configuredImage : self::DEFAULT_BUILDER_IMAGE;
302+
return $isStatic ? self::DEFAULT_STATIC_BUILDER_IMAGE : self::DEFAULT_BUILDER_IMAGE;
283303
}
284304

285305
/**

‎src/config/plugin/tinywan/typephp/app.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'docker' => [
1414
'enabled' => true,
1515
'image' => 'tinywan/typephp-webman-builder:v0.2.1',
16+
'static_image' => 'tinywan/typephp-webman-builder-static:v0.2.1',
1617
],
1718
// 默认输出配置
1819
'build' => [

0 commit comments

Comments
 (0)