Compare commits

..

34 Commits

Author SHA1 Message Date
Jerry Ma
63ab28097f Merge branch 'v3' into v3c/library-fixes 2026-06-16 13:59:12 +08:00
Jerry Ma
0010e35882 ext: add ext-fastchart and ext-fastjson registry entries (#1165) 2026-06-16 13:57:33 +08:00
crazywhalecc
728c8dd598 Make gmssl standalone 2026-06-16 13:19:03 +08:00
Marc
bf6216e59f Merge branch 'v3' into v3c/ext-fastchart-fastjson 2026-06-14 17:14:37 +07:00
Marc
411ad7cc0f Add macports support (#1179) 2026-06-06 21:06:13 +07:00
Kevin Boyd
0761267eb3 Combine the macos tool checks into a single checkBrewOrPorts method 2026-06-04 21:40:01 -07:00
Jerry Ma
80ae7b093c Merge branch 'v3' into v3c/library-fixes 2026-06-04 16:08:37 +08:00
Jerry Ma
072a3b5505 feat: add permissions for id-token and attestations in release build (#1181) 2026-06-04 10:35:38 +08:00
Marc
0e5738b710 toolchain: add SPC_DEFAULT_RANLIB and pin cmake AR/RANLIB (#1163) 2026-06-04 09:25:42 +07:00
crazywhalecc
3ff9426e50 feat: add permissions for id-token and attestations in release build 2026-06-04 09:51:06 +08:00
Kevin Boyd
6057394641 Simplify macports checks 2026-05-31 22:04:44 -07:00
Kevin Boyd
2f4fb9d28f Add a check for macports alongside Brew in toolchainmanager 2026-05-28 22:40:33 -07:00
Kevin Boyd
96ab2de4b1 Add preliminary MacPorts support 2026-05-28 22:38:56 -07:00
henderkes
daea8e10ad fix https://github.com/php/frankenphp/issues/1346 2026-05-28 12:22:14 +07:00
henderkes
727600a73a disable a whole lot of things we don't need to build 2026-05-26 21:10:00 +07:00
crazywhalecc
c641c3b8db Make mongodb standalone 2026-05-25 10:25:47 +08:00
Jerry Ma
48d6e9ebc2 LinuxMuslCheck: pass tool env via setEnv instead of command prefixes (#1170) 2026-05-24 22:59:47 +08:00
Jerry Ma
6cab47db67 deduplicate_flags: keep paired flag+value tokens together (#1168) 2026-05-24 22:57:23 +08:00
Jerry Ma
ec3fd0f4b0 patch: strip trailing U+200E from spc_fix_avx512_cache_before_80400.p… (#1167) 2026-05-24 22:57:01 +08:00
henderkes
e72f9aa623 LinuxMuslCheck: pass tool env via setEnv instead of command prefixes
Build the CC/CXX/AR/LD/RANLIB map once and hand it to shell()->setEnv()
so the configure/make invocations don't have to repeat the same prefix
on every line. For the sudo make-install path the env still needs to
be on the command line (sudo strips the parent env), so the same map
is rendered into $envFlags and prepended there. Also adds RANLIB,
which the upstream Makefile honours.
2026-05-24 21:40:26 +07:00
henderkes
c666cd6cd0 deduplicate_flags: keep paired flag+value tokens together
deduplicate_flags() split flags on whitespace then ran a per-token
unique. For paired flags like `-Xclang -mllvm` or `-framework Cocoa`,
where the value is a separate token, the value token could collide
with an unrelated flag or value and get dropped, corrupting the
command line.

Group known paired flags (-Xclang, -Xpreprocessor, -Xlinker,
-Xassembler, -framework, -arch, -target, -include, -imacros, -isystem,
-isysroot, -iquote, -idirafter, -MT, -MF, -MQ) with their following
token into a single atom before the unique pass.
2026-05-24 21:38:22 +07:00
henderkes
b8dd508148 patch: strip trailing U+200E from spc_fix_avx512_cache_before_80400.patch
The filename had a Left-To-Right Mark (U+200E) appended invisibly, so
the file is unreachable by code that constructs the path from a plain
ASCII string literal. Rename to the visible name.
2026-05-24 21:38:00 +07:00
henderkes
29a8c9c196 libraries: honour SPC_DEFAULT_CFLAGS/CXXFLAGS/LDFLAGS and bug fixes
bzip2, fastlz, jbig, qdbm: thread SPC_DEFAULT_CFLAGS into the
hand-rolled Makefile patches and shell compile commands. Backward
compatible when the env var is empty.

icu: append SPC_DEFAULT_CXXFLAGS/LDFLAGS to runConfigureICU's CXXFLAGS
and LDFLAGS so user flags reach the bundled cxx invocation.

openssl: append user CFLAGS/LDFLAGS after the target name on Configure
so options like -flto and -fprofile-* reach the openssl build.

libheif: rewrite libheif 1.22+'s C-incompatible
`struct heif_bad_pixel { uint32_t row; uint32_t column; };` as a
typedef so C consumers compile.

ncurses: filter the clang/zig-cc "N warning(s) generated." stdout line
out of MKlib_gen.sh's preprocessor pipe before sed turns it into
invalid C in lib_gen.c.

libaom: detect target CPU from SystemTarget instead of hard-coding
generic, fall back to generic only when neither nasm nor yasm is
available, and turn off examples/tests/tools/docs.
2026-05-24 21:28:31 +07:00
henderkes
37d8b87c3b oops 2026-05-24 21:11:49 +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
5172580294 toolchain: add SPC_DEFAULT_RANLIB and pin cmake AR/RANLIB
ClangBrew, ClangNative and GccNative now export SPC_DEFAULT_RANLIB
alongside SPC_DEFAULT_AR. UnixCMakeExecutor honours both when
generating the Linux toolchain file, so cmake uses the toolchain's
ar/ranlib (e.g. zig-ar/zig-ranlib for archives that the system
ranlib does not understand) instead of /usr/bin/ranlib.
2026-05-24 20:55:35 +07:00
henderkes
99e05aa22b ext: add ext-fastchart and ext-fastjson registry entries
Register two iliaal/fastchart and iliaal/fastjson PHP extensions
sourced from ghtar, extracted into php-src/ext/{fastchart,fastjson}.
Both target Linux and Darwin. fastchart depends on freetype and
suggests libpng, libjpeg, libwebp.
2026-05-24 20:55:11 +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
75 changed files with 531 additions and 1915 deletions

View File

@@ -38,6 +38,9 @@ jobs:
- name: "windows-x64"
os: "ubuntu-latest"
filename: "spc-windows-x64.exe"
permissions:
id-token: write
attestations: write
steps:
- name: "Checkout"
uses: "actions/checkout@v5"
@@ -105,6 +108,12 @@ jobs:
fi
fi
- name: "Generate build provenance attestation"
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v4
with:
subject-path: "${{ github.workspace }}/${{ matrix.operating-system.name == 'windows-x64' && 'spc.exe' || 'spc' }}"
- name: "Copy file"
run: |
if [ "${{ matrix.operating-system.name }}" != "windows-x64" ]; then

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,6 +0,0 @@
llvm-compiler-rt:
type: target
artifact:
binary: custom
depends:
- zig

View File

@@ -1,6 +0,0 @@
llvm-tools:
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

@@ -1,149 +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\DI\ApplicationContext;
use StaticPHP\Exception\BuildFailureException;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Package\PackageBuilder;
class llvm_tools
{
use GitHubTokenSetupTrait;
public const array TOOLS = ['llvm-objcopy', 'llvm-strip', 'llvm-profdata'];
#[CustomBinary('llvm-tools', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$llvmVersion = $this->detectLlvmVersion()
?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first');
$tarball = "llvm-project-{$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-tools', verified: false, version: $llvmVersion);
}
#[CustomBinaryCheckUpdate('llvm-tools', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult
{
$llvmVersion = $this->detectLlvmVersion()
?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first');
return new CheckUpdateResult(
old: $old_version,
new: $llvmVersion,
needUpdate: $old_version === null || $llvmVersion !== $old_version,
);
}
#[AfterBinaryExtract('llvm-tools', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function postExtract(string $target_path): void
{
$this->buildForHost($target_path);
}
public function buildForHost(?string $sourceRoot = null): void
{
$sourceRoot ??= SOURCE_PATH . '/llvm-tools';
$binDir = PKG_ROOT_PATH . '/llvm-tools/bin';
if ($this->allBuilt($binDir)) {
return;
}
$llvmDir = "{$sourceRoot}/llvm";
if (!is_dir($llvmDir)) {
throw new BuildFailureException("llvm-tools: missing source at {$llvmDir} (extraction layout changed?)");
}
$buildDir = "{$sourceRoot}/build";
$installDir = PKG_ROOT_PATH . '/llvm-tools';
f_mkdir($buildDir, recursive: true);
f_mkdir($binDir, recursive: true);
$cmakeArgs = implode(' ', array_map('escapeshellarg', [
'-S', $llvmDir,
'-B', $buildDir,
'-DCMAKE_BUILD_TYPE=Release',
'-DLLVM_ENABLE_PROJECTS=',
'-DLLVM_TARGETS_TO_BUILD=',
'-DLLVM_INCLUDE_BENCHMARKS=OFF',
'-DLLVM_INCLUDE_TESTS=OFF',
'-DLLVM_INCLUDE_EXAMPLES=OFF',
'-DLLVM_INCLUDE_DOCS=OFF',
'-DLLVM_ENABLE_ZLIB=OFF',
'-DLLVM_ENABLE_ZSTD=OFF',
'-DLLVM_ENABLE_LIBXML2=OFF',
'-DLLVM_ENABLE_TERMINFO=OFF',
'-DLLVM_ENABLE_LIBEDIT=OFF',
'-DLLVM_ENABLE_LIBPFM=OFF',
'-DLLVM_BUILD_LLVM_DYLIB=OFF',
'-DLLVM_LINK_LLVM_DYLIB=OFF',
'-DBUILD_SHARED_LIBS=OFF',
'-DCMAKE_C_COMPILER=' . zig::binary('zig-cc'),
'-DCMAKE_CXX_COMPILER=' . zig::binary('zig-c++'),
'-DCMAKE_INSTALL_PREFIX=' . $installDir,
]));
$jobs = ApplicationContext::get(PackageBuilder::class)->concurrency;
$targetArgs = implode(' ', array_map(fn ($t) => '--target ' . escapeshellarg($t), self::TOOLS));
shell()
->setEnv(['SPC_TARGET' => GNU_ARCH . '-linux-musl'])
->exec('cmake ' . $cmakeArgs)
->exec('cmake --build ' . escapeshellarg($buildDir) . ' ' . $targetArgs . " -j {$jobs}");
foreach (self::TOOLS as $t) {
$built = "{$buildDir}/bin/{$t}";
if (!is_file($built)) {
throw new BuildFailureException("llvm-tools: missing build output {$built}");
}
copy($built, "{$binDir}/{$t}");
chmod("{$binDir}/{$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()) {
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;
}
}

View File

@@ -15,23 +15,6 @@ use StaticPHP\Runtime\SystemTarget;
class zig
{
/** Directory zig extracts into. */
public static function path(): string
{
return PKG_ROOT_PATH . '/zig';
}
/** Path to a binary inside the zig install dir (zig, zig-cc, zig-c++, zig-ar, …). */
public static function binary(string $name = 'zig'): string
{
return self::path() . '/' . $name;
}
public static function isInstalled(): bool
{
return is_file(self::binary());
}
#[CustomBinary('zig', [
'linux-x86_64',
'linux-aarch64',
@@ -78,7 +61,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 +110,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

@@ -27,7 +27,10 @@ class brotli
{
UnixCMakeExecutor::create($lib)
->setBuildDir($lib->getSourceDir() . '/build-dir')
->addConfigureArgs("-DSHARE_INSTALL_PREFIX={$lib->getBuildRootPath()}")
->addConfigureArgs(
"-DSHARE_INSTALL_PREFIX={$lib->getBuildRootPath()}",
'-DBROTLI_DISABLE_TESTS=ON',
)
->build();
// Patch pkg-config files

View File

@@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Runtime\SystemTarget;
#[Library('gmp')]
class gmp
@@ -16,10 +17,12 @@ class gmp
#[BuildFor('Darwin')]
public function build(LibraryPackage $lib): void
{
UnixAutoconfExecutor::create($lib)
->appendEnv(['CFLAGS' => '-std=c17'])
->configure('--enable-fat')
->make();
$make = UnixAutoconfExecutor::create($lib)->appendEnv(['CFLAGS' => '-std=c17']);
if (SystemTarget::getTargetArch() === 'x86_64' && SystemTarget::getTargetOS() === 'Linux') {
$libc = SystemTarget::getLibc() === 'glibc' ? 'gnu' : 'musl';
$make->addConfigureArgs(["--host=x86_64-pc-linux-{$libc}"]);
}
$make->configure('--enable-fat')->make();
$lib->patchPkgconfPrefix(['gmp.pc']);
}
}

View File

@@ -45,6 +45,9 @@ class imagemagick
// implicit --with-gcc-arch
// bleeds host cpu features into built binaries
'--without-gcc-arch',
'--disable-docs',
'--without-utilities',
'--disable-dpc',
);
// special: linux-static target needs `-static`

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

@@ -35,6 +35,7 @@ class ldap
->optionalPackage('libsodium', '--with-argon2=libsodium', '--enable-argon2=no')
->addConfigureArgs(
'--disable-slapd',
'--disable-debug',
'--without-systemd',
'--without-cyrus-sasl',
'ac_cv_func_pthread_kill_other_threads_np=no'

View File

@@ -35,7 +35,12 @@ class libavif
->optionalPackage('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF')
->optionalPackage('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF')
->optionalPackage('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF')
->addConfigureArgs('-DAVIF_LIBYUV=OFF')
->addConfigureArgs(
'-DAVIF_LIBYUV=OFF',
'-DAVIF_BUILD_APPS=OFF',
'-DAVIF_BUILD_TESTS=OFF',
'-DAVIF_GTEST=OFF',
)
->build();
// patch pkgconfig
$lib->patchPkgconfPrefix(['libavif.pc']);
@@ -49,7 +54,7 @@ class libavif
'-DAVIF_BUILD_APPS=OFF',
'-DAVIF_BUILD_TESTS=OFF',
'-DAVIF_LIBYUV=OFF',
'-DAVIF_ENABLE_GTEST=OFF',
'-DAVIF_GTEST=OFF',
)
->build();
}

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
@@ -17,7 +16,7 @@ class libffi extends LibraryPackage
public function buildLinux(): void
{
UnixAutoconfExecutor::create($this)
->configure()->make();
->configure('--disable-docs')->make();
if (is_file("{$this->getBuildRootPath()}/lib64/libffi.a")) {
copy("{$this->getBuildRootPath()}/lib64/libffi.a", "{$this->getBuildRootPath()}/lib/libffi.a");
@@ -29,11 +28,12 @@ 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",
"--target={$arch}-apple-darwin",
'--disable-docs',
)
->make();
$this->patchPkgconfPrefix(['libffi.pc']);

View File

@@ -47,6 +47,7 @@ class libheif
'-DWITH_EXAMPLES=OFF',
'-DWITH_GDK_PIXBUF=OFF',
'-DBUILD_TESTING=OFF',
'-DBUILD_DOCUMENTATION=OFF',
'-DWITH_LIBSHARPYUV=ON', // optional: libwebp
'-DENABLE_PLUGIN_LOADING=OFF',
)

View File

@@ -25,6 +25,7 @@ class libjxl extends LibraryPackage
'-DJPEGXL_ENABLE_MANPAGES=OFF',
'-DJPEGXL_ENABLE_BENCHMARK=OFF',
'-DJPEGXL_ENABLE_PLUGINS=OFF',
'-DJPEGXL_ENABLE_DOXYGEN=OFF',
'-DJPEGXL_ENABLE_SJPEG=ON',
'-DJPEGXL_ENABLE_JNI=OFF',
'-DJPEGXL_ENABLE_TRANSCODE_JPEG=ON',

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

@@ -12,17 +12,25 @@ use StaticPHP\Runtime\Executor\UnixCMakeExecutor;
#[Library('libmemcached')]
class libmemcached extends LibraryPackage
{
private const array DISABLE_ARGS = [
'-DBUILD_TESTING=OFF',
'-DBUILD_DOCS=OFF',
'-DENABLE_MEMASLAP=OFF',
];
#[BuildFor('Linux')]
public function buildLinux(): void
{
UnixCMakeExecutor::create($this)
->addConfigureArgs('-DCMAKE_INSTALL_RPATH=""')
->addConfigureArgs('-DCMAKE_INSTALL_RPATH=""', ...self::DISABLE_ARGS)
->build();
}
#[BuildFor('Darwin')]
public function buildDarwin(): void
{
UnixCMakeExecutor::create($this)->build();
UnixCMakeExecutor::create($this)
->addConfigureArgs(...self::DISABLE_ARGS)
->build();
}
}

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')]
@@ -21,11 +20,13 @@ class libpng
{
$args = [
'--enable-hardware-optimizations',
'--disable-tests',
'--disable-tools',
"--with-zlib-prefix={$lib->getBuildRootPath()}",
];
// 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

@@ -13,17 +13,29 @@ use StaticPHP\Runtime\Executor\WindowsCMakeExecutor;
#[Library('librabbitmq')]
class librabbitmq extends LibraryPackage
{
private const array DISABLE_ARGS = [
'-DBUILD_EXAMPLES=OFF',
'-DBUILD_TESTING=OFF',
'-DBUILD_TOOLS=OFF',
'-DBUILD_TOOLS_DOCS=OFF',
'-DBUILD_API_DOCS=OFF',
];
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function buildUnix(): void
{
UnixCMakeExecutor::create($this)->addConfigureArgs('-DBUILD_STATIC_LIBS=ON')->build();
UnixCMakeExecutor::create($this)
->addConfigureArgs('-DBUILD_STATIC_LIBS=ON', ...self::DISABLE_ARGS)
->build();
}
#[BuildFor('Windows')]
public function buildWin(): void
{
WindowsCMakeExecutor::create($this)->build();
WindowsCMakeExecutor::create($this)
->addConfigureArgs(...self::DISABLE_ARGS)
->build();
rename("{$this->getLibDir()}\\librabbitmq.4.lib", "{$this->getLibDir()}\\rabbitmq.4.lib");
}
}

View File

@@ -41,6 +41,9 @@ class libtiff
'--disable-libdeflate',
'--disable-tools',
'--disable-contrib',
'--disable-tests',
'--disable-docs',
'--disable-sphinx',
'--disable-cxx',
'--without-x',
)

View File

@@ -13,12 +13,18 @@ use StaticPHP\Runtime\Executor\WindowsCMakeExecutor;
#[Library('libuv')]
class libuv
{
private const array DISABLE_ARGS = [
'-DLIBUV_BUILD_SHARED=OFF',
'-DLIBUV_BUILD_TESTS=OFF',
'-DLIBUV_BUILD_BENCH=OFF',
];
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function buildUnix(LibraryPackage $lib): void
{
UnixCMakeExecutor::create($lib)
->addConfigureArgs('-DLIBUV_BUILD_SHARED=OFF')
->addConfigureArgs(...self::DISABLE_ARGS)
->build();
// patch pkgconfig
$lib->patchPkgconfPrefix(['libuv-static.pc']);
@@ -28,7 +34,7 @@ class libuv
public function buildWindows(LibraryPackage $lib): void
{
WindowsCMakeExecutor::create($lib)
->addConfigureArgs('-DLIBUV_BUILD_SHARED=OFF')
->addConfigureArgs(...self::DISABLE_ARGS)
->build();
}
}

View File

@@ -34,6 +34,7 @@ class libxslt
'--without-crypto',
'--without-debug',
'--without-debugger',
'--without-profiler',
"--with-libxml-prefix={$lib->getBuildRootPath()}",
);
if (getenv('SPC_LD_LIBRARY_PATH') && getenv('SPC_LIBRARY_PATH')) {

View File

@@ -21,6 +21,7 @@ class mimalloc
->addConfigureArgs(
'-DMI_BUILD_SHARED=OFF',
'-DMI_BUILD_OBJECT=OFF',
'-DMI_BUILD_TESTS=OFF',
'-DMI_INSTALL_TOPLEVEL=ON',
);
if (SystemTarget::getLibc() === 'musl') {

View File

@@ -65,7 +65,6 @@ class ncurses
'--without-tests',
'--without-dlsym',
'--without-debug',
'--disable-stripping',
'--enable-symlinks',
"--with-terminfo-dirs={$terminfo_dirs}",
"--bindir={$package->getBinDir()}",

View File

@@ -24,6 +24,7 @@ class nghttp3
'-DBUILD_SHARED_LIBS=OFF',
'-DENABLE_STATIC_CRT=ON',
'-DENABLE_LIB_ONLY=ON',
'-DBUILD_TESTING=OFF',
)
->build();
}

View File

@@ -25,6 +25,7 @@ class ngtcp2
'-DENABLE_STATIC_CRT=ON',
'-DENABLE_LIB_ONLY=ON',
'-DENABLE_OPENSSL=OFF',
'-DBUILD_TESTING=OFF',
)
->build();
}

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 =
@@ -114,7 +119,7 @@ class openssl
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} " .

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,6 +20,7 @@ class readline
->configure(
'--with-curses',
'--enable-multibyte=yes',
'--disable-install-examples',
)
->make();
$lib->patchPkgconfPrefix(['readline.pc']);

View File

@@ -22,7 +22,8 @@ class tidy
->setBuildDir("{$lib->getSourceDir()}/build-dir")
->addConfigureArgs(
'-DSUPPORT_CONSOLE_APP=OFF',
'-DBUILD_SHARED_LIB=OFF'
'-DBUILD_SHARED_LIB=OFF',
'-DSUPPORT_LOCALIZATIONS=OFF',
);
if (version_compare(get_cmake_version(), '4.0.0', '>=')) {
$cmake->addConfigureArgs('-DCMAKE_POLICY_VERSION_MINIMUM=3.5');
@@ -38,7 +39,8 @@ class tidy
->setBuildDir("{$lib->getSourceDir()}/build-dir")
->addConfigureArgs(
'-DSUPPORT_CONSOLE_APP=OFF',
'-DBUILD_SHARED_LIB=OFF'
'-DBUILD_SHARED_LIB=OFF',
'-DSUPPORT_LOCALIZATIONS=OFF',
)->build();
// rename tidy_static.lib to tidy_a.lib

View File

@@ -20,27 +20,14 @@ class unixodbc extends LibraryPackage
{
$sysconf_selector = match ($os = SystemTarget::getTargetOS()) {
'Darwin' => match (SystemTarget::getTargetArch()) {
'x86_64' => '/usr/local/etc',
'aarch64' => '/opt/homebrew/etc',
'x86_64' => is_dir('/usr/local/etc') ? '/usr/local/etc' : '/opt/local/etc',
'aarch64' => is_dir('/opt/homebrew/etc') ? '/opt/homebrew/etc' : '/opt/local/etc',
default => throw new WrongUsageException('Unsupported architecture: ' . GNU_ARCH),
},
'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,9 @@ 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();
'--enable-readline=no',
)
->make();
$this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']);
$this->patchLaDependencyPrefix();
}

View File

@@ -22,6 +22,11 @@ class xz
->configure(
'--disable-scripts',
'--disable-doc',
'--disable-xz',
'--disable-xzdec',
'--disable-lzmadec',
'--disable-lzmainfo',
'--disable-lzma-links',
'--with-libiconv',
'--bindir=/tmp/xz', // xz binary will corrupt `tar` command, that's really strange.
)

View File

@@ -14,16 +14,20 @@ use StaticPHP\Util\FileSystem;
#[Library('zstd')]
class zstd
{
private const array DISABLE_ARGS = [
'-DZSTD_BUILD_STATIC=ON',
'-DZSTD_BUILD_SHARED=OFF',
'-DZSTD_BUILD_PROGRAMS=OFF',
'-DZSTD_BUILD_TESTS=OFF',
];
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $package): void
{
WindowsCMakeExecutor::create($package)
->setWorkingDir("{$package->getSourceDir()}/build/cmake")
->setBuildDir("{$package->getSourceDir()}/build/cmake/build")
->addConfigureArgs(
'-DZSTD_BUILD_STATIC=ON',
'-DZSTD_BUILD_SHARED=OFF',
)
->addConfigureArgs(...self::DISABLE_ARGS)
->build();
FileSystem::copy($package->getLibDir() . '\zstd_static.lib', $package->getLibDir() . '/zstd.lib');
FileSystem::copy($package->getLibDir() . '\zstd_static.lib', $package->getLibDir() . '/libzstd.lib');
@@ -35,10 +39,7 @@ class zstd
{
UnixCMakeExecutor::create($lib)
->setBuildDir("{$lib->getSourceDir()}/build/cmake/build")
->addConfigureArgs(
'-DZSTD_BUILD_STATIC=ON',
'-DZSTD_BUILD_SHARED=OFF',
)
->addConfigureArgs(...self::DISABLE_ARGS)
->build();
$lib->patchPkgconfPrefix(['libzstd.pc'], PKGCONF_PATCH_PREFIX);

View File

@@ -43,6 +43,8 @@ class curl
'-DZSTD_LIBRARY=zstd_static.lib',
'-DBUILD_TESTING=OFF',
'-DBUILD_EXAMPLES=OFF',
'-DBUILD_LIBCURL_DOCS=OFF',
'-DENABLE_CURL_MANUAL=OFF',
'-DUSE_LIBIDN2=OFF',
'-DCURL_USE_LIBPSL=OFF',
'-DUSE_WINDOWS_SSPI=ON',
@@ -81,6 +83,9 @@ class curl
->addConfigureArgs(
'-DBUILD_CURL_EXE=ON',
'-DBUILD_LIBCURL_DOCS=OFF',
'-DBUILD_TESTING=OFF',
'-DBUILD_EXAMPLES=OFF',
'-DENABLE_CURL_MANUAL=OFF',
'-DOPENSSL_ROOT_DIR=' . BUILD_ROOT_PATH,
)
->build();

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

@@ -60,6 +60,8 @@ class GenExtTestMatrixCommand extends BaseCommand
'glfw',
'imagick',
'intl',
'mongodb',
'gmssl',
];
/**

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

@@ -98,25 +98,6 @@ class ApplicationContext
return self::getContainer()->has($id);
}
/**
* Resolve $id, returning null if it can't be constructed.
* PHP-DI's has() returns true for any autowirable class even when get()
* would throw on missing scalar args — for "is this resolvable right now"
* semantics use this.
*
* @template T
* @param class-string<T> $id
* @return null|T
*/
public static function tryGet(string $id): mixed
{
try {
return self::getContainer()->get($id);
} catch (\Throwable) {
return null;
}
}
/**
* Set a service in the container.
* Use sparingly - prefer configuration-based definitions.

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

@@ -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

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use Package\Artifact\llvm_tools;
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\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
#[OptionalCheck([self::class, 'optionalCheck'])]
class LlvmToolsCheck
{
public static function optionalCheck(): bool
{
return ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain;
}
/** @noinspection PhpUnused */
#[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);
}
return CheckResult::fail('llvm-tools are not built', 'build-llvm-tools');
}
#[FixItem('build-llvm-tools')]
public function fixLlvmTools(): bool
{
$installer = new PackageInstaller(interactive: false);
$installer->addInstallPackage('llvm-tools');
$installer->run(true);
new llvm_tools()->buildForHost();
return new llvm_tools()->allBuilt(PKG_ROOT_PATH . '/llvm-tools/bin');
}
}

View File

@@ -33,15 +33,20 @@ class MacOSToolCheck
'glibtoolize',
];
#[CheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)]
public function checkBrew(): ?CheckResult
#[CheckItem('if homebrew or macports has installed', limit_os: 'Darwin', level: 998)]
public function checkBrewOrPorts(): ?CheckResult
{
if (($path = MacOSUtil::findCommand('brew')) === null) {
return CheckResult::fail('Homebrew is not installed', 'brew');
}
if ($path !== '/opt/homebrew/bin/brew' && getenv('GNU_ARCH') === 'aarch64') {
$brewPath = MacOSUtil::findCommand('brew');
$portPath = MacOSUtil::findCommand('port');
if ($brewPath && $brewPath !== '/opt/homebrew/bin/brew' && getenv('GNU_ARCH') === 'aarch64') {
return CheckResult::fail('Current homebrew (/usr/local/bin/homebrew) is not installed for M1 Mac, please re-install homebrew in /opt/homebrew/ !');
}
if ($brewPath === null && $portPath === null) {
return CheckResult::fail('Homebrew is not installed', 'brew');
}
return CheckResult::ok();
}
@@ -60,8 +65,8 @@ class MacOSToolCheck
return CheckResult::ok();
}
#[CheckItem('if homebrew llvm are installed', limit_os: 'Darwin')]
public function checkBrewLLVM(): ?CheckResult
#[CheckItem('if homebrew or macports llvm are installed', limit_os: 'Darwin')]
public function checkBrewOrPortsLLVM(): ?CheckResult
{
if (getenv('SPC_USE_LLVM') === 'brew') {
$homebrew_prefix = getenv('HOMEBREW_PREFIX') ?: (SystemTarget::getTargetArch() === 'aarch64' ? '/opt/homebrew' : '/usr/local/homebrew');
@@ -71,6 +76,16 @@ class MacOSToolCheck
}
return CheckResult::ok($path);
}
if (getenv('SPC_USE_LLVM') === 'port') {
$macportsPrefix = '/opt/local';
if (($path = MacOSUtil::findCommand('clang', ["{$macportsPrefix}/bin"])) === null) {
return CheckResult::fail('MacPorts llvm is not installed', 'build-tools', ['missing' => ['llvm']]);
}
return CheckResult::ok($path);
}
return null;
}
@@ -91,7 +106,7 @@ class MacOSToolCheck
if ($command_path !== []) {
return CheckResult::fail("Current {$bison} version is too old: " . $matches[0]);
}
return $this->checkBisonVersion(['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin']);
return $this->checkBisonVersion(['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin', '/opt/local/bin']);
}
return CheckResult::ok($matches[0]);
}
@@ -108,6 +123,9 @@ class MacOSToolCheck
#[FixItem('build-tools')]
public function fixBuildTools(array $missing): bool
{
$brewPath = MacOSUtil::findCommand('brew');
$portPath = MacOSUtil::findCommand('port');
$replacement = [
'glibtoolize' => 'libtool',
];
@@ -115,7 +133,18 @@ class MacOSToolCheck
if (isset($replacement[$cmd])) {
$cmd = $replacement[$cmd];
}
shell()->exec('brew install --formula ' . escapeshellarg($cmd));
if ($brewPath !== null) {
shell()->exec('brew install --formula ' . escapeshellarg($cmd));
continue;
}
if ($portPath !== null) {
shell()->exec('port install ' . escapeshellarg($cmd));
continue;
}
return false;
}
return true;
}

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

@@ -11,8 +11,6 @@ use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalPathTrait;
use StaticPHP\Util\InteractiveTerm;
@@ -180,15 +178,14 @@ class PackageBuilder
if (SystemTarget::getTargetOS() === 'Darwin') {
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
} elseif (SystemTarget::getTargetOS() === 'Linux') {
$objcopy = getenv('OBJCOPY') ?: 'objcopy';
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
shell()
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}");
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
} else {
shell()
->exec("{$objcopy} --only-keep-debug {$binary_path} {$debug_file}")
->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}");
->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}")
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
}
} else {
logger()->debug('extractDebugInfo is only supported on Linux and macOS');
@@ -202,12 +199,9 @@ class PackageBuilder
*/
public function stripBinary(string $binary_path): void
{
$strip = ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain
? PKG_ROOT_PATH . '/llvm-tools/bin/llvm-strip'
: 'strip';
shell()->exec(match (SystemTarget::getTargetOS()) {
'Darwin' => "{$strip} -S {$binary_path}",
'Linux' => "{$strip} --strip-unneeded {$binary_path}",
'Darwin' => "strip -S {$binary_path}",
'Linux' => "strip --strip-unneeded {$binary_path}",
'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now
default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'),
});

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

@@ -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

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Toolchain;
use StaticPHP\Util\GlobalEnvManager;
class ClangPortsToolchain extends ClangNativeToolchain
{
public function initEnv(): void
{
$macports_prefix = getenv('MACPORTS_PREFIX') ?: '/opt/local';
GlobalEnvManager::putenv("SPC_DEFAULT_CC={$macports_prefix}/bin/clang");
GlobalEnvManager::putenv("SPC_DEFAULT_CXX={$macports_prefix}/bin/clang++");
GlobalEnvManager::putenv("SPC_DEFAULT_AR={$macports_prefix}/bin/llvm-ar");
GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld');
GlobalEnvManager::addPathIfNotExists("{$macports_prefix}/bin");
}
}

View File

@@ -41,6 +41,7 @@ class ToolchainManager
'Windows' => MSVCToolchain::class,
'Darwin' => match (getenv('SPC_USE_LLVM') ?: 'system') {
'brew' => ClangBrewToolchain::class,
'port' => ClangPortsToolchain::class,
default => ClangNativeToolchain::class,
},
default => throw new WrongUsageException('Unsupported OS family: ' . PHP_OS_FAMILY),

View File

@@ -4,22 +4,12 @@ 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
@@ -28,25 +18,33 @@ class ZigToolchain implements UnixToolchainInterface
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 +55,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 +71,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 +106,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.
@@ -152,10 +134,10 @@ class GlobalEnvManager
}
// test bison
if (PHP_OS_FAMILY === 'Darwin') {
if ($bison = MacOSUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) {
if ($bison = MacOSUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin', '/opt/local/bin/bison'])) {
self::putenv("BISON={$bison}");
}
if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) {
if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin', '/opt/local/bin/yacc'])) {
self::putenv("YACC={$yacc}");
}
}

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

@@ -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