Compare commits

..

9 Commits

Author SHA1 Message Date
henderkes
6df778f92f llvm-tools: build host llvm-objcopy/strip/profdata under ZigToolchain
Add an `llvm-tools` target artifact that downloads llvm-project source
matching the version of clang shipped by the active zig install,
builds llvm-objcopy, llvm-strip and llvm-profdata into
PKG_ROOT_PATH/llvm-tools/bin, and exposes them through the same
path/binary/isInstalled static surface as the other artifacts.

A new LlvmToolsCheck doctor item runs when the active toolchain is
ZigToolchain and reports whether the three tools are built, with a
fix that installs the package and runs the build.

PackageBuilder now picks the right tool when the active toolchain is
ZigToolchain:
- extractDebugInfo() honours OBJCOPY from the environment, then falls
  back to llvm-tools' llvm-objcopy under Zig and plain objcopy
  otherwise.
- stripBinary() uses llvm-strip under Zig and plain strip otherwise.

System strip/objcopy refuse zig-produced archives and bitcode
sections, so without this the strip stage breaks LTO builds. Other
toolchains keep using the system binaries.

ApplicationContext::tryGet() wraps the container's get() in a
try/catch and returns null on failure, so PackageBuilder can ask
"which toolchain is active right now" without PHP-DI throwing on
autowirable-but-unconstructable classes.

Depends on v3c/artifact-static-helpers (uses zig::isInstalled()
and zig::binary()).
2026-05-24 21:50:57 +07:00
Jerry Ma
582a88ef60 artifact: use {pkg_root_path} template in rust and go_win extract (#1164) 2026-05-24 22:04:39 +08:00
Jerry Ma
153003b75c imagemagick: --without-gcc-arch (#1162) 2026-05-24 22:04:02 +08:00
Jerry Ma
899555a964 watcher: drop ldflags from compile-only invocation (#1161) 2026-05-24 22:03:38 +08:00
Jerry Ma
891a222c39 revert useless patch (#1160) 2026-05-24 22:03:11 +08:00
henderkes
39beb68024 artifact: use {pkg_root_path} template in rust and go_win extract
Switch the rust and go_win downloaders from baking PKG_ROOT_PATH into
the extract path at download time to the {pkg_root_path} template,
which ArtifactExtractor resolves at extract time. This keeps the path
stable across runs where pkg_root_path differs between download and
extract (e.g. containerised vs host builds).
2026-05-24 20:56:08 +07:00
henderkes
0807e9e253 watcher: drop ldflags from compile-only invocation
The shell invocation runs `$CXX -c` to compile watcher-c.cpp to a .o,
which never links. Passing linker flags to a compile-only step is
either ignored or, with some flags, an error. Drop them.
2026-05-24 20:54:44 +07:00
henderkes
1a779be028 imagemagick: --without-gcc-arch
ax_gcc_archflag has no Zen cpuid pattern and falls back to
-mtune=amdfam10, which under LLVM+LTO emits SSE4a extrq and SIGILLs on
Intel hosts. Disable the implicit --with-gcc-arch so host CPU features
do not bleed into the built binaries.
2026-05-24 20:54:21 +07:00
henderkes
bdfd3eb269 also revert #1122 2026-05-24 20:41:18 +07:00
64 changed files with 412 additions and 1803 deletions

View File

@@ -100,10 +100,9 @@ SPC_TARGET=${GNU_ARCH}-linux-musl
CC=${SPC_DEFAULT_CC}
CXX=${SPC_DEFAULT_CXX}
AR=${SPC_DEFAULT_AR}
RANLIB=${SPC_DEFAULT_RANLIB}
LD=${SPC_DEFAULT_LD}
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections -Wno-unused-command-line-argument"
SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
SPC_DEFAULT_CXXFLAGS="${SPC_DEFAULT_CFLAGS}"
SPC_DEFAULT_LDFLAGS="-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -Wl,-z,noexecstack -Wl,--gc-sections"
; upx executable path
@@ -126,8 +125,6 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE
SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE -fvisibility=hidden -fvisibility-inlines-hidden ${SPC_DEFAULT_CXXFLAGS}"
; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so
SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=""
; EXTRA_LDFLAGS_PROGRAM for `make` php; appended only to SAPI executable links (cli/fpm/cgi/micro/embed). Used by PGO to inject -fprofile-use= without polluting libphp.{a,so}.
SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM=""
; optional, path to openssl conf. This affects where openssl will look for the default CA.
; default on Debian/Alpine: /etc/ssl, default on RHEL: /etc/pki/tls
@@ -143,7 +140,6 @@ SPC_USE_LLVM=system
CC=${SPC_DEFAULT_CC}
CXX=${SPC_DEFAULT_CXX}
AR=${SPC_DEFAULT_AR}
RANLIB=${SPC_DEFAULT_RANLIB}
LD=${SPC_DEFAULT_LD}
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
SPC_DEFAULT_CFLAGS="--target=${MAC_ARCH}-apple-darwin -O3 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
@@ -167,7 +163,5 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -fvis
SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fpie -fvisibility=hidden -fvisibility-inlines-hidden -Werror=unknown-warning-option ${SPC_DEFAULT_CXXFLAGS}"
; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.dylib
SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=""
; EXTRA_LDFLAGS_PROGRAM for `make` php; appended only to SAPI executable links (cli/fpm/cgi/micro/embed). Used by PGO to inject -fprofile-use= without polluting libphp.{a,dylib}.
SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM=""
; minimum compatible macOS version (LLVM vars, availability not guaranteed)
MACOSX_DEPLOYMENT_TARGET=12.0

View File

@@ -1,20 +0,0 @@
ext-fastchart:
type: php-extension
artifact:
source:
type: ghtar
repo: iliaal/fastchart
extract: php-src/ext/fastchart
prefer-stable: true
metadata:
license-files: [LICENSE]
depends:
- freetype
suggests:
- libpng
- libjpeg
- libwebp
php-extension:
os:
- Linux
- Darwin

View File

@@ -1,14 +0,0 @@
ext-fastjson:
type: php-extension
artifact:
source:
type: ghtar
repo: iliaal/fastjson
extract: php-src/ext/fastjson
prefer-stable: true
metadata:
license-files: [LICENSE]
php-extension:
os:
- Linux
- Darwin

View File

@@ -1,6 +0,0 @@
llvm-compiler-rt:
type: target
artifact:
binary: custom
depends:
- zig

View File

@@ -25,6 +25,7 @@ class go_xcaddy
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$pkgroot = PKG_ROOT_PATH;
$name = SystemTarget::getCurrentPlatformString();
$arch = match (explode('-', $name)[1]) {
'x86_64' => 'amd64',
@@ -63,7 +64,7 @@ class go_xcaddy
if ($file_hash !== $hash) {
throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}");
}
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: '{pkg_root_path}/go-xcaddy', verified: true, version: $version);
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version);
}
#[CustomBinaryCheckUpdate('go-xcaddy', [
@@ -108,7 +109,7 @@ class go_xcaddy
'GOROOT' => "{$target_path}",
'GOBIN' => "{$target_path}/bin",
'GOPATH' => "{$target_path}/go",
])->exec('CGO_ENABLED=0 go install github.com/caddyserver/xcaddy/cmd/xcaddy@master');
])->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest');
GlobalEnvManager::addPathIfNotExists("{$target_path}/bin");
}
}

View File

@@ -1,193 +0,0 @@
<?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\Artifact\Downloader\Type\GitHubTokenSetupTrait;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
use StaticPHP\Exception\BuildFailureException;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Runtime\SystemTarget;
/**
* Builds the compiler-rt bits zig ships without — libclang_rt.profile.a (PGO instrumentation)
* and clang_rt.crtbegin.o/crtend.o (__dso_handle for shared libs). Target-arch specific:
* libs land in PKG_ROOT_PATH/zig/lib/{triple}.
* Also builds libclang_rt.cpu_model.a (__cpu_model / __cpu_indicator_init, the libgcc-compatible
* globals that __builtin_cpu_supports()/__builtin_cpu_init() reference) for arches that have it.
*/
class llvm_compiler_rt
{
use GitHubTokenSetupTrait;
#[CustomBinary('llvm-compiler-rt', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$llvmVersion = $this->detectZigLlvmVersion()
?? throw new DownloaderException('llvm-compiler-rt: could not detect bundled clang version from zig cc --version');
$tarball = "compiler-rt-{$llvmVersion}.src.tar.xz";
$url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}";
$tarballPath = DOWNLOAD_PATH . '/' . $tarball;
default_shell()->executeCurlDownload($url, $tarballPath, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry());
return DownloadResult::archive($tarball, ['url' => $url, 'version' => $llvmVersion], extract: '{source_path}/llvm-compiler-rt', verified: false, version: $llvmVersion);
}
#[CustomBinaryCheckUpdate('llvm-compiler-rt', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult
{
$llvmVersion = $this->detectZigLlvmVersion()
?? throw new DownloaderException('llvm-compiler-rt: could not detect bundled clang version from zig cc --version');
return new CheckUpdateResult(
old: $old_version,
new: $llvmVersion,
needUpdate: $old_version === null || $llvmVersion !== $old_version,
);
}
#[AfterBinaryExtract('llvm-compiler-rt', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function postExtract(string $target_path): void
{
$this->buildForTriple($target_path);
}
public function buildForTriple(?string $sourceDir = null, ?string $triple = null): void
{
$sourceDir ??= SOURCE_PATH . '/llvm-compiler-rt';
$triple ??= SystemTarget::getCanonicalTriple();
$libDir = zig::path() . '/lib/' . $triple;
if ($this->isBuilt($libDir)) {
return;
}
if (!is_dir($sourceDir) || !is_dir("{$sourceDir}/lib/profile")) {
throw new BuildFailureException("llvm-compiler-rt: missing source at {$sourceDir} (extraction layout changed?)");
}
f_mkdir($libDir, recursive: true);
$profileLib = "{$libDir}/libclang_rt.profile.a";
$crtBegin = "{$libDir}/clang_rt.crtbegin.o";
$crtEnd = "{$libDir}/clang_rt.crtend.o";
if (!file_exists($profileLib)) {
$this->buildProfileRuntime($sourceDir, $profileLib, $triple);
}
if (!file_exists($crtBegin) || !file_exists($crtEnd)) {
$this->buildCrtObjects($sourceDir, $crtBegin, $crtEnd, $triple);
}
$cpuModelLib = "{$libDir}/libclang_rt.cpu_model.a";
if (self::cpuModelArch($triple) !== null && !file_exists($cpuModelLib)) {
$this->buildCpuModelBuiltins($sourceDir, $cpuModelLib, $triple);
}
}
public function isBuilt(string $libDir): bool
{
return file_exists("{$libDir}/libclang_rt.profile.a")
&& file_exists("{$libDir}/clang_rt.crtbegin.o")
&& file_exists("{$libDir}/clang_rt.crtend.o")
&& (self::cpuModelArch(basename($libDir)) === null || file_exists("{$libDir}/libclang_rt.cpu_model.a"));
}
private function detectZigLlvmVersion(): ?string
{
if (!zig::isInstalled()) {
return null;
}
[$rc, $out] = shell()->execWithResult(escapeshellarg(zig::binary()) . ' cc --version', false);
if ($rc !== 0) {
return null;
}
return preg_match('/clang version (\d+\.\d+\.\d+)/', implode("\n", $out), $m) ? $m[1] : null;
}
private function buildProfileRuntime(string $srcRoot, string $libPath, string $triple): void
{
$profileSrc = "{$srcRoot}/lib/profile";
$profileInc = "{$srcRoot}/include";
// Skip OS-specific sources we can't satisfy without their SDKs.
$skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap'];
$sources = array_filter(
array_merge(glob("{$profileSrc}/*.c") ?: [], glob("{$profileSrc}/*.cpp") ?: []),
fn ($f) => !array_any($skip, fn ($s) => str_contains($f, $s)),
);
$objDir = "{$srcRoot}/obj-profile-{$triple}";
f_mkdir($objDir, recursive: true);
$cflags = "-target {$triple} -c -O2 -fPIC -fvisibility=hidden "
. '-I' . escapeshellarg($profileInc) . ' '
. '-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1';
$srcArgs = implode(' ', array_map('escapeshellarg', $sources));
shell()->cd($objDir)->exec("zig cc {$cflags} {$srcArgs}");
shell()->cd($objDir)->exec('zig ar rcs ' . escapeshellarg($libPath) . ' *.o');
}
private function buildCrtObjects(string $srcRoot, string $crtBegin, string $crtEnd, string $triple): void
{
$beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c";
$endSrc = "{$srcRoot}/lib/builtins/crtend.c";
if (!is_file($beginSrc) || !is_file($endSrc)) {
throw new BuildFailureException("llvm-compiler-rt: crtbegin/crtend source missing under {$srcRoot}/lib/builtins");
}
$cflags = "-target {$triple} -c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY";
foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) {
shell()->exec("zig cc {$cflags} -o " . escapeshellarg($dst) . ' ' . escapeshellarg($src));
}
}
/**
* Build libclang_rt.cpu_model.a, provides
* the globals that __builtin_cpu_supports() reference.
*/
private function buildCpuModelBuiltins(string $srcRoot, string $libPath, string $triple): void
{
$builtins = "{$srcRoot}/lib/builtins";
$family = self::cpuModelArch($triple);
$cpuModelDir = "{$builtins}/cpu_model";
if (is_dir($cpuModelDir)) {
$src = "{$cpuModelDir}/{$family}.c";
$includes = '-I' . escapeshellarg($builtins) . ' -I' . escapeshellarg($cpuModelDir);
} else {
$src = "{$builtins}/cpu_model.c";
$includes = '-I' . escapeshellarg($builtins);
}
if (!is_file($src)) {
throw new BuildFailureException("llvm-compiler-rt: cpu_model source not found for {$triple} under {$builtins}");
}
$objDir = "{$srcRoot}/obj-cpu-model-{$triple}";
f_mkdir($objDir, recursive: true);
$obj = "{$objDir}/cpu_model.o";
$cflags = "-target {$triple} -c -O2 -fPIC {$includes}";
shell()->exec('zig cc ' . $cflags . ' -o ' . escapeshellarg($obj) . ' ' . escapeshellarg($src));
shell()->exec('zig ar rcs ' . escapeshellarg($libPath) . ' ' . escapeshellarg($obj));
}
private static function cpuModelArch(string $triple): ?string
{
$arch = explode('-', $triple)[0];
return match (true) {
in_array($arch, ['x86_64', 'amd64', 'i386', 'i686', 'x86'], true) => 'x86',
in_array($arch, ['aarch64', 'arm64'], true) => 'aarch64',
str_starts_with($arch, 'riscv') => 'riscv',
default => null,
};
}
}

View File

@@ -22,6 +22,30 @@ class llvm_tools
public const array TOOLS = ['llvm-objcopy', 'llvm-strip', 'llvm-profdata'];
/** Install prefix for the locally-built llvm-tools. */
public static function path(): string
{
return PKG_ROOT_PATH . '/llvm-tools';
}
/** Path to a binary under llvm-tools/bin (llvm-objcopy, llvm-strip, llvm-profdata, …). */
public static function binary(string $name = 'llvm-strip'): string
{
return self::path() . '/bin/' . $name;
}
/** True when every required TOOLS binary is present and executable. */
public static function isInstalled(): bool
{
foreach (self::TOOLS as $t) {
$p = self::binary($t);
if (!is_file($p) || !is_executable($p)) {
return false;
}
}
return true;
}
#[CustomBinary('llvm-tools', [
'linux-x86_64',
'linux-aarch64',
@@ -70,8 +94,7 @@ class llvm_tools
public function buildForHost(?string $sourceRoot = null): void
{
$sourceRoot ??= SOURCE_PATH . '/llvm-tools';
$binDir = PKG_ROOT_PATH . '/llvm-tools/bin';
if ($this->allBuilt($binDir)) {
if (self::isInstalled()) {
return;
}
$llvmDir = "{$sourceRoot}/llvm";
@@ -79,7 +102,8 @@ class llvm_tools
throw new BuildFailureException("llvm-tools: missing source at {$llvmDir} (extraction layout changed?)");
}
$buildDir = "{$sourceRoot}/build";
$installDir = PKG_ROOT_PATH . '/llvm-tools';
$installDir = self::path();
$binDir = self::path() . '/bin';
f_mkdir($buildDir, recursive: true);
f_mkdir($binDir, recursive: true);
@@ -119,22 +143,11 @@ class llvm_tools
if (!is_file($built)) {
throw new BuildFailureException("llvm-tools: missing build output {$built}");
}
copy($built, "{$binDir}/{$t}");
chmod("{$binDir}/{$t}", 0755);
copy($built, self::binary($t));
chmod(self::binary($t), 0755);
}
}
public function allBuilt(string $binDir): bool
{
foreach (self::TOOLS as $t) {
$p = "{$binDir}/{$t}";
if (!is_file($p) || !is_executable($p)) {
return false;
}
}
return true;
}
private function detectLlvmVersion(): ?string
{
if (!zig::isInstalled()) {

View File

@@ -78,7 +78,7 @@ class zig
if ($file_hash !== $sha256) {
throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}");
}
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: '{pkg_root_path}/zig', verified: true, version: $latest_version);
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version);
}
#[CustomBinaryCheckUpdate('zig', [
@@ -127,24 +127,26 @@ class zig
break;
}
}
if (!$all_exist) {
$script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh';
$script_content = file_get_contents($script_path);
file_put_contents("{$target_path}/zig-cc", $script_content);
chmod("{$target_path}/zig-cc", 0755);
$script_content = str_replace('zig cc', 'zig c++', $script_content);
file_put_contents("{$target_path}/zig-c++", $script_content);
file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@");
file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@");
file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@");
file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@");
chmod("{$target_path}/zig-c++", 0755);
chmod("{$target_path}/zig-ar", 0755);
chmod("{$target_path}/zig-ld.lld", 0755);
chmod("{$target_path}/zig-ranlib", 0755);
chmod("{$target_path}/zig-objcopy", 0755);
if ($all_exist) {
return;
}
$script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh';
$script_content = file_get_contents($script_path);
file_put_contents("{$target_path}/zig-cc", $script_content);
chmod("{$target_path}/zig-cc", 0755);
$script_content = str_replace('zig cc', 'zig c++', $script_content);
file_put_contents("{$target_path}/zig-c++", $script_content);
file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@");
file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@");
file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@");
file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@");
chmod("{$target_path}/zig-c++", 0755);
chmod("{$target_path}/zig-ar", 0755);
chmod("{$target_path}/zig-ld.lld", 0755);
chmod("{$target_path}/zig-ranlib", 0755);
chmod("{$target_path}/zig-objcopy", 0755);
}
}

View File

@@ -11,7 +11,6 @@ use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\SourcePatcher;
#[Extension('xlswriter')]
@@ -21,20 +20,14 @@ class xlswriter extends PhpExtensionPackage
#[CustomPhpConfigureArg('Linux')]
public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): string
{
$arg = '--with-xlswriter --enable-reader';
$shared = $shared ? '=shared' : '';
$arg = "--with-xlswriter{$shared} --enable-reader";
if ($installer->getLibraryPackage('openssl')) {
$arg .= ' --with-openssl=' . $installer->getLibraryPackage('openssl')->getBuildRootPath();
}
return $arg;
}
#[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-xlswriter')]
#[PatchDescription('Fix Unix build: add -std=gnu17 to CFLAGS to fix build errors on older GCC versions')]
public function patchBeforeUnixMake(): void
{
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -std=gnu17');
}
#[BeforeStage('php', [php::class, 'makeForWindows'], 'ext-xlswriter')]
#[PatchDescription('Fix Windows build: apply win32 patch and add UTF-8 BOM to theme.c')]
public function patchBeforeMakeForWindows(): void
@@ -47,11 +40,4 @@ class xlswriter extends PhpExtensionPackage
file_put_contents($this->getSourceDir() . '/library/libxlsxwriter/src/theme.c', $bom . $content);
}
}
public function getSharedExtensionEnv(): array
{
$parent = parent::getSharedExtensionEnv();
$parent['CFLAGS'] .= ' -std=gnu17';
return $parent;
}
}

View File

@@ -17,9 +17,7 @@ class bzip2
#[PatchBeforeBuild]
public function patchBeforeBuild(LibraryPackage $lib): void
{
// Makefile pins -O2 -fPIC; inject SPC_DEFAULT_CFLAGS
$extra = deduplicate_flags(trim((string) getenv('SPC_DEFAULT_CFLAGS')) . ' -fPIC -Wall');
FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', "CFLAGS={$extra}");
FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall');
}
#[BuildFor('Windows')]

View File

@@ -18,11 +18,9 @@ class fastlz
{
$cc = getenv('CC') ?: 'cc';
$ar = getenv('AR') ?: 'ar';
$extra = trim((string) getenv('SPC_DEFAULT_CFLAGS'));
$extra = $extra !== '' ? $extra . ' -fPIC' : '-O3 -fPIC';
shell()->cd($lib->getSourceDir())->initializeEnv($lib)
->exec("{$cc} -c {$extra} fastlz.c -o fastlz.o")
->exec("{$cc} -c -O3 -fPIC fastlz.c -o fastlz.o")
->exec("{$ar} rcs libfastlz.a fastlz.o");
// Copy header file

View File

@@ -24,12 +24,9 @@ class icu
#[BuildFor('Linux')]
public function buildLinux(LibraryPackage $lib, ToolchainInterface $toolchain, PackageBuilder $builder): void
{
// runConfigureICU bakes CXXFLAGS/LDFLAGS, apply user flags too
$userCxxFlags = trim((string) getenv('SPC_DEFAULT_CXXFLAGS'));
$userLdFlags = trim((string) getenv('SPC_DEFAULT_LDFLAGS'));
$cppflags = 'CPPFLAGS="-DU_CHARSET_IS_UTF8=1 -DU_USING_ICU_NAMESPACE=1 -DU_STATIC_IMPLEMENTATION=1 -DPIC -fPIC"';
$cxxflags = "CXXFLAGS=\"-std=c++17 -DPIC -fPIC -fno-ident {$userCxxFlags}\"";
$ldflags = $toolchain->isStatic() ? "LDFLAGS=\"-static {$userLdFlags}\"" : "LDFLAGS=\"{$userLdFlags}\"";
$cxxflags = 'CXXFLAGS="-std=c++17 -DPIC -fPIC -fno-ident"';
$ldflags = $toolchain->isStatic() ? 'LDFLAGS="-static"' : '';
shell()->cd($lib->getSourceDir() . '/source')->initializeEnv($lib)
->exec(
"{$cppflags} {$cxxflags} {$ldflags} " .

View File

@@ -17,9 +17,7 @@ class jbig
#[PatchBeforeBuild]
public function patchBeforeBuild(LibraryPackage $lib): void
{
$extra = trim((string) getenv('SPC_DEFAULT_CFLAGS'));
$cflags = ($extra !== '' ? $extra : '-O2') . ' -W -Wno-unused-result -fPIC';
FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', "CFLAGS = {$cflags}");
FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', 'CFLAGS = -O2 -W -Wno-unused-result -fPIC');
}
#[BuildFor('Darwin')]

View File

@@ -27,7 +27,7 @@ class krb5
$resolved = array_keys($installer->getResolvedPackages());
$spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]);
$config = $spc->getPackageDepsConfig($lib->getName(), $resolved);
$config = $spc->getPackageDepsConfig($lib->getName(), $resolved, include_suggests: true);
$extraEnv = [
'CFLAGS' => '-fcommon',
'LIBS' => $config['libs'],

View File

@@ -9,10 +9,8 @@ use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixCMakeExecutor;
use StaticPHP\Runtime\Executor\WindowsCMakeExecutor;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
use StaticPHP\Util\System\UnixUtil;
#[Library('libaom')]
class libaom extends LibraryPackage
@@ -41,23 +39,9 @@ class libaom extends LibraryPackage
$new = trim($extra . ' -D_GNU_SOURCE');
f_putenv("SPC_COMPILER_EXTRA={$new}");
}
$targetCpu = SystemTarget::getTargetArch();
if (str_starts_with($targetCpu, 'aarch')) {
$targetCpu = str_replace('aarch', 'arm', $targetCpu);
}
if (!UnixUtil::findCommand('nasm') && !UnixUtil::findCommand('yasm')) {
$targetCpu = 'generic';
}
UnixCMakeExecutor::create($this)
->setBuildDir("{$this->getSourceDir()}/builddir")
->addConfigureArgs(
"-DAOM_TARGET_CPU={$targetCpu}",
'-DCONFIG_RUNTIME_CPU_DETECT=1',
'-DENABLE_EXAMPLES=OFF',
'-DENABLE_TESTS=OFF',
'-DENABLE_TOOLS=OFF',
'-DENABLE_DOCS=OFF',
)
->addConfigureArgs('-DAOM_TARGET_CPU=generic')
->build();
f_putenv("SPC_COMPILER_EXTRA={$extra}");
$this->patchPkgconfPrefix(['aom.pc']);

View File

@@ -8,7 +8,6 @@ use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Runtime\SystemTarget;
#[Library('libffi')]
class libffi extends LibraryPackage
@@ -29,7 +28,7 @@ class libffi extends LibraryPackage
#[BuildFor('Darwin')]
public function buildDarwin(): void
{
$arch = SystemTarget::getTargetArch();
$arch = getenv('SPC_ARCH');
UnixAutoconfExecutor::create($this)
->configure(
"--host={$arch}-apple-darwin",

View File

@@ -24,17 +24,6 @@ class libheif
'list(APPEND REQUIRES_PRIVATE "libbrotlidec")' . "\n" . ' list(APPEND REQUIRES_PRIVATE "libbrotlienc")'
);
}
// libheif 1.22+ ships a C-incompatible header: `struct heif_bad_pixel`
$heif_properties = $lib->getSourceDir() . '/libheif/api/libheif/heif_properties.h';
if (file_exists($heif_properties)
&& str_contains(file_get_contents($heif_properties), 'struct heif_bad_pixel { uint32_t row; uint32_t column; };')
) {
FileSystem::replaceFileStr(
$heif_properties,
'struct heif_bad_pixel { uint32_t row; uint32_t column; };',
'typedef struct heif_bad_pixel { uint32_t row; uint32_t column; } heif_bad_pixel;'
);
}
}
#[BuildFor('Darwin')]

View File

@@ -17,17 +17,10 @@ use StaticPHP\Util\FileSystem;
class liblz4
{
#[PatchBeforeBuild]
#[PatchDescription('Compile lib sources individually so -flto -c with multiple inputs works under zig-cc/clang')]
#[PatchDescription('Fix Makefile install target for static liblz4')]
public function patchBeforeBuild(LibraryPackage $lib): void
{
// `-flto -c` with multiple input files only writes a .o for the
// first source — the others are silently dropped, leaving liblz4.a with a
// single object. Compile each source individually so all .o files exist.
FileSystem::replaceFileStr(
$lib->getSourceDir() . '/lib/Makefile',
"liblz4.a: \$(SRCFILES)\nifeq (\$(BUILD_STATIC),yes) # can be disabled on command line\n\t@echo compiling static library\n\t\$(COMPILE.c) \$^\n\t\$(AR) rcs \$@ *.o\nendif",
"liblz4.a: \$(SRCFILES:.c=.o)\nifeq (\$(BUILD_STATIC),yes) # can be disabled on command line\n\t@echo compiling static library\n\t\$(AR) rcs \$@ \$^\nendif"
);
FileSystem::replaceFileStr($lib->getSourceDir() . '/programs/Makefile', 'install: lz4', "install: lz4\n\ninstallewfwef: lz4");
}
#[BuildFor('Windows')]

View File

@@ -9,7 +9,6 @@ use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Runtime\Executor\WindowsCMakeExecutor;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
#[Library('libpng')]
@@ -25,7 +24,7 @@ class libpng
];
// Enable architecture-specific optimizations
match (SystemTarget::getTargetArch()) {
match (getenv('SPC_ARCH')) {
'x86_64' => $args[] = '--enable-intel-sse',
'aarch64' => $args[] = '--enable-arm-neon',
default => null,

View File

@@ -6,8 +6,6 @@ namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\Package\PatchBeforeBuild;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
@@ -18,24 +16,6 @@ use StaticPHP\Util\FileSystem;
#[Library('ncursesw')]
class ncurses
{
#[PatchBeforeBuild]
#[PatchDescription('Filter clang/zig "N warning(s) generated." line out of MKlib_gen.sh preprocessor pipe')]
public function patchBeforeBuild(LibraryPackage $lib): void
{
// MKlib_gen.sh feeds the C preprocessor's stdout through a sed/awk
// pipeline into lib_gen.c. zig-cc/clang emits "N warning(s) generated."
// on stdout (not stderr), and that line ends up as invalid C in the
// generated source. Filter it out of the pipe before sed sees it.
$mklibGen = $lib->getSourceDir() . '/ncurses/base/MKlib_gen.sh';
if (is_file($mklibGen) && !str_contains((string) file_get_contents($mklibGen), "| grep -v ' generated")) {
FileSystem::replaceFileStr(
$mklibGen,
'$preprocessor $TMP 2>/dev/null \\',
"\$preprocessor \$TMP 2>/dev/null \\\n| grep -v ' generated\\.\$' \\",
);
}
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(LibraryPackage $package, ToolchainInterface $toolchain): void
@@ -65,7 +45,6 @@ class ncurses
'--without-tests',
'--without-dlsym',
'--without-debug',
'--disable-stripping',
'--enable-symlinks',
"--with-terminfo-dirs={$terminfo_dirs}",
"--bindir={$package->getBinDir()}",

View File

@@ -76,7 +76,7 @@ class openssl
public function buildForDarwin(LibraryPackage $pkg): void
{
$zlib_libs = $pkg->getInstaller()->getLibraryPackage('zlib')->getStaticLibFiles();
$arch = SystemTarget::getTargetArch();
$arch = getenv('SPC_ARCH');
shell()->cd($pkg->getSourceDir())->initializeEnv($pkg)
->exec(
@@ -95,7 +95,12 @@ class openssl
#[BuildFor('Linux')]
public function build(LibraryPackage $lib): void
{
$arch = SystemTarget::getTargetArch();
$arch = getenv('SPC_ARCH');
$env = "CC='" . getenv('CC') . ' -idirafter ' . BUILD_INCLUDE_PATH .
' -idirafter /usr/include/ ' .
' -idirafter /usr/include/' . getenv('SPC_ARCH') . '-linux-gnu/ ' .
"' ";
$ex_lib = trim($lib->getInstaller()->getLibraryPackage('zlib')->getStaticLibFiles()) . ' -ldl -pthread';
$zlib_extra =
@@ -106,15 +111,9 @@ class openssl
$openssl_dir ??= LinuxUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl';
$ex_lib = trim($ex_lib);
// anything we want included (PGO -fprofile-*, LTO, custom hardening)
// has to be appended on the command line *after* the target name.
$userCFlags = trim((string) getenv('SPC_DEFAULT_CFLAGS'));
$userLdFlags = trim((string) getenv('SPC_DEFAULT_LDFLAGS'));
$userExtra = trim($userCFlags . ' ' . $userLdFlags);
shell()->cd($lib->getSourceDir())->initializeEnv($lib)
->exec(
'./Configure no-shared zlib ' .
"{$env} ./Configure no-shared zlib " .
"--prefix={$lib->getBuildRootPath()} " .
"--libdir={$lib->getLibDir()} " .
"--openssldir={$openssl_dir} " .
@@ -122,8 +121,7 @@ class openssl
'enable-pie ' .
'no-legacy ' .
'no-tests ' .
"linux-{$arch} " .
$userExtra
"linux-{$arch}"
)
->exec('make clean')
->exec("make -j{$lib->getBuilder()->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"")

View File

@@ -58,7 +58,7 @@ class postgresql extends LibraryPackage
public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void
{
$spc_config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]);
$config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()));
$config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), include_suggests: $builder->getOption('with-suggests', false));
$env_vars = [
'CFLAGS' => $config['cflags'] . ' -std=c17',

View File

@@ -20,11 +20,6 @@ class qdbm
{
$ac = UnixAutoconfExecutor::create($lib)->configure();
FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/MYLIBS = libqdbm.a.*/m', 'MYLIBS = libqdbm.a');
// Makefile pins -O3, replace with SPC_DEFAULT_CFLAGS
$extra = trim((string) getenv('SPC_DEFAULT_CFLAGS'));
if ($extra !== '') {
FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/^CFLAGS = .*$/m', "CFLAGS = -Wall {$extra}");
}
$ac->make(SystemTarget::getTargetOS() === 'Darwin' ? 'mac' : '');
$lib->patchPkgconfPrefix(['qdbm.pc']);
}

View File

@@ -27,20 +27,7 @@ class unixodbc extends LibraryPackage
'Linux' => '/etc',
default => throw new WrongUsageException("Unsupported OS: {$os}"),
};
// unixodbc bundles libltdl; libltdl is incompatible with -flto
// (https://bugs.gentoo.org/532672).
$stripLto = static fn (string $s): string => clean_spaces((string) preg_replace('/(^|\s)-flto(=\S+)?(?=\s|$)/', ' ', $s));
$cflags = $stripLto((string) getenv('SPC_DEFAULT_CFLAGS'));
$cxxflags = $stripLto((string) getenv('SPC_DEFAULT_CXXFLAGS'));
$ldflags = $stripLto((string) getenv('SPC_DEFAULT_LDFLAGS'));
$make = UnixAutoconfExecutor::create($this)
->setEnv([
'CFLAGS' => $cflags,
'CXXFLAGS' => $cxxflags,
'LDFLAGS' => $ldflags,
])
UnixAutoconfExecutor::create($this)
->configure(
'--disable-debug',
'--disable-dependency-tracking',
@@ -48,15 +35,8 @@ class unixodbc extends LibraryPackage
'--with-included-ltdl',
"--sysconfdir={$sysconf_selector}",
'--enable-gui=no',
);
// The exe/ subdirectory builds odbcinst/iusql/etc, turn it into a no-op
file_put_contents(
"{$this->getSourceDir()}/exe/Makefile",
".PHONY: all install clean check distclean install-strip\nall install clean check distclean install-strip:\n\t@true\n",
);
$make->make();
)
->make();
$this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']);
$this->patchLaDependencyPrefix();
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Package\Target;
use Package\Target\php\frankenphp;
use Package\Target\php\pgo;
use Package\Target\php\unix;
use Package\Target\php\windows;
use StaticPHP\Artifact\ArtifactCache;
@@ -49,7 +48,6 @@ class php extends TargetPackage
use unix;
use windows;
use frankenphp;
use pgo;
/** @var string[] Supported major PHP versions */
public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'];
@@ -270,14 +268,12 @@ class php extends TargetPackage
}
}
// linux does not support loading shared libraries when target is pure static
if ($package->getName() === 'php-embed') {
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') {
throw new WrongUsageException(
'Linux does not support loading shared libraries when linking libc statically. ' .
'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.'
);
}
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') {
throw new WrongUsageException(
'Linux does not support loading shared libraries when linking libc statically. ' .
'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.'
);
}
}
@@ -336,7 +332,7 @@ class php extends TargetPackage
logger()->info("Adding hardcoded INI [{$source_name} = {$ini_value}]");
}
if (!empty($custom_ini)) {
ApplicationContext::invoke([SourcePatcher::class, 'patchHardcodedINI'], ['php_source_dir' => $package->getSourceDir(), 'ini' => $custom_ini]);
ApplicationContext::invoke([SourcePatcher::class, 'patchHardcodedINI'], [$package->getSourceDir(), $custom_ini]);
}
// Patch StaticPHP version

View File

@@ -73,7 +73,10 @@ trait frankenphp
$staticFlags = '';
}
$config = new SPCConfigUtil()->config(['frankenphp']);
$resolved = array_keys($installer->getResolvedPackages());
// remove self from deps
$resolved = array_filter($resolved, fn ($pkg_name) => $pkg_name !== $package->getName());
$config = new SPCConfigUtil()->config($resolved);
$cflags = "{$package->getLibExtraCFlags()} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . " -DFRANKENPHP_VERSION={$frankenphp_version}";
$libs = $config['libs'];
@@ -85,13 +88,10 @@ trait frankenphp
$libs .= ' -lgcov';
}
$extraLdProgram = clean_spaces((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM'));
$env = [
'CGO_ENABLED' => '1',
'CGO_CFLAGS' => clean_spaces($cflags),
'CGO_LDFLAGS' => clean_spaces("{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs} {$extraLdProgram}"),
// cgo strips flags not on its safe allowlist; widen it
'CGO_LDFLAGS_ALLOW' => '-Wl,-z,.*|-Wl,--.*|-flto.*|-fprofile-.*',
'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}",
'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' .
'-ldflags \"-linkmode=external ' . $extLdFlags . ' ' .
'-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' .
@@ -101,12 +101,10 @@ trait frankenphp
"-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}",
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
$pgo = file_exists("{$source_dir}/caddy/frankenphp/default.pgo") ? "--pgo {$source_dir}/caddy/frankenphp/default.pgo " : '';
InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with xcaddy'));
shell()->cd(BUILD_LIB_PATH)
->setEnv($env)
->exec('go clean -cache') // fix stale include evaluation
->exec("xcaddy build --output frankenphp {$pgo}{$xcaddy_modules}");
->exec("xcaddy build --output frankenphp {$xcaddy_modules}");
$builder->deployBinary(BUILD_LIB_PATH . '/frankenphp', BUILD_BIN_PATH . '/frankenphp');
$package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp');

View File

@@ -1,127 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Target\php;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\ConditionalOn;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Util\Pgo\PgoContext;
use StaticPHP\Util\SourcePatcher;
trait pgo
{
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
#[PatchDescription('Inject __llvm_profile_write_file() flush at php/frankenphp shutdown for instrumented builds')]
public function pgoApplyShutdownPatches(PgoContext $pgo): void
{
if (!$pgo->isInstrument() && !$pgo->isCsInstrument()) {
return;
}
foreach (PgoContext::SHUTDOWN_PATCHES as $dir => $patch) {
$cwd = SOURCE_PATH . '/' . $dir;
if (!is_dir($cwd)) {
continue;
}
if (!SourcePatcher::patchFile($patch, $cwd)) {
throw new WrongUsageException("PGO --phase=instrument: patch {$patch} failed to apply in {$cwd}");
}
logger()->info("PGO --phase=instrument: applied {$patch}");
}
}
#[ConditionalOn(PgoContext::class)]
#[AfterStage('php', [self::class, 'configureForUnix'], 'php')]
#[PatchDescription('Patch libtool to passthrough -fcs-profile-* for context-sensitive PGO')]
public function pgoPatchLibtoolForCsInstrument(PgoContext $pgo): void
{
if (!$pgo->isCsInstrument()) {
return;
}
$libtool = SOURCE_PATH . '/php-src/libtool';
if (!is_file($libtool)) {
return;
}
$contents = (string) file_get_contents($libtool);
if (str_contains($contents, '-fcs-profile-*')) {
return;
}
$patched = str_replace('-fprofile-*|-F*', '-fprofile-*|-fcs-profile-*|-F*', $contents);
if ($patched === $contents) {
logger()->warning('PGO --phase=cs-instrument: could not patch libtool for -fcs-profile-* passthrough');
return;
}
file_put_contents($libtool, $patched);
logger()->info('PGO --phase=cs-instrument: patched libtool for -fcs-profile-* passthrough');
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'configureForUnix'], 'php')]
public function pgoApplyConfigureFlags(PgoContext $pgo): void
{
$sapis = $pgo->trainableSapis();
if ($sapis === []) {
return;
}
$pgo->applyEnvFor($sapis[0]);
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'makeCliForUnix'], 'php')]
public function pgoBeforeMakeCli(PgoContext $pgo, TargetPackage $package): void
{
$this->pgoBeforeSapiMake($pgo, $package, 'cli');
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'makeCgiForUnix'], 'php')]
public function pgoBeforeMakeCgi(PgoContext $pgo, TargetPackage $package): void
{
$this->pgoBeforeSapiMake($pgo, $package, 'cgi');
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'makeFpmForUnix'], 'php')]
public function pgoBeforeMakeFpm(PgoContext $pgo, TargetPackage $package): void
{
$this->pgoBeforeSapiMake($pgo, $package, 'fpm');
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'makeMicroForUnix'], 'php')]
public function pgoBeforeMakeMicro(PgoContext $pgo, TargetPackage $package): void
{
$this->pgoBeforeSapiMake($pgo, $package, 'micro');
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'makeEmbedForUnix'], 'php')]
public function pgoBeforeMakeEmbed(PgoContext $pgo, TargetPackage $package): void
{
$this->pgoBeforeSapiMake($pgo, $package, 'embed');
}
#[ConditionalOn(PgoContext::class)]
#[BeforeStage('php', [self::class, 'buildFrankenphpForUnix'], 'php')]
public function pgoBeforeBuildFrankenphp(PgoContext $pgo): void
{
$pgo->applyEnvFor('frankenphp');
logger()->info("PGO {$pgo->mode}: applying flags for frankenphp");
}
private function pgoBeforeSapiMake(PgoContext $pgo, TargetPackage $package, string $sapi): void
{
$resolved = $pgo->resolveSapi($sapi);
if (!in_array($resolved, $pgo->trainableSapis(), true)) {
return;
}
shell()->cd($package->getSourceDir())->exec('make clean');
$pgo->applyEnvFor($sapi);
logger()->info("PGO {$pgo->mode}: applying flags for {$sapi}");
}
}

View File

@@ -20,7 +20,6 @@ use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Toolchain\ZigToolchain;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
@@ -42,15 +41,6 @@ trait unix
// php-src patches from micro (reads SPC_MICRO_PATCHES env var)
SourcePatcher::patchPhpSrc();
$microFileinfo = "{$package->getSourceDir()}/sapi/micro/php_micro_fileinfo.c";
if (is_file($microFileinfo) && !str_contains((string) file_get_contents($microFileinfo), 'Elf32_Shdr')) {
FileSystem::replaceFileStr(
$microFileinfo,
'typedef Elf32_Ehdr Elf_Ehdr;',
'typedef Elf32_Ehdr Elf_Ehdr; typedef Elf32_Shdr Elf_Shdr;',
);
}
// patch configure.ac for musl and musl-toolchain
$musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl';
FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac');
@@ -69,7 +59,7 @@ trait unix
}
if (self::getPHPVersionID() >= 80300 && self::getPHPVersionID() < 80400) {
SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', SOURCE_PATH . '/php-src');
SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', $this->getSourceDir());
}
}
@@ -102,10 +92,12 @@ trait unix
$args = [];
$version_id = self::getPHPVersionID();
// disable undefined behavior sanitizer for zig, trips up on lua minijit and opcache-jit
if (SystemTarget::getTargetOS() === 'Linux' && ToolchainManager::getToolchainClass() === ZigToolchain::class) {
$compiler_extra = getenv('SPC_COMPILER_EXTRA') ?: '';
GlobalEnvManager::putenv('SPC_COMPILER_EXTRA=' . trim($compiler_extra . ' -fno-sanitize=undefined'));
// disable undefined behavior sanitizer when opcache JIT is enabled (Linux only)
if (SystemTarget::getTargetOS() === 'Linux' && !$package->getBuildOption('disable-opcache-jit', false)) {
if ($version_id >= 80500 || $installer->isPackageResolved('ext-opcache')) {
$compiler_extra = getenv('SPC_COMPILER_EXTRA') ?: '';
GlobalEnvManager::putenv('SPC_COMPILER_EXTRA=' . trim($compiler_extra . ' -fno-sanitize=undefined'));
}
}
// PHP JSON extension is built-in since PHP 8.0
if ($version_id < 80000) {
@@ -137,10 +129,6 @@ trait unix
$args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi';
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
$args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed';
// Cross-compile: pass --host so configure picks the correct fiber asm file and host_cpu logic
if ($host_triple = SystemTarget::getAutoconfHostTriple()) {
$args[] = "--host={$host_triple}";
}
$args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null;
$args = implode(' ', array_filter($args));
@@ -153,7 +141,7 @@ trait unix
$this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([
'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'CPPFLAGS' => "-I{$package->getIncludeDir()}",
'LDFLAGS' => "-L{$package->getLibDir()}",
'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
'LIBS' => $vars['EXTRA_LIBS'] ?? '',
])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir());
}
@@ -180,7 +168,7 @@ trait unix
#[BeforeStage('php', [self::class, 'makeForUnix'], 'php')]
#[PatchDescription('Patch Makefile to fix //lib path for Linux builds')]
#[PatchDescription('Under CI: patch BUILD_CC to system cc — zig-cc-built minilua segfaults there for reasons we cannot reproduce locally')]
#[PatchDescription('Patch BUILD_CC to use system cc instead of zig-cc (prevents minilua crash)')]
public function tryPatchMakefileUnix(TargetPackage $package, ToolchainInterface $toolchain): void
{
if (SystemTarget::getTargetOS() !== 'Linux') {
@@ -190,8 +178,7 @@ trait unix
// replace //lib with /lib in Makefile
shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile');
// CI escape hatch: in CI, zig-cc-built minilua segfaults
if ($toolchain instanceof ZigToolchain && getenv('CI')) {
if ($toolchain instanceof ZigToolchain) {
$makefile = "{$package->getSourceDir()}/Makefile";
FileSystem::replaceFileRegex($makefile, '/^BUILD_CC\s*=\s*zig-cc\s*$/m', 'BUILD_CC = cc');
}
@@ -242,7 +229,6 @@ trait unix
#[Stage]
public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$start = microtime(true);
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli'));
$concurrency = $builder->concurrency;
$vars = $this->makeVars($installer);
@@ -253,13 +239,11 @@ trait unix
$builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php');
$package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php');
InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-cli'), true, $start);
}
#[Stage]
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$start = microtime(true);
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
$concurrency = $builder->concurrency;
$vars = $this->makeVars($installer);
@@ -270,13 +254,11 @@ trait unix
$builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi');
$package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi');
InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-cgi'), true, $start);
}
#[Stage]
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$start = microtime(true);
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
$concurrency = $builder->concurrency;
$vars = $this->makeVars($installer);
@@ -287,58 +269,43 @@ trait unix
$builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm');
$package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm');
InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-fpm'), true, $start);
}
#[Stage]
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$start = microtime(true);
$phar_patched = false;
try {
if ($installer->isPackageResolved('ext-phar')) {
$phar_patched = true;
SourcePatcher::patchMicroPhar(self::getPHPVersionID());
}
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro'));
// apply --with-micro-fake-cli option
$vars = $this->makeVars($installer);
$vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : '';
$makeArgs = $this->makeVarsToArgs($vars);
// build
shell()->cd($package->getSourceDir())
->setEnv($vars)
->exec("make -j{$builder->concurrency} {$makeArgs} micro");
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro'));
// apply --with-micro-fake-cli option
$vars = $this->makeVars($installer);
$vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : '';
$makeArgs = $this->makeVarsToArgs($vars);
// build
shell()->cd($package->getSourceDir())
->setEnv($vars)
->exec("make -j{$builder->concurrency} {$makeArgs} micro");
$dst = BUILD_BIN_PATH . '/micro.sfx';
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst);
// patch after UPX-ed micro.sfx (Linux only)
if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) {
// cut binary with readelf to remove UPX extra segment
[$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);
InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-micro'), true, $start);
} finally {
if ($phar_patched) {
SourcePatcher::unpatchMicroPhar();
$dst = BUILD_BIN_PATH . '/micro.sfx';
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst);
// patch after UPX-ed micro.sfx (Linux only)
if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) {
// cut binary with readelf to remove UPX extra segment
[$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);
}
#[Stage]
public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$start = microtime(true);
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed'));
$shared_exts = array_filter(
$installer->getResolvedPackages(),
@@ -352,43 +319,22 @@ trait unix
$root = BUILD_ROOT_PATH;
$sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i';
$vars = $this->makeVars($installer);
$makeArgs = $this->makeVarsToArgs($vars);
shell()->cd($package->getSourceDir())
->setEnv($vars)
->setEnv($this->makeVars($installer))
->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec("make -j{$builder->concurrency} {$makeArgs} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs");
// install-modules deref'd libtool's `$ext.so → $ext-X.so` symlink for each built-with-php ext; restore them.
$release = null;
if (preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m)) {
$release = $m[1];
foreach ($shared_exts as $ext) {
$name = $ext->getExtensionName();
$u = BUILD_MODULES_PATH . "/{$name}.so";
$v = BUILD_MODULES_PATH . "/{$name}-{$release}.so";
if (file_exists($v) && file_exists($u) && !is_link($u)) {
unlink($u);
symlink(basename($v), $u);
}
}
}
->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs");
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared -------------
// INSTALL_IT for embed copies through libtool's symlink, leaving only unversioned libphp.{so,dylib} — rename and symlink back so shared exts can `-lphp`. (static libphp.a is never versioned, even with -release.)
// process libphp.so for shared embed
$suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so';
$libphp_so = "{$package->getLibDir()}/libphp.{$suffix}";
if (file_exists($libphp_so)) {
if ($release !== null) {
$versioned = "{$package->getLibDir()}/libphp-{$release}.{$suffix}";
if (file_exists($versioned)) {
@unlink($versioned);
}
rename($libphp_so, $versioned);
symlink(basename($versioned), $libphp_so);
$libphp_so = $versioned;
// rename libphp.so if -release is set
if (SystemTarget::getTargetOS() === 'Linux') {
$this->processLibphpSoFile($libphp_so, $installer);
}
// deploy
$builder->deployBinary($libphp_so, $libphp_so, false);
$package->setOutput('Library path for embed SAPI', $libphp_so);
}
@@ -397,9 +343,6 @@ trait unix
$increment_files = $diff->getChangedFiles();
$files = [];
foreach ($increment_files as $increment_file) {
if (is_link($increment_file) || !file_exists($increment_file)) {
continue;
}
$builder->deployBinary($increment_file, $increment_file, false);
$files[] = basename($increment_file);
}
@@ -407,11 +350,6 @@ trait unix
$package->setOutput('Built shared extensions', implode(', ', $files));
}
// phpize needs prefix patched whether libphp is .a or .so
$package->runStage([$this, 'patchUnixEmbedScripts']);
InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-embed'), true, $start);
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
// process libphp.a for static embed (only when present)
@@ -421,6 +359,9 @@ trait unix
shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}");
UnixUtil::exportDynamicSymbols($libphp_a);
}
// deploy embed php scripts
$package->runStage([$this, 'patchUnixEmbedScripts']);
}
#[Stage]
@@ -453,15 +394,8 @@ trait unix
try {
logger()->debug('Building shared extensions...');
foreach ($shared_extensions as $extension) {
$ext_start = microtime(true);
InteractiveTerm::setMessage('Building shared extension: ' . ConsoleColor::yellow($extension->getName()));
try {
$extension->buildShared();
} catch (\Throwable $e) {
InteractiveTerm::error('Building shared extension failed: ' . ConsoleColor::red($extension->getName()));
throw $e;
}
InteractiveTerm::success('Built shared extension: ' . ConsoleColor::green($extension->getName()), true, $ext_start);
InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName()));
$extension->buildShared();
}
} finally {
// restore php-config
@@ -553,8 +487,6 @@ trait unix
$package->runStage([$this, 'makeForUnix']);
}
// shared extensions build before frankenphp so their undefined references are
// collected into libphp's exported dynamic-symbol list.
$package->runStage([$this, 'unixBuildSharedExt']);
}
@@ -667,7 +599,7 @@ trait unix
copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c');
copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php');
$config = new SPCConfigUtil()->config(['php']);
$config = new SPCConfigUtil()->config($installer->getAvailableResolvedPackageNames());
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
if ($toolchain->isStatic()) {
$lens .= ' -static';
@@ -748,37 +680,96 @@ trait unix
return $php;
}
/**
* Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS.
*/
private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void
{
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
$libDir = BUILD_LIB_PATH;
$modulesDir = BUILD_MODULES_PATH;
$realLibName = 'libphp.so';
$cwd = getcwd();
if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) {
$release = $matches[1];
$realLibName = "libphp-{$release}.so";
$libphpRelease = "{$libDir}/{$realLibName}";
if (!file_exists($libphpRelease) && file_exists($libphpSo)) {
rename($libphpSo, $libphpRelease);
}
if (file_exists($libphpRelease)) {
chdir($libDir);
if (file_exists($libphpSo)) {
unlink($libphpSo);
}
symlink($realLibName, 'libphp.so');
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg($realLibName),
escapeshellarg($libphpRelease)
));
}
if (is_dir($modulesDir)) {
chdir($modulesDir);
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) {
if (!$ext->isBuildShared()) {
continue;
}
$name = $ext->getName();
$versioned = "{$name}-{$release}.so";
$unversioned = "{$name}.so";
$src = "{$modulesDir}/{$versioned}";
$dst = "{$modulesDir}/{$unversioned}";
if (is_file($src)) {
rename($src, $dst);
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg($unversioned),
escapeshellarg($dst)
));
}
}
}
chdir($cwd);
}
$target = "{$libDir}/{$realLibName}";
if (file_exists($target)) {
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
$output = implode("\n", $output);
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
$currentSoname = $sonameMatch[1];
if ($currentSoname !== basename($target)) {
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg(basename($target)),
escapeshellarg($target)
));
}
}
}
}
/**
* Make environment variables for php make.
* This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking.
*/
private function makeVars(PackageInstaller $installer): array
{
$config = new SPCConfigUtil(['libs_only_deps' => true])->config(['php']);
$config = new SPCConfigUtil(['libs_only_deps' => true])->config($installer->getAvailableResolvedPackageNames());
$static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : '';
$pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : '';
$lib = BUILD_LIB_PATH;
// Append SPC_EXTRA_LIBS to libs for dynamic linking support (e.g., X11)
$extra_libs = getenv('SPC_EXTRA_LIBS') ?: '';
$libs = trim($config['libs'] . ' ' . $extra_libs);
// libtool input (libphp.la). `make EXTRA_LDFLAGS=…` cmdline overrides fully replace the Makefile value, so re-include $config['ldflags'] for -L paths.
$extra_ldflags = clean_spaces($config['ldflags'] . ' ' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'));
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared'
&& !str_contains($extra_ldflags, '-avoid-version')
&& !preg_match('/-release\s+\S+/', $extra_ldflags)) {
$extra_ldflags = trim($extra_ldflags . ' -avoid-version -module');
}
$extra_ldflags_program_env = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM') ?: '';
$extra_ldflags_program = clean_spaces("-L{$lib} {$static} {$pie} {$extra_ldflags_program_env}");
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_CXXFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS'),
'EXTRA_LDFLAGS' => $extra_ldflags,
'EXTRA_LDFLAGS_PROGRAM' => $extra_ldflags_program,
'EXTRA_LDFLAGS_PROGRAM' => deduplicate_flags(getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . " {$config['ldflags']} {$static} {$pie}"),
'EXTRA_LDFLAGS' => $config['ldflags'],
'EXTRA_LIBS' => $libs,
]);
}

View File

@@ -27,18 +27,12 @@ class Artifact
/** @var null|callable Bind custom source fetcher callback */
protected mixed $custom_source_callback = null;
/** @var null|string Display label describing where the custom source callback came from */
protected ?string $custom_source_callback_origin = null;
/** @var null|callable Bind custom source check-update callback */
protected mixed $custom_source_check_update_callback = null;
/** @var array<string, callable> Bind custom binary fetcher callbacks */
protected mixed $custom_binary_callbacks = [];
/** @var array<string, string> Display label per platform describing where the custom binary callback came from */
protected array $custom_binary_callback_origins = [];
/** @var array<string, callable> Bind custom binary check-update callbacks */
protected array $custom_binary_check_update_callbacks = [];
@@ -291,19 +285,15 @@ class Artifact
* Get source extraction directory.
*
* Rules:
* 1. If cache_type is 'local': use the absolute dirname recorded at download time (no symlink/copy).
* 2. If extract is not specified: SOURCE_PATH/{artifact_name}
* 3. If extract is relative path: SOURCE_PATH/{value}
* 4. If extract is absolute path: {value}
* 5. If extract is array (dict): handled by extractor (selective extraction)
* 1. If extract is not specified: SOURCE_PATH/{artifact_name}
* 2. If extract is relative path: SOURCE_PATH/{value}
* 3. If extract is absolute path: {value}
* 4. If extract is array (dict): handled by extractor (selective extraction)
*/
public function getSourceDir(): string
{
// Prefer cache extract path, fall back to config
$cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name);
if (($cache_info['cache_type'] ?? null) === 'local' && isset($cache_info['dirname'])) {
return FileSystem::convertPath($cache_info['dirname']);
}
$extract = is_string($cache_info['extract'] ?? null)
? $cache_info['extract']
: ($this->config['source']['extract'] ?? null);
@@ -417,13 +407,10 @@ class Artifact
/**
* Set custom source fetcher callback.
*
* @param string $origin Short label shown in progress output (e.g. 'package downloader', 'custom url')
*/
public function setCustomSourceCallback(callable $callback, string $origin = 'package downloader'): void
public function setCustomSourceCallback(callable $callback): void
{
$this->custom_source_callback = $callback;
$this->custom_source_callback_origin = $origin;
}
public function getCustomSourceCallback(): ?callable
@@ -431,11 +418,6 @@ class Artifact
return $this->custom_source_callback ?? null;
}
public function getCustomSourceCallbackOrigin(): ?string
{
return $this->custom_source_callback_origin;
}
/**
* Set custom source check-update callback.
*/
@@ -470,19 +452,11 @@ class Artifact
*
* @param string $target_os Target OS platform string (e.g. linux-x86_64)
* @param callable $callback Custom binary fetcher callback
* @param string $origin Short label shown in progress output (e.g. 'package downloader')
*/
public function setCustomBinaryCallback(string $target_os, callable $callback, string $origin = 'package downloader'): void
public function setCustomBinaryCallback(string $target_os, callable $callback): void
{
ConfigValidator::validatePlatformString($target_os);
$this->custom_binary_callbacks[$target_os] = $callback;
$this->custom_binary_callback_origins[$target_os] = $origin;
}
public function getCustomBinaryCallbackOrigin(): ?string
{
$current_platform = SystemTarget::getCurrentPlatformString();
return $this->custom_binary_callback_origins[$current_platform] ?? null;
}
/**

View File

@@ -317,10 +317,7 @@ class ArtifactDownloader
if (!is_dir(DOWNLOAD_PATH)) {
FileSystem::createDir(DOWNLOAD_PATH);
}
$pending = array_values(array_filter($this->artifacts, fn ($a) => $this->generateQueue($a) !== []));
if ($pending !== []) {
logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $pending)) . " with concurrency {$this->parallel} ...");
}
logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ...");
// Download artifacts parallelly
if ($this->parallel > 1) {
$this->downloadWithConcurrency();
@@ -554,8 +551,8 @@ class ArtifactDownloader
$instance = null;
$call = $this->downloaders[$item['config']['type']] ?? null;
$type_display_name = match (true) {
$item['lock'] === 'source' && $artifact->getCustomSourceCallback() !== null => $artifact->getCustomSourceCallbackOrigin() ?? 'source package downloader',
$item['lock'] === 'binary' && $artifact->getCustomBinaryCallback() !== null => $artifact->getCustomBinaryCallbackOrigin() ?? 'binary package downloader',
$item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader',
$item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader',
default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'],
};
$try_h = $try ? 'Try downloading' : 'Downloading';
@@ -734,16 +731,6 @@ class ArtifactDownloader
$binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true);
$source_downloaded = $artifact->isSourceDownloaded(compare_hash: true);
if ($source_downloaded && $artifact->getName() === 'php-src' && ($requested = $this->getOption('with-php'))) {
$info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src');
$cv = $info['version'] ?? null;
$ct = $info['cache_type'] ?? null;
$matches = $requested === 'git' ? $ct === 'git' : ($cv !== null && $ct !== 'git' && ($cv === $requested || str_starts_with($cv, $requested . '.')));
if (!$matches) {
$source_downloaded = false;
}
}
$item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')];
$item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')];
@@ -838,21 +825,21 @@ class ArtifactDownloader
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) {
return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader);
}, 'custom url');
});
}
}
foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) {
return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader);
}, 'custom git');
});
}
}
foreach ($this->custom_locals as $artifact_name => $local_path) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) {
return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader);
}, 'custom local dir');
});
}
}
}

View File

@@ -136,12 +136,6 @@ class ArtifactExtractor
throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!");
}
// Local (--custom-local): source lives in place at $cache_info['dirname'].
if (($cache_info['cache_type'] ?? null) === 'local') {
$artifact->emitAfterSourceExtract($artifact->getSourceDir());
return SPC_STATUS_ALREADY_EXTRACTED;
}
$source_file = $this->cache->getCacheFullPath($cache_info);
$target_path = $artifact->getSourceDir();
@@ -177,12 +171,8 @@ class ArtifactExtractor
return SPC_STATUS_ALREADY_EXTRACTED;
}
// Remove old directory if hash mismatch.
// Guard: a symlink at $target_path (left over from older local-source handling) must be
// unlinked directly — never recurse into the link target, that would wipe the user's tree.
if (is_link($target_path)) {
@unlink($target_path);
} elseif (is_dir($target_path)) {
// Remove old directory if hash mismatch
if (is_dir($target_path)) {
logger()->notice("Source [{$name}] hash mismatch, re-extracting...");
FileSystem::removeDir($target_path);
}

View File

@@ -9,12 +9,7 @@ use StaticPHP\Doctor\Doctor;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\Pgo\PgoContext;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Exception\ParseException;
@@ -26,8 +21,6 @@ class CraftCommand extends BaseCommand
public function configure(): void
{
$this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml');
$this->addOption('libs-only', null, null, 'Build only the libraries needed by the configured extensions (skip PHP and SAPI build).');
PgoContext::registerOptions($this);
}
public function handle(): int
@@ -46,13 +39,6 @@ class CraftCommand extends BaseCommand
// apply env
array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}"));
// re-substitute env.ini's CC=${SPC_DEFAULT_CC} bindings.
ToolchainManager::initToolchain();
GlobalEnvManager::reapplyOsIni();
// stash craft for doctor checks that depend on what's being built (e.g. frankenphp → go-xcaddy)
ApplicationContext::set('craft', $craft);
// run doctor
if ($craft['craft-options']['doctor']) {
$doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX);
@@ -97,67 +83,23 @@ class CraftCommand extends BaseCommand
FileSystem::resetDir(SOURCE_PATH);
}
$pgo = $this->getOption('libs-only') ? null : PgoContext::tryFromInput($this->input, $craft['sapi'], $build_options);
$starttime = microtime(true);
// run installer
$installer = new PackageInstaller($build_options);
ApplicationContext::get(PackageBuilder::class)->setArgument('extensions', implode(',', $craft['extensions']));
if ($this->getOption('libs-only')) {
$with_suggests = (bool) ($craft['build-options']['with-suggests'] ?? false);
$libs = $this->resolveLibsForExtensions($craft, $with_suggests);
if ($libs === []) {
$this->output->writeln('<comment>No libraries needed for the configured extensions; nothing to do.</comment>');
return static::SUCCESS;
}
foreach ($libs as $lib) {
$installer->addBuildPackage($lib);
}
} else {
$installer->addBuildPackage('php');
}
$installer->addBuildPackage('php');
$installer->run(true);
$usedtime = round(microtime(true) - $starttime, 1);
$tag = $pgo !== null ? " (PGO {$pgo->mode})" : '';
$this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
$this->output->writeln("<info>✔ BUILD SUCCESSFUL{$tag} ({$usedtime} s)</info>");
$this->output->writeln("<info>✔ BUILD SUCCESSFUL ({$usedtime} s)</info>");
$this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
if ($pgo !== null && $pgo->isInstrument()) {
$this->output->writeln("<comment>Next: exercise the instrumented binary, then re-run craft with --pgo to consume {$pgo->profileRoot}.</comment>");
}
$installer->printBuildPackageOutputs();
return static::SUCCESS;
}
/** @return list<string> library package names transitively required by the configured extensions */
private function resolveLibsForExtensions(array $craft, bool $include_suggests): array
{
$exts = array_merge($craft['extensions'], $craft['shared-extensions'] ?? []);
$ext_pkgs = array_map(fn ($x) => "ext-{$x}", $exts);
$extra = $craft['packages'] ?? [];
$resolved = DependencyResolver::resolve(
array_merge($ext_pkgs, $extra),
include_suggests: $include_suggests,
);
$libs = [];
foreach ($resolved as $pkg_name) {
if (str_starts_with($pkg_name, 'ext-') || !PackageLoader::hasPackage($pkg_name)) {
continue;
}
if (PackageLoader::getPackage($pkg_name)->getType() === 'library') {
$libs[] = $pkg_name;
}
}
return $libs;
}
/**
* Validate and parse craft.yml file to array.
*

View File

@@ -10,7 +10,6 @@ use StaticPHP\DI\ApplicationContext;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
@@ -35,7 +34,6 @@ class ExtractCommand extends BaseCommand
public function handle(): int
{
GlobalEnvManager::afterInit();
$cache = ApplicationContext::get(ArtifactCache::class);
$extractor = new ArtifactExtractor($cache);
$force_source = (bool) $this->getOption('source-only');

View File

@@ -44,7 +44,7 @@ class SPCConfigCommand extends BaseCommand
'absolute_libs' => (bool) $this->getOption('absolute-libs'),
]);
$packages = array_merge(array_map(fn ($x) => "ext-{$x}", $extensions), $libraries);
$config = $util->config($packages);
$config = $util->config($packages, $include_suggests);
$this->output->writeln(match (true) {
$this->getOption('includes') => $config['cflags'],

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Doctor\CheckResult;
use StaticPHP\Package\PackageInstaller;
#[OptionalCheck([self::class, 'optionalCheck'])]
class GoXcaddyCheck
{
public static function optionalCheck(): bool
{
if (!ApplicationContext::has('craft')) {
return false;
}
/** @var null|array $craft */
$craft = ApplicationContext::get('craft');
return in_array('frankenphp', $craft['sapi'] ?? [], true);
}
#[CheckItem('if go-xcaddy is installed', level: 800)]
public function check(): CheckResult
{
if (!new PackageInstaller()->addInstallPackage('go-xcaddy')->isPackageInstalled('go-xcaddy')) {
return CheckResult::fail('go-xcaddy is not installed', 'install-go-xcaddy');
}
return CheckResult::ok(PKG_ROOT_PATH . '/go-xcaddy/bin/xcaddy');
}
#[FixItem('install-go-xcaddy')]
public function installGoXcaddy(): bool
{
$installer = new PackageInstaller(interactive: false);
$installer->addInstallPackage('go-xcaddy');
$installer->run(true);
return $installer->isPackageInstalled('go-xcaddy');
}
}

View File

@@ -73,20 +73,13 @@ class LinuxMuslCheck
$prefix = 'sudo ';
logger()->warning('Current user is not root, using sudo for running command');
}
$sysEnv = ['CC' => 'gcc', 'CXX' => 'g++', 'AR' => 'ar', 'LD' => 'ld', 'RANLIB' => 'ranlib'];
$envFlags = '';
foreach ($sysEnv as $k => $v) {
$envFlags .= "{$k}={$v} ";
}
$envFlags = rtrim($envFlags);
$shell = shell()->cd(SOURCE_PATH . '/musl-wrapper')
->setEnv($sysEnv)
->exec('./configure --disable-gcc-wrapper')
->exec('make -j');
->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper')
->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j');
if ($prefix !== '') {
f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && {$envFlags} {$prefix}make install");
f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install");
} else {
$shell->exec("{$prefix}make install");
$shell->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install");
}
return true;
}

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use Package\Artifact\llvm_compiler_rt;
use Package\Artifact\zig;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Doctor\CheckResult;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
#[OptionalCheck([self::class, 'optionalCheck'])]
class LlvmCompilerRtCheck
{
public static function optionalCheck(): bool
{
return ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain;
}
/** @noinspection PhpUnused */
#[CheckItem('if llvm-compiler-rt is built for current target', level: 799)]
public function checkLlvmCompilerRt(): CheckResult
{
$libDir = zig::path() . '/lib/' . SystemTarget::getCanonicalTriple();
if (new llvm_compiler_rt()->isBuilt($libDir)) {
return CheckResult::ok($libDir);
}
return CheckResult::fail('llvm-compiler-rt is not built for ' . SystemTarget::getCanonicalTriple(), 'build-llvm-compiler-rt');
}
#[FixItem('build-llvm-compiler-rt')]
public function fixLlvmCompilerRt(): bool
{
$installer = new PackageInstaller(interactive: false);
$installer->addInstallPackage('llvm-compiler-rt');
$installer->run(true);
new llvm_compiler_rt()->buildForTriple();
$libDir = zig::path() . '/lib/' . SystemTarget::getCanonicalTriple();
return new llvm_compiler_rt()->isBuilt($libDir);
}
}

View File

@@ -26,9 +26,8 @@ class LlvmToolsCheck
#[CheckItem('if llvm-tools (objcopy/strip/profdata) are built', level: 798)]
public function checkLlvmTools(): CheckResult
{
$binDir = PKG_ROOT_PATH . '/llvm-tools/bin';
if (new llvm_tools()->allBuilt($binDir)) {
return CheckResult::ok($binDir);
if (llvm_tools::isInstalled()) {
return CheckResult::ok(llvm_tools::path() . '/bin');
}
return CheckResult::fail('llvm-tools are not built', 'build-llvm-tools');
}
@@ -40,6 +39,6 @@ class LlvmToolsCheck
$installer->addInstallPackage('llvm-tools');
$installer->run(true);
new llvm_tools()->buildForHost();
return new llvm_tools()->allBuilt(PKG_ROOT_PATH . '/llvm-tools/bin');
return llvm_tools::isInstalled();
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use Package\Artifact\zig;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
@@ -27,7 +26,7 @@ class ZigCheck
public function checkZig(): CheckResult
{
if (new PackageInstaller()->addInstallPackage('zig')->isPackageInstalled('zig')) {
return CheckResult::ok(zig::binary());
return CheckResult::ok();
}
return CheckResult::fail('zig is not installed', 'install-zig');
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace StaticPHP\Package;
use Package\Artifact\llvm_tools;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
@@ -180,7 +181,10 @@ class PackageBuilder
if (SystemTarget::getTargetOS() === 'Darwin') {
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
} elseif (SystemTarget::getTargetOS() === 'Linux') {
$objcopy = getenv('OBJCOPY') ?: 'objcopy';
$objcopy = getenv('OBJCOPY')
?: (ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain
? llvm_tools::binary('llvm-objcopy')
: 'objcopy');
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
shell()
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
@@ -203,7 +207,7 @@ class PackageBuilder
public function stripBinary(string $binary_path): void
{
$strip = ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain
? PKG_ROOT_PATH . '/llvm-tools/bin/llvm-strip'
? llvm_tools::binary('llvm-strip')
: 'strip';
shell()->exec(match (SystemTarget::getTargetOS()) {
'Darwin' => "{$strip} -S {$binary_path}",

View File

@@ -154,9 +154,6 @@ class PackageInstaller
$this->resolvePackages();
}
$this->reconcilePhpSrcVersion();
$this->emitPreInstallEvents();
if ($this->interactive && !$disable_delay_msg) {
// show install or build options in terminal with beautiful output
$this->printInstallerInfo();
@@ -218,7 +215,7 @@ class PackageInstaller
if (!$is_to_build && $should_use_binary) {
// install binary
if ($this->interactive) {
InteractiveTerm::indicateProgress('Installing ' . $this->kindLabel($package) . ': ' . ConsoleColor::yellow($package->getName()));
InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName()));
}
try {
// Start tracking for binary installation
@@ -230,17 +227,17 @@ class PackageInstaller
// Stop tracking on error
$this->tracker?->stopTracking();
if ($this->interactive) {
InteractiveTerm::finish('Installing ' . $this->kindLabel($package) . ' failed: ' . ConsoleColor::red($package->getName()), false);
InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false);
echo PHP_EOL;
}
throw $e;
}
if ($this->interactive) {
InteractiveTerm::finish('Installed ' . $this->kindLabel($package) . ': ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : ''));
InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : ''));
}
} elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) {
if ($this->interactive) {
InteractiveTerm::indicateProgress('Building ' . $this->kindLabel($package) . ': ' . ConsoleColor::yellow($package->getName()));
InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName()));
}
try {
// Start tracking for build
@@ -263,13 +260,13 @@ class PackageInstaller
// Stop tracking on error
$this->tracker?->stopTracking();
if ($this->interactive) {
InteractiveTerm::finish('Building ' . $this->kindLabel($package) . ' failed: ' . ConsoleColor::red($package->getName()), false);
InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false);
echo PHP_EOL;
}
throw $e;
}
if ($this->interactive) {
InteractiveTerm::finish('Built ' . $this->kindLabel($package) . ': ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
}
}
}
@@ -362,15 +359,6 @@ class PackageInstaller
return false;
}
public function emitPreInstallEvents(): void
{
foreach ($this->packages as $package) {
if ($package->hasStage('preInstall')) {
$package->runStage('preInstall');
}
}
}
public function emitPostInstallEvents(): void
{
foreach ($this->packages as $package) {
@@ -583,77 +571,6 @@ class PackageInstaller
return null;
}
private function reconcilePhpSrcVersion(): void
{
$src_dir = SOURCE_PATH . '/php-src';
$cache = ApplicationContext::get(ArtifactCache::class);
$requested = $this->options['dl-with-php'] ?? null;
if ($requested !== null && $requested !== '' && $requested !== 'git') {
$info = $cache->getSourceInfo('php-src') ?? [];
$cv = $info['version'] ?? null;
if (($info['cache_type'] ?? null) === 'git' || $cv === null
|| ($cv !== $requested && !str_starts_with($cv, $requested . '.'))) {
$resolved = null;
$candidates = glob(DOWNLOAD_PATH . '/php-' . $requested . '.*.tar.xz') ?: [];
if ($candidates !== []) {
usort($candidates, 'strnatcmp');
if (preg_match('/^php-([0-9.]+)\.tar\.xz$/', basename(end($candidates)), $vm)) {
$resolved = $vm[1];
}
} elseif ($this->download) {
$j = @file_get_contents('https://www.php.net/releases/index.php?json&version=' . urlencode($requested));
$rel = is_string($j) ? json_decode($j, true) : null;
$resolved = is_array($rel) ? ($rel['version'] ?? null) : null;
} else {
throw new WrongUsageException("Requested PHP '{$requested}' but no php-{$requested}.*.tar.xz in downloads/; drop --no-download or run 'bin/spc download php-src --with-php={$requested}' first.");
}
if ($resolved !== null) {
$cf = DOWNLOAD_PATH . '/.cache.json';
$j = json_decode(@file_get_contents($cf) ?: '{}', true) ?: [];
$tarball = DOWNLOAD_PATH . "/php-{$resolved}.tar.xz";
$j['php-src']['source'] = [
'lock_type' => 'source', 'cache_type' => 'archive',
'filename' => "php-{$resolved}.tar.xz",
'extract' => $info['extract'] ?? null,
'hash' => is_file($tarball) ? sha1_file($tarball) : null,
'time' => time(), 'version' => $resolved,
'config' => $info['config'] ?? ['type' => 'php-release', 'domain' => 'https://www.php.net'],
'downloader' => $info['downloader'] ?? \StaticPHP\Artifact\Downloader\Type\PhpRelease::class,
];
$j['php-src']['binary'] ??= [];
file_put_contents($cf, json_encode($j, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
ApplicationContext::set(ArtifactCache::class, $cache = new ArtifactCache());
}
}
}
if (!is_dir($src_dir) || ($info = $cache->getSourceInfo('php-src')) === null) {
return;
}
if (($info['cache_type'] ?? null) === 'git') {
if (!is_dir($src_dir . '/.git')) {
FileSystem::removeDir($src_dir);
}
return;
}
$vh = $src_dir . '/main/php_version.h';
if (is_file($vh)
&& preg_match('/#define\s+PHP_VERSION\s+"([^"]+)"/', file_get_contents($vh), $m)
&& $m[1] !== ($info['version'] ?? null)
) {
FileSystem::removeDir($src_dir);
}
}
private function kindLabel(Package $package): string
{
return match (true) {
$package instanceof PhpExtensionPackage => 'extension',
$package instanceof TargetPackage => 'target',
$package instanceof LibraryPackage => 'library',
default => 'package',
};
}
/**
* @param Package[] $packages
*/
@@ -771,7 +688,7 @@ class PackageInstaller
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) {
$frankenphp = PackageLoader::getPackage('frankenphp');
$this->install_packages[$frankenphp->getName()] = $frankenphp;
$this->build_packages[$frankenphp->getName()] = $frankenphp;
$this->build_packages[$package->getName()] = $package;
$added = true;
}
$this->build_packages[$package->getName()] = $package;

View File

@@ -12,7 +12,6 @@ use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Toolchain\ZigToolchain;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\SPCConfigUtil;
@@ -279,15 +278,10 @@ class PhpExtensionPackage extends Package
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group ';
$postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group ';
// -Wl,-Bsymbolic: bind zend_* refs to the .so's own copies, not via global lookup
$ldflags = (string) $config['ldflags'];
if (PHP_OS_FAMILY !== 'Darwin' && !str_contains($ldflags, '-Wl,-Bsymbolic')) {
$ldflags = clean_spaces($ldflags . ' -Wl,-Bsymbolic');
}
return [
'CFLAGS' => $config['cflags'],
'CXXFLAGS' => $config['cxxflags'],
'LDFLAGS' => $ldflags,
'CXXFLAGS' => $config['cflags'],
'LDFLAGS' => $config['ldflags'],
'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"),
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
@@ -309,7 +303,6 @@ class PhpExtensionPackage extends Package
public function configureForUnix(array $env, PhpExtensionPackage $package): void
{
$phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
// CustomPhpConfigureArg keys are OS names ('Linux'/'Darwin'), not platform strings
shell()->cd($package->getSourceDir())
->setEnv($env)
->exec(
@@ -325,53 +318,11 @@ class PhpExtensionPackage extends Package
#[Stage]
public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void
{
// phpize Makefile's _SHARED_LIBADD line misses our static archives — splice them in
$package->patchSharedLibAdd();
$extra_ldflags = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS');
$makeArgs = $extra_ldflags !== '' ? 'EXTRA_LDFLAGS=' . escapeshellarg($extra_ldflags) : '';
shell()->cd($package->getSourceDir())
->setEnv($env)
->exec('make clean')
->exec("make -j{$builder->concurrency} {$makeArgs}")
->exec("make install {$makeArgs}");
// install-modules deref'd libtool's `$ext.so → $ext-X.so` symlink into two regular files; restore the symlink.
if (preg_match('/-release\s+(\S+)/', $extra_ldflags, $m)) {
$name = $package->getExtensionName();
$unversioned = BUILD_MODULES_PATH . "/{$name}.so";
$versioned = BUILD_MODULES_PATH . "/{$name}-{$m[1]}.so";
if (file_exists($versioned) && file_exists($unversioned) && !is_link($unversioned)) {
unlink($unversioned);
symlink(basename($versioned), $unversioned);
}
}
}
public function patchSharedLibAdd(): void
{
$config = new SPCConfigUtil()->getExtensionConfig($this);
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a')
? '-l:libstdc++.a'
: (str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : '');
$makefile = $this->getSourceDir() . '/Makefile';
if (!is_file($makefile)) {
return;
}
$content = (string) file_get_contents($makefile);
if (!preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $content, $m)) {
return;
}
$prefix = $m[1];
$current = trim($m[2]);
$merged = clean_spaces("{$current} {$staticLibs} {$lstdcpp}");
$merged = deduplicate_flags($merged);
FileSystem::replaceFileRegex(
$makefile,
'/^(.*_SHARED_LIBADD\s*=.*)$/m',
$prefix . $merged
);
->exec("make -j{$builder->concurrency}")
->exec('make install');
}
/**
@@ -382,31 +333,14 @@ class PhpExtensionPackage extends Package
*/
public function buildSharedForUnix(PackageBuilder $builder): void
{
// skip virtual addons (arg-type=none + display-name → owning ext); the parent ext built it
$argType = $this->extension_config['arg-type'] ?? null;
$displayName = $this->extension_config['display-name'] ?? null;
if ($argType === 'none' && $displayName !== null && $displayName !== $this->getExtensionName()) {
logger()->info("Skipping virtual extension [{$this->getName()}] — it's part of [{$displayName}].");
return;
}
if (!is_dir($this->getSourceDir())) {
throw new ValidationException(
"Extension source directory not found: {$this->getSourceDir()}",
validation_module: "Extension {$this->getName()} source"
);
}
$env = $this->getSharedExtensionEnv();
$this->runStage([$this, 'phpizeForUnix'], ['env' => $env]);
$this->runStage([$this, 'configureForUnix'], ['env' => $env]);
$this->runStage([$this, 'makeForUnix'], ['env' => $env]);
// libtool's -release X gives $name-X.so as the real file
$soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName()
. (preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m) ? "-{$m[1]}" : '')
. '.so';
// process *.so file
$soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so';
if (!file_exists($soFile)) {
throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build");
}

View File

@@ -36,13 +36,6 @@ class ArtifactLoader
public static function getArtifactInstance(string $artifact_name): ?Artifact
{
self::initArtifactInstances();
if (!isset(self::$artifacts[$artifact_name])) {
// Artifact may have been registered after initArtifactInstances() ran (e.g., from a vendor registry)
$config = ArtifactConfig::get($artifact_name);
if ($config !== null) {
self::$artifacts[$artifact_name] = new Artifact($artifact_name, $config);
}
}
return self::$artifacts[$artifact_name] ?? null;
}

View File

@@ -367,18 +367,15 @@ class PackageLoader
public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable
{
// match condition; '*' is a wildcard that fires for every package's stage
// match condition
$installer = ApplicationContext::get(PackageInstaller::class);
$stages = array_merge(
self::$before_stages[$package_name][$stage] ?? [],
$package_name === '*' ? [] : (self::$before_stages['*'][$stage] ?? []),
);
$stages = self::$before_stages[$package_name][$stage] ?? [];
foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) {
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
continue;
}
foreach ($conditionals as $class) {
if (ApplicationContext::tryGet($class) === null) {
if (!ApplicationContext::has($class)) {
continue 2;
}
}
@@ -388,19 +385,16 @@ class PackageLoader
public static function getAfterStageCallbacks(string $package_name, string $stage): array
{
// match condition; '*' is a wildcard that fires for every package's stage
// match condition
$installer = ApplicationContext::get(PackageInstaller::class);
$stages = array_merge(
self::$after_stages[$package_name][$stage] ?? [],
$package_name === '*' ? [] : (self::$after_stages['*'][$stage] ?? []),
);
$stages = self::$after_stages[$package_name][$stage] ?? [];
$result = [];
foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) {
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
continue;
}
foreach ($conditionals as $class) {
if (ApplicationContext::tryGet($class) === null) {
if (!ApplicationContext::has($class)) {
continue 2;
}
}
@@ -431,20 +425,6 @@ class PackageLoader
{
foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) {
foreach ($ev_all as $package_name => $stages) {
// wildcard hooks fire for every package's stage; nothing to validate against
if ($package_name === '*') {
foreach ($stages as $stage_name => $before_events) {
foreach ($before_events as [$event_callable, $only_when_package_resolved, $conditionals]) {
if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) {
throw new RegistryException("{$event_name} event for wildcard [*] stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}].");
}
if (!is_callable($event_callable)) {
throw new RegistryException("{$event_name} event for wildcard [*] stage [{$stage_name}] has invalid callable.");
}
}
}
continue;
}
// check package exists
if (!self::hasPackage($package_name)) {
throw new RegistryException(

View File

@@ -89,19 +89,14 @@ class Registry
self::$current_registry_name = $registry_name;
try {
// resolve autoload manually — path-repo installs have no vendor/, FileSystem::fullpath would throw
// Load composer autoload if specified (for external registries with their own dependencies)
if (isset($data['autoload']) && is_string($data['autoload'])) {
$base = dirname($registry_file);
$autoload_path = FileSystem::isRelativePath($data['autoload'])
? rtrim($base, '/') . DIRECTORY_SEPARATOR . $data['autoload']
: $data['autoload'];
$autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file));
if (file_exists($autoload_path)) {
logger()->debug("Loading external autoload from: {$autoload_path}");
require_once $autoload_path;
} elseif (str_contains(rtrim(FileSystem::convertPath($base), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) {
logger()->debug("Registry autoload not present, relying on consumer autoloader: {$autoload_path}");
} else {
throw new RegistryException("Path does not exist: {$autoload_path}");
logger()->warning("Autoload file not found: {$autoload_path}");
}
}

View File

@@ -11,7 +11,6 @@ use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\Shell\UnixShell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\InteractiveTerm;
use ZM\Logger\ConsoleColor;
@@ -150,17 +149,13 @@ class UnixAutoconfExecutor extends Executor
*/
private function getDefaultConfigureArgs(): array
{
$args = [
return [
'--disable-shared',
'--enable-static',
"--prefix={$this->package->getBuildRootPath()}",
'--with-pic',
'--enable-pic',
];
if ($host_triple = SystemTarget::getAutoconfHostTriple()) {
$args[] = "--host={$host_triple}";
}
return $args;
}
/**

View File

@@ -302,12 +302,9 @@ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "{$include}")
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "{$include}")
CMAKE;
// pin AR/RANLIB so cmake uses zig-ar/zig-ranlib instead of system /usr/bin/ranlib (zig archives need it)
// Whoops, linux may need CMAKE_AR sometimes
if (PHP_OS_FAMILY === 'Linux') {
$ar = getenv('SPC_DEFAULT_AR') ?: getenv('AR') ?: 'ar';
$ranlib = getenv('SPC_DEFAULT_RANLIB') ?: (getenv('RANLIB') ?: 'ranlib');
$toolchain .= "\nSET(CMAKE_AR \"{$ar}\")";
$toolchain .= "\nSET(CMAKE_RANLIB \"{$ranlib}\")";
$toolchain .= "\nSET(CMAKE_AR \"ar\")";
}
FileSystem::writeFile(SOURCE_PATH . '/toolchain.cmake', $toolchain);
return $created = realpath(SOURCE_PATH . '/toolchain.cmake');

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace StaticPHP\Runtime;
use StaticPHP\Toolchain\ZigToolchain;
use StaticPHP\Util\System\LinuxUtil;
/**
@@ -17,7 +16,7 @@ class SystemTarget
*/
public static function getLibc(): ?string
{
if ($target = self::target()) {
if ($target = getenv('SPC_TARGET')) {
if (str_contains($target, '-gnu')) {
return 'glibc';
}
@@ -58,7 +57,7 @@ class SystemTarget
public static function getLibcVersion(): ?string
{
if (PHP_OS_FAMILY === 'Linux') {
$target = self::target();
$target = (string) getenv('SPC_TARGET');
if (str_contains($target, '-gnu.2.')) {
return preg_match('/-gnu\.(2\.\d+)/', $target, $matches) ? $matches[1] : null;
}
@@ -76,7 +75,7 @@ class SystemTarget
*/
public static function getTargetOS(): string
{
$target = self::target();
$target = (string) getenv('SPC_TARGET');
return match (true) {
str_contains($target, '-linux') => 'Linux',
str_contains($target, '-macos') => 'Darwin',
@@ -92,7 +91,7 @@ class SystemTarget
*/
public static function getTargetArch(): string
{
$target = self::target();
$target = (string) getenv('SPC_TARGET');
return match (true) {
str_contains($target, 'x86_64') || str_contains($target, 'amd64') => 'x86_64',
str_contains($target, 'aarch64') || str_contains($target, 'arm64') => 'aarch64',
@@ -128,70 +127,4 @@ class SystemTarget
{
return in_array(self::getTargetOS(), ['Linux', 'Darwin', 'BSD']);
}
/**
* Returns the canonical target triple (arch-os-abi) for per-target build
* artifacts. Always returns a non-null triple, falling back to a host-derived
* triple when SPC_TARGET is unset or names 'native'.
* Strips libc version suffix (-gnu.2.17 → -gnu) and trailing flags (' -dynamic').
*/
public static function getCanonicalTriple(): string
{
$target = self::target();
if ($target !== '' && !str_contains($target, 'native')) {
$cleaned = (string) preg_replace('/(-gnu|-musl)\.[\d.]+/', '$1', $target);
$cleaned = preg_split('/\s+/', trim($cleaned))[0] ?? '';
if ($cleaned !== '') {
return $cleaned;
}
}
$arch = self::getTargetArch();
return match (self::getTargetOS()) {
'Linux' => $arch . '-linux-' . (self::getLibc() === 'musl' ? 'musl' : 'gnu'),
'Darwin' => $arch . '-macos-none',
'Windows' => $arch . '-windows-gnu',
default => $arch . '-unknown-unknown',
};
}
/**
* Returns a GNU host triple for autoconf --host= when SPC_TARGET names an
* architecture different from the build host (true cross-compile).
* Returns null for same-arch builds.
* Strips libc version suffix (-gnu.2.17 → -gnu) and trailing flags (e.g. ' -dynamic').
*/
public static function getAutoconfHostTriple(): ?string
{
$target = self::target();
if ($target === '' || str_contains($target, 'native')) {
return null;
}
$cleaned = preg_split('/\s+/', trim((string) preg_replace('/(-gnu|-musl)\.[\d.]+/', '$1', $target)))[0];
if ($cleaned === '') {
return null;
}
// Only emit --host for true cross-arch builds; same-arch (incl. cross-libc) lets autoconf detect.
$target_arch_token = explode('-', $cleaned)[0];
$arch_aliases = [
'x86_64' => ['x86_64', 'amd64'],
'aarch64' => ['aarch64', 'arm64'],
'arm' => ['arm', 'armv6', 'armv7', 'armhf', 'armel'],
'i386' => ['i386', 'i486', 'i586', 'i686'],
];
$host_arch = GNU_ARCH;
if (array_any($arch_aliases, fn ($aliases) => in_array($target_arch_token, $aliases, true) && in_array($host_arch, $aliases, true))) {
return null;
}
return $cleaned;
}
/** native toolchains ignore SPC_TARGET */
private static function target(): string
{
$tc = (string) getenv('SPC_TOOLCHAIN');
if ($tc !== '' && $tc !== ZigToolchain::class) {
return '';
}
return (string) getenv('SPC_TARGET');
}
}

View File

@@ -15,7 +15,6 @@ class ClangBrewToolchain extends ClangNativeToolchain
GlobalEnvManager::putenv("SPC_DEFAULT_CC={$homebrew_prefix}/opt/llvm/bin/clang");
GlobalEnvManager::putenv("SPC_DEFAULT_CXX={$homebrew_prefix}/opt/llvm/bin/clang++");
GlobalEnvManager::putenv("SPC_DEFAULT_AR={$homebrew_prefix}/opt/llvm/bin/llvm-ar");
GlobalEnvManager::putenv("SPC_DEFAULT_RANLIB={$homebrew_prefix}/opt/llvm/bin/llvm-ranlib");
GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld');
GlobalEnvManager::addPathIfNotExists("{$homebrew_prefix}/opt/llvm/bin");
}

View File

@@ -21,7 +21,6 @@ class ClangNativeToolchain implements UnixToolchainInterface
GlobalEnvManager::putenv('SPC_DEFAULT_CC=clang');
GlobalEnvManager::putenv('SPC_DEFAULT_CXX=clang++');
GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar');
GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=ranlib');
GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld');
}

View File

@@ -18,7 +18,6 @@ class GccNativeToolchain implements UnixToolchainInterface
GlobalEnvManager::putenv('SPC_DEFAULT_CC=gcc');
GlobalEnvManager::putenv('SPC_DEFAULT_CXX=g++');
GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar');
GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=ranlib');
GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld');
}

View File

@@ -4,49 +4,46 @@ declare(strict_types=1);
namespace StaticPHP\Toolchain;
use Package\Artifact\llvm_compiler_rt;
use Package\Artifact\zig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\UnixToolchainInterface;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\System\LinuxUtil;
use ZM\Logger\ConsoleColor;
class ZigToolchain implements UnixToolchainInterface
{
private static bool $afterInitDone = false;
public function initEnv(): void
{
// Set environment variables for zig toolchain
GlobalEnvManager::putenv('SPC_DEFAULT_CC=zig-cc');
GlobalEnvManager::putenv('SPC_DEFAULT_CXX=zig-c++');
GlobalEnvManager::putenv('SPC_DEFAULT_AR=zig-ar');
GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=zig-ranlib');
GlobalEnvManager::putenv('SPC_DEFAULT_LD=zig-ld.lld');
GlobalEnvManager::addPathIfNotExists($this->getPath());
// Generate additional objects needed for zig toolchain
$paths = ['/usr/lib/gcc', '/usr/local/lib/gcc'];
$objects = ['crtbeginS.o', 'crtendS.o'];
$found = [];
foreach ($objects as $obj) {
$located = null;
foreach ($paths as $base) {
$output = shell_exec("find {$base} -name {$obj} 2>/dev/null | grep -v '/32/' | head -n 1");
$line = trim((string) $output);
if ($line !== '') {
$located = $line;
break;
}
}
if ($located) {
$found[] = $located;
}
}
GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found));
}
public function afterInit(): void
{
if (self::$afterInitDone) {
return;
}
self::$afterInitDone = true;
// zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough
if (function_exists('posix_setrlimit') && function_exists('posix_getrlimit')) {
$limits = posix_getrlimit();
$hard = (int) ($limits['hardopenfiles'] ?? 2048);
$soft = (int) ($limits['softopenfiles'] ?? 1024);
$target = min(max($soft, 2048), $hard > 0 ? $hard : 2048);
if ($target > $soft) {
posix_setrlimit(POSIX_RLIMIT_NOFILE, $target, $hard);
}
}
GlobalEnvManager::addPathIfNotExists($this->getPath());
f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough
$cflags = getenv('SPC_DEFAULT_CFLAGS') ?: '';
$cxxflags = getenv('SPC_DEFAULT_CXXFLAGS') ?: '';
$extraCflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: '';
@@ -57,8 +54,7 @@ class ZigToolchain implements UnixToolchainInterface
GlobalEnvManager::putenv("SPC_DEFAULT_CXXFLAGS={$cxxflags}");
GlobalEnvManager::putenv("SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS={$extraCflags}");
GlobalEnvManager::putenv('RANLIB=zig-ranlib');
GlobalEnvManager::putenv('SPC_COMPILER_RT_DIR=' . zig::path() . '/lib/' . SystemTarget::getCanonicalTriple());
GlobalEnvManager::putenv('OBJCOPY=' . PKG_ROOT_PATH . '/llvm-tools/bin/llvm-objcopy');
GlobalEnvManager::putenv('OBJCOPY=zig-objcopy');
$extra_libs = getenv('SPC_EXTRA_LIBS') ?: '';
if (!str_contains($extra_libs, '-lunwind')) {
// Add unwind library if not already present
@@ -74,8 +70,6 @@ class ZigToolchain implements UnixToolchainInterface
// zig-cc/clang treats strlcpy/strlcat as compiler builtins, so configure link tests pass (HAVE_STRLCPY=1)
$extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=ac_cv_func_strlcpy=no ac_cv_func_strlcat=no {$extra_vars}");
$this->ensureCompilerRt();
}
public function getCompilerInfo(): ?string
@@ -111,53 +105,8 @@ class ZigToolchain implements UnixToolchainInterface
return false;
}
private function ensureCompilerRt(): void
{
if (!zig::isInstalled()) {
return;
}
$rt = new llvm_compiler_rt();
$triple = SystemTarget::getCanonicalTriple();
$libDir = zig::path() . '/lib/' . $triple;
if ($rt->isBuilt($libDir)) {
return;
}
if (!is_dir(SOURCE_PATH . '/llvm-compiler-rt/lib/profile')) {
// Source not yet downloaded; install via nested PackageInstaller. The recursion guard
// on afterInit prevents the nested run from re-entering this method. Save the outer
// installer/builder in the container so executors keep seeing the outer one after.
// The PackageInstaller surfaces its own spinner for the install; AfterBinaryExtract
// builds for the current triple, so we're done after run().
$outerInstaller = ApplicationContext::tryGet(PackageInstaller::class);
$outerBuilder = ApplicationContext::tryGet(PackageBuilder::class);
try {
new PackageInstaller()
->addInstallPackage('llvm-compiler-rt')
->run(true);
} finally {
if ($outerInstaller !== null) {
ApplicationContext::set(PackageInstaller::class, $outerInstaller);
}
if ($outerBuilder !== null) {
ApplicationContext::set(PackageBuilder::class, $outerBuilder);
}
}
return;
}
// Source already extracted from a previous run on a different triple; rebuild here with our
// own progress spinner since we're outside the PackageInstaller flow.
InteractiveTerm::indicateProgress('Building llvm-compiler-rt for ' . ConsoleColor::yellow($triple));
try {
$rt->buildForTriple();
} catch (\Throwable $e) {
InteractiveTerm::finish('Build llvm-compiler-rt for ' . ConsoleColor::red($triple) . ' failed', false);
throw $e;
}
InteractiveTerm::finish('Built llvm-compiler-rt for ' . ConsoleColor::green($triple));
}
private function getPath(): string
{
return zig::path();
return PKG_ROOT_PATH . '/zig';
}
}

View File

@@ -123,24 +123,6 @@ class GlobalEnvManager
}
}
/** Re-substitute env.ini's CC=${SPC_DEFAULT_CC} bindings after a toolchain swap. */
public static function reapplyOsIni(): void
{
$ini = self::readIniFile();
$os_ini = match (PHP_OS_FAMILY) {
'Windows' => $ini['windows'] ?? [],
'Darwin' => $ini['macos'] ?? [],
'Linux' => $ini['linux'] ?? [],
'BSD' => $ini['freebsd'] ?? [],
default => [],
};
foreach (['CC', 'CXX', 'AR', 'RANLIB', 'LD'] as $k) {
if (isset($os_ini[$k])) {
self::putenv("{$k}={$os_ini[$k]}");
}
}
}
/**
* Initialize the toolchain after the environment variables are set.
* The toolchain or environment availability check is done here.

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\DI\ApplicationContext;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\ProgressIndicator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
@@ -28,19 +27,14 @@ class InteractiveTerm
}
}
public static function success(string $message, bool $indent = false, ?float $start_time = null): void
public static function success(string $message, bool $indent = false): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
if ($start_time !== null) {
$message .= ' (' . Helper::formatTime(microtime(true) - $start_time) . ')';
}
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
} else {
// wipe the current indicator line so our persistent ✔ line doesn't get appended to a spinner
$clear = (self::$indicator !== null && $output->isDecorated() && !$no_ansi) ? "\x0D\x1B[2K" : '';
$output->writeln($clear . ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message));
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message));
logger()->debug(strip_ansi_colors($message));
}
}

View File

@@ -1,265 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util\Pgo;
use StaticPHP\Command\BaseCommand;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Util\FileSystem;
use Symfony\Component\Console\Input\InputInterface;
final class PgoContext
{
public const string MODE_INSTRUMENT = 'instrument';
public const string MODE_CS_INSTRUMENT = 'cs-instrument';
public const string MODE_USE = 'use';
/**
* @var array<string, string>
*/
public const array TRAINABLE = [
'cli' => 'build-cli',
'micro' => 'build-micro',
'cgi' => 'build-cgi',
'fpm' => 'build-fpm',
'embed' => 'build-embed',
'frankenphp' => 'build-frankenphp',
];
public const array SHUTDOWN_PATCHES = [
'php-src' => 'spc_pgo_flush_php_main.patch',
'frankenphp' => 'spc_pgo_flush_frankenphp.patch',
];
/** @var list<string> */
private array $trainableSapis = [];
public function __construct(
public readonly string $mode,
public readonly string $profileRoot,
) {
if (!in_array($mode, [self::MODE_INSTRUMENT, self::MODE_CS_INSTRUMENT, self::MODE_USE], true)) {
throw new WrongUsageException("PgoContext: unknown mode '{$mode}'");
}
}
public static function registerOptions(BaseCommand $cmd): void
{
$cmd->addOption('pgi', null, null, 'PGO instrument pass: build with -fprofile-generate so the resulting binary writes .profraw on shutdown.');
$cmd->addOption('cs-pgi', null, null, 'PGO context-sensitive instrument pass: -fprofile-use=<existing.profdata> + -fcs-profile-generate. Requires a prior --pgi/--pgo cycle.');
$cmd->addOption('pgo', null, null, 'PGO use pass: merge the collected .profraw into .profdata, then rebuild with -fprofile-use.');
}
/**
* @param array<string> $sapis
* @param array<string, mixed> $build_options
*/
public static function tryFromInput(InputInterface $input, array $sapis, array &$build_options): ?self
{
$modes = array_filter(['pgi', 'cs-pgi', 'pgo'], fn ($m) => (bool) $input->getOption($m));
if (count($modes) > 1) {
throw new WrongUsageException('--pgi, --cs-pgi, and --pgo are mutually exclusive');
}
$picked = array_values($modes)[0] ?? null;
if ($picked === null) {
return null;
}
$mode = match ($picked) {
'pgi' => self::MODE_INSTRUMENT,
'cs-pgi' => self::MODE_CS_INSTRUMENT,
'pgo' => self::MODE_USE,
};
$ctx = new self($mode, BUILD_ROOT_PATH . '/pgo-data');
$ctx->setTrainableSapis($sapis);
match ($mode) {
self::MODE_INSTRUMENT => $ctx->setupInstrument(),
self::MODE_CS_INSTRUMENT => $ctx->setupCsInstrument(),
self::MODE_USE => $ctx->mergeProfiles(),
};
if ($ctx->isInstrument() || $ctx->isCsInstrument()) {
$build_options['no-strip'] = true;
}
ApplicationContext::set(self::class, $ctx);
return $ctx;
}
public function isInstrument(): bool
{
return $this->mode === self::MODE_INSTRUMENT;
}
public function isCsInstrument(): bool
{
return $this->mode === self::MODE_CS_INSTRUMENT;
}
public function isUse(): bool
{
return $this->mode === self::MODE_USE;
}
/**
* @param list<string> $sapis
*/
public function setTrainableSapis(array $sapis): void
{
$resolved = [];
foreach ($sapis as $sapi) {
$r = $this->resolveSapi($sapi);
if (!in_array($r, $resolved, true)) {
$resolved[] = $r;
}
}
if ($resolved === []) {
throw new WrongUsageException(
'PGO: no trainable SAPI selected; supply one of ' . implode(', ', array_keys(self::TRAINABLE))
);
}
$this->trainableSapis = $resolved;
}
/** @return list<string> */
public function trainableSapis(): array
{
return $this->trainableSapis;
}
/**
* Static-embed mode links libphp.a into frankenphp, sharing a single binary
* and profdata. Shared-embed keeps them separate.
*/
public function resolveSapi(string $sapi): string
{
if ($sapi === 'embed' && getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') {
return 'frankenphp';
}
return $sapi;
}
public function rawDir(string $sapi): string
{
return $this->profileRoot . '/' . $sapi;
}
public function csRawDir(string $sapi): string
{
return $this->profileRoot . '/cs-' . $sapi;
}
public function profDataFile(string $sapi): string
{
return $this->profileRoot . '/' . $sapi . '.profdata';
}
public function cflagsFor(string $sapi): string
{
$sapi = $this->resolveSapi($sapi);
if ($this->mode === self::MODE_USE && !is_file($this->profDataFile($sapi))) {
return '';
}
return match ($this->mode) {
self::MODE_INSTRUMENT => '-fprofile-generate=' . $this->rawDir($sapi)
. ' -fprofile-update=atomic',
self::MODE_CS_INSTRUMENT => '-fprofile-use=' . $this->profDataFile($sapi)
. ' -fcs-profile-generate=' . $this->csRawDir($sapi)
. ' -fprofile-update=atomic'
. ' -Wno-error=profile-instr-unprofiled'
. ' -Wno-error=profile-instr-out-of-date'
. ' -Wno-backend-plugin',
self::MODE_USE => '-fprofile-use=' . $this->profDataFile($sapi)
. ' -Wno-error=profile-instr-unprofiled'
. ' -Wno-error=profile-instr-out-of-date'
. ' -Wno-backend-plugin',
default => throw new WrongUsageException("PgoContext: unreachable mode '{$this->mode}'"),
};
}
public function ldflagsFor(string $sapi): string
{
$resolved = $this->resolveSapi($sapi);
$flags = $this->cflagsFor($sapi);
$patterns = ['/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/'];
if ($resolved === 'frankenphp') {
$patterns[] = '/\s*-fprofile-use=\S+/';
$patterns[] = '/\s*-fcs-profile-generate=\S+/';
}
return trim((string) preg_replace($patterns, '', $flags));
}
public function applyEnvFor(string $sapi): void
{
self::overwritePgoFlags('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $this->cflagsFor($sapi));
self::overwritePgoFlags('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldflagsFor($sapi));
}
public function setupInstrument(): void
{
FileSystem::removeDir($this->profileRoot);
FileSystem::createDir($this->profileRoot);
foreach ($this->trainableSapis as $sapi) {
FileSystem::createDir($this->rawDir($sapi));
}
}
public function setupCsInstrument(): void
{
foreach ($this->trainableSapis as $sapi) {
if (!is_file($this->profDataFile($sapi))) {
throw new WrongUsageException(
"PGO --phase=cs-instrument: missing {$sapi}.profdata; run --phase=instrument and --phase=use first"
);
}
FileSystem::createDir($this->csRawDir($sapi));
}
}
public function mergeProfiles(): void
{
foreach ($this->trainableSapis as $sapi) {
$this->mergeSapi($sapi);
}
}
private function mergeSapi(string $sapi): void
{
$raws = glob($this->rawDir($sapi) . '/*.profraw') ?: [];
$csRaws = glob($this->csRawDir($sapi) . '/*.profraw') ?: [];
if ($raws === [] && $csRaws === []) {
if ($sapi === 'frankenphp') {
logger()->warning(
'PGO --phase=use: no .profraw for frankenphp (cgo-glue PGO will be skipped); ' .
'run --phase=instrument, exercise frankenphp longer, then re-run --phase=use'
);
return;
}
throw new WrongUsageException(
"PGO --phase=use: no .profraw for {$sapi}; run --phase=instrument, exercise the binary, then re-run --phase=use"
);
}
$out = $this->profDataFile($sapi);
$inputs = array_merge($raws, $csRaws);
$argv = implode(' ', array_map('escapeshellarg', $inputs));
$profdata = PKG_ROOT_PATH . '/llvm-tools/bin/llvm-profdata';
shell()->exec(escapeshellarg($profdata) . ' merge --failure-mode=warn -output=' . escapeshellarg($out) . ' ' . $argv);
if (!is_file($out) || filesize($out) === 0) {
throw new WrongUsageException("PGO --phase=use: empty merge output for {$sapi}");
}
logger()->info("PGO merged {$sapi}: " . filesize($out) . ' bytes');
}
private static function overwritePgoFlags(string $var, string $append): void
{
$cur = (string) getenv($var);
$cur = preg_replace('/\s*-f(cs-)?profile-(generate|use)=\S+/', '', $cur) ?? $cur;
$cur = preg_replace('/\s*-fprofile-update=atomic/', '', $cur) ?? $cur;
$cur = preg_replace('/\s*-Wno-error=profile-instr-\S+/', '', $cur) ?? $cur;
$cur = preg_replace('/\s*-Wno-backend-plugin/', '', $cur) ?? $cur;
f_putenv($var . '=' . trim(trim($cur) . ' ' . $append));
}
}

View File

@@ -67,7 +67,7 @@ class PkgConfigUtil
public static function getCflags(string $pkg_config_str): string
{
// get other things
$result = self::execWithResult("pkg-config --static --cflags {$pkg_config_str}");
$result = self::execWithResult("pkg-config --static --cflags-only-other {$pkg_config_str}");
return trim($result);
}

View File

@@ -5,12 +5,9 @@ declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\SystemTarget;
class SPCConfigUtil
@@ -35,82 +32,123 @@ class SPCConfigUtil
$this->absolute_libs = $options['absolute_libs'] ?? false;
}
public function config(array $packages = []): array
public function config(array $packages = [], bool $include_suggests = false): array
{
// Walk depends+suggests within the resolved set; reaching `php` fans out to its
// effective link closure (resolved static exts + virtual-target SAPIs).
$installer = ApplicationContext::get(PackageInstaller::class);
$resolved_set = array_flip(array_keys($installer->getResolvedPackages()));
$php_extras = [];
// if have php, make php as all extension's dependency
if (!$this->no_php) {
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) {
if ($ext->isBuildStatic()) {
$php_extras[] = $ext->getName();
}
$dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))];
} else {
$dep_override = [];
}
$resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests);
$ldflags = $this->getLdflagsString();
$cflags = $this->getIncludesString($resolved);
$libs = $this->getLibsString($resolved, !$this->absolute_libs);
// additional OS-specific libraries (e.g. macOS -lresolv)
// embed
if ($extra_libs = SystemTarget::getRuntimeLibs()) {
$libs .= " {$extra_libs}";
}
$extra_env = getenv('SPC_EXTRA_LIBS');
if (is_string($extra_env) && !empty($extra_env)) {
$libs .= " {$extra_env}";
}
// package frameworks
if (SystemTarget::getTargetOS() === 'Darwin') {
$libs .= " {$this->getFrameworksString($resolved)}";
}
// C++
if ($this->hasCpp($resolved)) {
$target_os = SystemTarget::getTargetOS();
if ($target_os === 'Darwin') {
$libcpp = '-lc++';
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
} elseif ($target_os !== 'Windows') {
// Linux and other Unix-like systems use libstdc++
$libcpp = '-lstdc++';
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
}
foreach ($installer->getResolvedPackages(TargetPackage::class) as $target) {
if ($target->isVirtualTarget()) {
$php_extras[] = $target->getName();
}
// Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed
}
if ($this->libs_only_deps) {
// mimalloc must come first
if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs);
}
return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs),
];
}
// embed
if (!$this->no_php) {
if (SystemTarget::getTargetOS() === 'Windows') {
// Windows: use php8embed.lib directly (either full path or short name)
$major = intdiv(PHP_VERSION_ID, 10000);
$php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib";
// Windows system libs required by PHP
// Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib
$libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib";
} else {
$libs = "-lphp {$libs} -lc";
}
}
$sorted = [];
$visited = [];
foreach ($packages as $pkg) {
self::visitConfigDeps(
is_string($pkg) ? $pkg : $pkg->getName(),
$resolved_set,
$php_extras,
$visited,
$sorted,
);
$allLibs = getenv('LIBS') . ' ' . $libs;
// mimalloc must come first
if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs);
}
return $this->configWithResolvedPackages($sorted);
return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces($allLibs),
];
}
/**
* [Helper function]
* Get configuration for a specific extension(s) dependencies.
*
* Uses the installer's resolved package set as the source of truth — only libraries that
* are actually enabled in this build appear in the result. The resolved set already
* reflects the user's `--with-suggests` choice.
*
* @param array|PhpExtensionPackage $extension_packages Extension instance or list
* @return array{
* cflags: string,
* cxxflags: string,
* ldflags: string,
* libs: string
* }
*/
public function getExtensionConfig(array|PhpExtensionPackage $extension_packages): array
public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array
{
if (!is_array($extension_packages)) {
$extension_packages = [$extension_packages];
}
$names = array_map(fn ($y) => $y->getName(), $extension_packages);
return $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names));
return $this->config(
packages: array_map(fn ($y) => $y->getName(), $extension_packages),
include_suggests: $include_suggests,
);
}
/**
* [Helper function]
* Get configuration for a specific library(s) dependencies.
*
* Like {@see getExtensionConfig()}, draws from the resolved package set so we never
* link against a library that wasn't built.
*
* @param array|LibraryPackage $lib Library instance or list
* @param array|LibraryPackage $lib Library instance or list
* @param bool $include_suggests Whether to include suggested libraries
* @return array{
* cflags: string,
* cxxflags: string,
* ldflags: string,
* libs: string
* }
*/
public function getLibraryConfig(array|LibraryPackage $lib): array
public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array
{
if (!is_array($lib)) {
$lib = [$lib];
@@ -119,37 +157,39 @@ class SPCConfigUtil
$this->no_php = true;
$save_libs_only_deps = $this->libs_only_deps;
$this->libs_only_deps = true;
$names = array_map(fn ($y) => $y->getName(), $lib);
$ret = $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names));
$ret = $this->config(
packages: array_map(fn ($y) => $y->getName(), $lib),
include_suggests: $include_suggests,
);
$this->no_php = $save_no_php;
$this->libs_only_deps = $save_libs_only_deps;
return $ret;
}
/**
* Get build configuration for a package's sub-dependencies within a resolved set.
* Get build configuration for a package and its sub-dependencies within a resolved set.
*
* Walks both depends and suggests edges — the resolved set is the filter, so anything
* reachable but unbuilt is naturally excluded. No `include_suggests` knob is needed.
* This is useful when you need to statically link something against a specific
* library and all its transitive dependencies. It properly handles optional
* dependencies by only including those that were actually resolved.
*
* @param string $package_name The package to get config for
* @param string[] $resolved_packages The full resolved package list
* @param bool $include_suggests Whether to include resolved suggests
* @return array{
* cflags: string,
* cxxflags: string,
* ldflags: string,
* libs: string
* }
*/
public function getPackageDepsConfig(string $package_name, array $resolved_packages): array
public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array
{
// Get sub-dependencies within the resolved set
$sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, include_suggests: true);
$sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests);
if (empty($sub_deps)) {
return [
'cflags' => '',
'cxxflags' => '',
'ldflags' => '',
'libs' => '',
];
@@ -170,12 +210,11 @@ class SPCConfigUtil
}
/**
* Build cflags/cxxflags/ldflags/libs for an already-resolved package set (skip dependency resolution).
* Get configuration using already-resolved packages (skip dependency resolution).
*
* @param string[] $resolved_packages Resolved package names in build order
* @param string[] $resolved_packages Already resolved package names in build order
* @return array{
* cflags: string,
* cxxflags: string,
* ldflags: string,
* libs: string
* }
@@ -183,14 +222,9 @@ class SPCConfigUtil
public function configWithResolvedPackages(array $resolved_packages): array
{
$ldflags = $this->getLdflagsString();
$includes = $this->getIncludesString($resolved_packages);
$cflags = $this->getIncludesString($resolved_packages);
$libs = $this->getLibsString($resolved_packages, !$this->absolute_libs);
// includes (-I…) are language-agnostic — same source for cflags/cxxflags, swap only the env names
$cflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CFLAGS') ?: '') . ' ' . getenv('CFLAGS') . ' ' . $includes));
$cxxflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CXXFLAGS') ?: '') . ' ' . getenv('CXXFLAGS') . ' ' . $includes));
$ldflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_LDFLAGS') ?: '') . ' ' . getenv('LDFLAGS') . ' ' . $ldflags));
// additional OS-specific libraries (e.g. macOS -lresolv)
if ($extra_libs = SystemTarget::getRuntimeLibs()) {
$libs .= " {$extra_libs}";
@@ -226,25 +260,15 @@ class SPCConfigUtil
$libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs);
}
return [
'cflags' => $cflags,
'cxxflags' => $cxxflags,
'ldflags' => $ldflags,
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs),
];
}
// embed
if (!$this->no_php) {
if (SystemTarget::getTargetOS() === 'Windows') {
// Windows: use php8embed.lib directly (either full path or short name)
$major = intdiv(PHP_VERSION_ID, 10000);
$php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib";
// Windows system libs required by PHP
// Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib
$libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib";
} else {
$libs = "-lphp {$libs} -lc";
}
$libs = "-lphp {$libs} -lc";
}
$allLibs = getenv('LIBS') . ' ' . $libs;
@@ -255,9 +279,8 @@ class SPCConfigUtil
}
return [
'cflags' => $cflags,
'cxxflags' => $cxxflags,
'ldflags' => $ldflags,
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces($allLibs),
];
}
@@ -276,50 +299,6 @@ class SPCConfigUtil
return implode(' ', $list);
}
private static function visitConfigDeps(
string $name,
array $resolved_set,
array $php_extras,
array &$visited,
array &$sorted,
): void {
if (isset($visited[$name]) || !isset($resolved_set[$name])) {
return;
}
$visited[$name] = true;
$deps = array_merge(
PackageConfig::get($name, 'depends', []),
PackageConfig::get($name, 'suggests', []),
);
if ($name === 'php') {
$deps = array_merge($deps, $php_extras);
}
foreach ($deps as $dep) {
self::visitConfigDeps($dep, $resolved_set, $php_extras, $visited, $sorted);
}
$sorted[] = $name;
}
/**
* For each input package name, gather its transitive deps within the installer's resolved
* set (walking depends + suggests edges), plus the package itself, deduped and in build order.
*
* @param string[] $package_names Input package names
* @return string[] Resolved packages to link against
*/
private function collectEnabledLinkPackages(array $package_names): array
{
$resolved = array_keys(ApplicationContext::get(PackageInstaller::class)->getResolvedPackages());
$out = [];
foreach ($package_names as $name) {
$sub = DependencyResolver::getSubDependencies($name, $resolved, include_suggests: true);
$out = [...$out, ...$sub, $name];
}
return array_values(array_unique($out));
}
private function hasCpp(array $packages): bool
{
foreach ($packages as $package) {

View File

@@ -168,9 +168,6 @@ class SourcePatcher
*/
public static function patchMicroPhar(int $version_id): void
{
if (file_exists(SOURCE_PATH . '/php-src/ext/phar/phar.c.bak')) {
return;
}
FileSystem::backupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c');
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/phar/phar.c',
@@ -196,12 +193,6 @@ class SourcePatcher
public static function unpatchMicroPhar(): void
{
// Tolerate a missing .bak: both drivers call this, and the first restore
// consumes the backup. Without this guard the second call throws
// "Cannot find bak file". No .bak means the source is already pristine.
if (!file_exists(SOURCE_PATH . '/php-src/ext/phar/phar.c.bak')) {
return;
}
FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c');
}

View File

@@ -256,30 +256,10 @@ function clean_spaces(string $string): string
*/
function deduplicate_flags(string $flags): string
{
// Flags that take their value as a separate token.
static $paired = [
'-Xclang', '-Xpreprocessor', '-Xlinker', '-Xassembler',
'-framework', '-arch', '-target',
'-include', '-imacros', '-isystem', '-isysroot', '-iquote', '-idirafter',
'-MT', '-MF', '-MQ',
];
$tokens = preg_split('/\s+/', trim($flags)) ?: [];
// Group paired flag+value into a single atom before dedup.
$atoms = [];
$n = count($tokens);
for ($i = 0; $i < $n; ++$i) {
if (in_array($tokens[$i], $paired, true) && $i + 1 < $n) {
$atoms[] = $tokens[$i] . ' ' . $tokens[$i + 1];
++$i;
} else {
$atoms[] = $tokens[$i];
}
}
$tokens = preg_split('/\s+/', trim($flags));
// Reverse, unique, reverse back - keeps last occurrence of duplicates
$deduplicated = array_reverse(array_unique(array_reverse($atoms)));
$deduplicated = array_reverse(array_unique(array_reverse($tokens)));
return implode(' ', $deduplicated);
}

View File

@@ -72,6 +72,7 @@ putenv('PKG_ROOT_PATH=' . PKG_ROOT_PATH);
putenv('SOURCE_PATH=' . SOURCE_PATH);
putenv('DOWNLOAD_PATH=' . DOWNLOAD_PATH);
putenv('CPU_COUNT=' . CPU_COUNT);
putenv('SPC_ARCH=' . php_uname('m'));
putenv('GNU_ARCH=' . GNU_ARCH);
putenv('MAC_ARCH=' . MAC_ARCH);

View File

@@ -1,15 +0,0 @@
--- a/frankenphp.c
+++ b/frankenphp.c
@@ -1254,6 +1254,12 @@
go_frankenphp_shutdown_main_thread();
+ /* spc-pgo: explicit profile flush so the cgo-instrumented frankenphp
+ * still writes .profraw on Go-runtime exit (which bypasses libc atexit).
+ * Weak symbol → no-op in non-PGO builds. */
+ { extern int __llvm_profile_write_file(void) __attribute__((weak));
+ if (__llvm_profile_write_file) __llvm_profile_write_file(); }
+
return NULL;
}

View File

@@ -1,12 +0,0 @@
--- a/main/main.c
+++ b/main/main.c
@@ -2563,6 +2563,9 @@
#endif
zend_observer_shutdown();
+
+ { extern int __llvm_profile_write_file(void) __attribute__((weak));
+ if (__llvm_profile_write_file) __llvm_profile_write_file(); }
}
/* }}} */

View File

@@ -1,14 +1,9 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
BUILDROOT_INC="${BUILD_INCLUDE_PATH:-$SCRIPT_DIR/../../../buildroot/include}"
BUILDROOT_ABS="$(realpath "$BUILDROOT_INC" 2>/dev/null || true)"
BUILDROOT_ABS="${BUILD_ROOT_PATH:-$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)}"
PARSED_ARGS=()
is_buildroot_inc() {
[[ -n "$BUILDROOT_ABS" && "$1" == "$BUILDROOT_ABS" ]]
}
while [[ $# -gt 0 ]]; do
case "$1" in
-isystem)
@@ -16,37 +11,27 @@ while [[ $# -gt 0 ]]; do
ARG="$1"
shift
ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)"
is_buildroot_inc "$ARG_ABS" && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG")
[[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG")
;;
-isystem*)
ARG="${1#-isystem}"
shift
ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)"
is_buildroot_inc "$ARG_ABS" && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG")
[[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG")
;;
-march=*|-mcpu=*)
OPT_NAME="${1%%=*}"
OPT_VALUE="${1#*=}"
# zig rejects -march=armv8-a but accepts -mcpu=generic+v8a; rewrite
# armv<X>[.<Y>]-a[+feat] -> generic+v<X>[_<Y>]a[+feat] so it goes through.
if [[ "$OPT_VALUE" =~ ^armv([89])(\.([0-9]+))?-a(\+.*)?$ ]]; then
arch_feat="v${BASH_REMATCH[1]}"
[[ -n "${BASH_REMATCH[3]}" ]] && arch_feat="${arch_feat}_${BASH_REMATCH[3]}"
OPT_VALUE="generic+${arch_feat}a${BASH_REMATCH[4]}"
# Skip armv8- flags entirely as Zig doesn't support them
if [[ "$OPT_VALUE" == armv8-* ]]; then
shift
continue
fi
# zig uses snake_case in CPU/feature names (x86-64 -> x86_64).
# replace -march=x86-64 with -march=x86_64
OPT_VALUE="${OPT_VALUE//-/_}"
PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}")
shift
;;
-mtune=generic)
PARSED_ARGS+=("-mtune=baseline")
shift
;;
-Wlogical-op|-Wduplicated-cond|-Wduplicated-branches|-Wno-clobbered|-Wjump-misses-init|-Wformat-truncation|-Warray-bounds=*|-Wimplicit-fallthrough=*)
# GCC-only warning flags that clang/zig doesn't recognize; drop to silence -Wunknown-warning-option noise
shift
;;
*)
PARSED_ARGS+=("$1")
shift
@@ -54,29 +39,6 @@ while [[ $# -gt 0 ]]; do
esac
done
IS_LINK=1
NEED_PROFILE_RT=0 # https://codeberg.org/ziglang/zig/issues/32066
NEED_CRT=0 # https://codeberg.org/ziglang/zig/issues/32064
for _a in "${PARSED_ARGS[@]}"; do
case "$_a" in
-c|-S|-E|-M|-MM) IS_LINK=0 ;;
-fprofile-generate*|-fprofile-instr-generate*|-fcs-profile-generate*) NEED_PROFILE_RT=1 ;;
-shared) NEED_CRT=1 ;;
esac
done
[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* || "$SPC_COMPILER_EXTRA" == *-fcs-profile-generate* ]] && NEED_PROFILE_RT=1
RT_DIR="${SPC_COMPILER_RT_DIR:-}"
if [[ $IS_LINK -eq 1 && $NEED_PROFILE_RT -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/libclang_rt.profile.a" ]]; then
PARSED_ARGS+=("$RT_DIR/libclang_rt.profile.a" "-Wl,-u,__llvm_profile_runtime")
fi
if [[ $IS_LINK -eq 1 && $NEED_CRT -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/clang_rt.crtbegin.o" && -f "$RT_DIR/clang_rt.crtend.o" ]]; then
PARSED_ARGS+=("$RT_DIR/clang_rt.crtbegin.o" "$RT_DIR/clang_rt.crtend.o")
fi
if [[ $IS_LINK -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/libclang_rt.cpu_model.a" ]]; then
PARSED_ARGS+=("$RT_DIR/libclang_rt.cpu_model.a")
fi
[[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET=""
if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then