Compare commits

..

2 Commits

Author SHA1 Message Date
Jerry Ma
5a1fd1f388 Merge branch 'v3' into v3c/artifact-static-helpers 2026-05-29 17:32:02 +09:00
henderkes
91cf4f83b5 artifact: add path/binary/isInstalled static helpers
Give zig, rust, go_win and go_xcaddy a small consistent surface for
locating the install directory and a binary inside it:

- path(): install/extract root for the artifact
- binary($name = '<default>'): full path to a binary under that root,
  picking the artifact's natural layout (top-level for zig, bin/ for
  rust and the go toolchains)
- isInstalled(): is the default binary present on disk

Callers that previously concatenated PKG_ROOT_PATH . '/zig/zig' (and
the equivalents for the other artifacts) by hand can call the helpers
instead, and any later code that needs to ask "is this toolchain
available" can use isInstalled() without rebuilding the path.
2026-05-24 21:39:56 +07:00
36 changed files with 113 additions and 471 deletions

View File

@@ -38,9 +38,6 @@ 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"
@@ -108,12 +105,6 @@ 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

@@ -13,5 +13,4 @@ ext-imagick:
os:
- Linux
- Darwin
- Windows
arg-type: custom

View File

@@ -16,59 +16,13 @@ imagemagick:
- libtiff
- libheif
- bzip2
depends@windows:
- zlib
suggests:
- zstd
- xz
- libzip
- libxml2
headers@windows:
- imagemagick/MagickWand/MagickWand.h
lang: cpp
pkg-configs:
- Magick++-7.Q16HDRI
- MagickCore-7.Q16HDRI
- MagickWand-7.Q16HDRI
static-libs@windows:
- CORE_RL_MagickWand_.lib
- CORE_RL_MagickCore_.lib
- CORE_RL_coders_.lib
- CORE_RL_filters_.lib
- CORE_RL_aom_.lib
- CORE_RL_brotli_.lib
- CORE_RL_bzip2_.lib
- CORE_RL_cairo_.lib
- CORE_RL_croco_.lib
- CORE_RL_de265_.lib
- CORE_RL_exr_.lib
- CORE_RL_ffi_.lib
- CORE_RL_freetype_.lib
- CORE_RL_fribidi_.lib
- CORE_RL_gdk-pixbuf_.lib
- CORE_RL_glib_.lib
- CORE_RL_harfbuzz_.lib
- CORE_RL_heif_.lib
- CORE_RL_highway_.lib
- CORE_RL_imath_.lib
- CORE_RL_jpeg-turbo-12_.lib
- CORE_RL_jpeg-turbo-16_.lib
- CORE_RL_jpeg-turbo_.lib
- CORE_RL_jpeg-xl_.lib
- CORE_RL_lcms_.lib
- CORE_RL_lqr_.lib
- CORE_RL_lzma_.lib
- CORE_RL_openh264_.lib
- CORE_RL_openjpeg_.lib
- CORE_RL_openjph_.lib
- CORE_RL_pango_.lib
- CORE_RL_pixman_.lib
- CORE_RL_png_.lib
- CORE_RL_raqm_.lib
- CORE_RL_raw_.lib
- CORE_RL_rsvg_.lib
- CORE_RL_tiff_.lib
- CORE_RL_webp_.lib
- CORE_RL_xml_.lib
- CORE_RL_zip_.lib
- CORE_RL_zlib_.lib

View File

@@ -11,10 +11,6 @@ libzip:
license: BSD-3-Clause
depends:
- zlib
depends@windows:
- zlib
- bzip2
- xz
suggests:
- bzip2
- xz

View File

@@ -5,6 +5,8 @@ postgresql:
type: ghtagtar
repo: postgres/postgres
match: REL_18_\d+
binary:
windows-x86_64: { type: url, url: 'https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip', extract: { lib/libpq.lib: '{build_root_path}/lib/libpq.lib', lib/libpgport.lib: '{build_root_path}/lib/libpgport.lib', lib/libpgcommon.lib: '{build_root_path}/lib/libpgcommon.lib', include/libpq-fe.h: '{build_root_path}/include/libpq-fe.h', include/postgres_ext.h: '{build_root_path}/include/postgres_ext.h', include/pg_config_ext.h: '{build_root_path}/include/pg_config_ext.h', include/libpq/libpq-fs.h: '{build_root_path}/include/libpq/libpq-fs.h' } }
metadata:
license-files: ['@/postgresql.txt']
license: PostgreSQL
@@ -14,9 +16,6 @@ postgresql:
- openssl
- zlib
- libedit
depends@windows:
- openssl
- zlib
suggests@unix:
- icu
- libxslt

View File

@@ -15,6 +15,23 @@ use StaticPHP\Util\GlobalEnvManager;
class go_win
{
/** GOROOT for the Windows Go toolchain. */
public static function path(): string
{
return PKG_ROOT_PATH . '/go-win';
}
/** Path to a binary inside go-win's bin/ (go.exe, gofmt.exe, …). */
public static function binary(string $name = 'go.exe'): string
{
return self::path() . '/bin/' . $name;
}
public static function isInstalled(): bool
{
return is_file(self::binary());
}
#[CustomBinary('go-win', [
'windows-x86_64',
])]

View File

@@ -17,6 +17,23 @@ use StaticPHP\Util\System\LinuxUtil;
class go_xcaddy
{
/** GOROOT for the bundled Go toolchain used to build xcaddy. */
public static function path(): string
{
return PKG_ROOT_PATH . '/go-xcaddy';
}
/** Path to a binary inside go-xcaddy's bin/ (xcaddy, go, …). */
public static function binary(string $name = 'xcaddy'): string
{
return self::path() . '/bin/' . $name;
}
public static function isInstalled(): bool
{
return is_file(self::binary());
}
#[CustomBinary('go-xcaddy', [
'linux-x86_64',
'linux-aarch64',

View File

@@ -16,6 +16,23 @@ use StaticPHP\Util\System\LinuxUtil;
class rust
{
/** Install prefix the rust tarball's install.sh writes into. */
public static function path(): string
{
return PKG_ROOT_PATH . '/rust';
}
/** Path to a binary inside the rust install dir (cargo, rustc, rustup, …). */
public static function binary(string $name = 'cargo'): string
{
return self::path() . '/bin/' . $name;
}
public static function isInstalled(): bool
{
return is_file(self::binary());
}
#[CustomBinary('rust', [
'linux-x86_64',
'linux-aarch64',

View File

@@ -15,6 +15,23 @@ 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',

View File

@@ -4,17 +4,12 @@ declare(strict_types=1);
namespace Package\Extension;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\CustomPhpConfigureArg;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Util\FileSystem;
#[Extension('imagick')]
class imagick extends PhpExtensionPackage
class imagick
{
#[CustomPhpConfigureArg('Darwin')]
#[CustomPhpConfigureArg('Linux')]
@@ -23,35 +18,4 @@ class imagick extends PhpExtensionPackage
$disable_omp = ' ac_cv_func_omp_pause_resource_all=no';
return '--with-imagick=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . $disable_omp;
}
#[CustomPhpConfigureArg('Windows')]
public function getWindowsConfigureArg(bool $shared): string
{
// config.w32 uses PHP_IMAGICK as an extra search path for CORE_RL_*.lib; the static
// ImageMagick libs are installed flat in buildroot/lib (headers in buildroot/include/imagemagick).
return '--with-imagick=' . BUILD_LIB_PATH;
}
#[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-imagick')]
#[PatchDescription('Add the Win32 system libraries the static ImageMagick stack needs')]
public function patchConfigW32ForWindows(): void
{
$config = $this->getSourceDir() . '/config.w32';
// Idempotency guard (the source dir may be patched in place and reused across builds).
if (str_contains(FileSystem::readFile($config), 'LIBS_IMAGICK')) {
return;
}
// The static ImageMagick stack needs several Win32 system libraries (GDI+, WIC, urlmon, ...)
// that aren't already pulled in by the other extensions. (imagick itself builds as plain C:
// ImageMagick is built with a 32-bit channel mask, see imagemagick.php buildWin, so the
// MagickCore headers don't require a C++ translation unit.)
FileSystem::replaceFileStr(
$config,
"AC_DEFINE('HAVE_IMAGICK', 1);",
'ADD_FLAG("LIBS_IMAGICK", "gdiplus.lib urlmon.lib msimg32.lib oleaut32.lib windowscodecs.lib iphlpapi.lib");' . "\n\t\t" .
"AC_DEFINE('HAVE_IMAGICK', 1);"
);
}
}

View File

@@ -22,7 +22,6 @@ class gettext_win
{
$ver = WindowsUtil::findVisualStudio();
$vs_ver_dir = match ($ver['major_version']) {
'18', // VS 2026 reuses the VS2022 (MSVC17) solution, which msbuild builds via forward compatibility.
'17' => '\MSVC17',
'16' => '\MSVC16',
default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"),
@@ -45,9 +44,7 @@ class gettext_win
{
$vs_ver_dir = ApplicationContext::get('gettext_win_vs_ver_dir');
cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}\\libintl_static")
// WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot
// read ("is not a native COFF file"); disable it so the .lib stays plain COFF.
->exec('msbuild libintl_static.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=10.0 /p:WholeProgramOptimization=false');
->exec('msbuild libintl_static.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=10.0');
FileSystem::createDir($lib->getLibDir());
FileSystem::createDir($lib->getIncludeDir());
// libintl_a.lib is the static library output; copy as libintl.lib for linker compatibility

View File

@@ -6,7 +6,6 @@ namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Runtime\SystemTarget;
@@ -16,67 +15,6 @@ use StaticPHP\Util\FileSystem;
#[Library('imagemagick')]
class imagemagick
{
/**
* Build a fully static, self-contained ImageMagick 7 (Q16-HDRI, /MT) on Windows using the
* official VisualMagick build (the ImageMagick/Windows + Configure + Dependencies repos), which
* bundles every delegate. ImageMagick has no autoconf/CMake build on Windows, so this clones the
* VisualMagick tree, generates a static x64 solution via the Configure tool, and builds it with
* msbuild. The resulting CORE_RL_*.lib static libraries + MagickWand/MagickCore headers are
* installed into the build root for ext-imagick to link.
*
* A short working directory is used (VisualMagick's tree is deeply nested and otherwise exceeds
* MAX_PATH); override with SPC_IMAGEMAGICK_BUILD_DIR.
*/
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void
{
$work = getenv('SPC_IMAGEMAGICK_BUILD_DIR') ?: 'C:\im';
$configure_release = '2026.05.30.2033';
$configure_url = "https://github.com/ImageMagick/Configure/releases/download/{$configure_release}/Configure.Release.x64.exe";
FileSystem::createDir($work);
// Clone the VisualMagick repos (ImageMagick source + Configure + Dependencies + all delegates).
if (!is_dir("{$work}\\ImageMagick")) {
cmd()->cd($work)->exec(SPC_GIT_EXEC . ' clone --depth 1 https://github.com/ImageMagick/Windows.git .');
cmd()->cd($work)->exec('bash clone-repositories.sh --imagemagick7');
}
// Use the prebuilt Configure tool (building it from source needs the MFC components).
default_shell()->executeCurlDownload($configure_url, "{$work}\\Configure\\Configure.Release.x64.exe", retries: 2);
// Generate a static, /MT (linkRuntime), x64, Q16-HDRI solution with the configs embedded
// (zeroConfigurationSupport) and OpenMP off (no vcomp runtime dependency).
cmd()->cd("{$work}\\Configure")
->exec('Configure.Release.x64.exe /noWizard /VS2026 /x64 /static /linkRuntime /noOpenMP /zeroConfigurationSupport');
// x64 IM7 defaults to a 64-bit channel mask, whose magick-baseconfig.h #errors unless the
// consuming translation unit is C++. ext-imagick is plain C, so force a 32-bit channel mask
// (ample: 32 channels >> RGBA/CMYK) before building, keeping libs and the installed header in sync.
FileSystem::replaceFileStr(
"{$work}\\ImageMagick\\MagickCore\\magick-baseconfig.h",
'#define MAGICKCORE_CHANNEL_MASK_DEPTH 64',
'#define MAGICKCORE_CHANNEL_MASK_DEPTH 32'
);
cmd()->cd($work)
->exec('msbuild IM7.Static.x64.sln /m /t:Rebuild /nologo /p:Configuration=Release,Platform=x64');
$artifacts = "{$work}\\Artifacts\\lib";
if (!is_dir($artifacts)) {
throw new EnvironmentException('ImageMagick VisualMagick build produced no Artifacts/lib; build failed.');
}
// Install the static libs (flat, onto the build-root lib path) and the public headers.
FileSystem::createDir($lib->getLibDir());
foreach (glob("{$artifacts}\\CORE_RL_*.lib") as $f) {
FileSystem::copy($f, $lib->getLibDir() . '\\' . basename($f));
}
foreach (['MagickWand', 'MagickCore'] as $dir) {
FileSystem::createDir($lib->getIncludeDir() . "\\imagemagick\\{$dir}");
foreach (glob("{$work}\\ImageMagick\\{$dir}\\*.h") as $h) {
FileSystem::copy($h, $lib->getIncludeDir() . "\\imagemagick\\{$dir}\\" . basename($h));
}
}
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function buildUnix(LibraryPackage $lib, ToolchainInterface $toolchain): void

View File

@@ -21,7 +21,6 @@ class libffi_win
{
$ver = WindowsUtil::findVisualStudio();
$vs_ver_dir = match ($ver['major_version']) {
'18', // VS 2026 reuses the vs17 solution, which msbuild builds via forward compatibility.
'17' => '\win32\vs17_x64',
'16' => '\win32\vs16_x64',
default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported!"),
@@ -34,9 +33,7 @@ class libffi_win
{
$vs_ver_dir = ApplicationContext::get('libffi_win_vs_ver_dir');
cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}")
// WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot
// read ("is not a native COFF file"); disable it so the .lib stays plain COFF.
->exec('msbuild libffi-msvc.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WholeProgramOptimization=false');
->exec('msbuild libffi-msvc.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64');
FileSystem::createDir($lib->getLibDir());
FileSystem::createDir($lib->getIncludeDir());

View File

@@ -21,7 +21,6 @@ class libiconv_win
{
$ver = WindowsUtil::findVisualStudio();
$vs_ver_dir = match ($ver['major_version']) {
'18', // VS 2026 reuses the VS2022 (MSVC17) solution, which msbuild builds via forward compatibility.
'17' => '\MSVC17',
'16' => '\MSVC16',
default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"),
@@ -34,9 +33,7 @@ class libiconv_win
{
$vs_ver_dir = ApplicationContext::get('vs_ver_dir');
cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}")
// WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot
// read ("is not a native COFF file"); disable it so the .lib stays plain COFF.
->exec('msbuild libiconv.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WholeProgramOptimization=false');
->exec('msbuild libiconv.sln /t:Rebuild /p:Configuration=Release /p:Platform=x64');
FileSystem::createDir($lib->getLibDir());
FileSystem::createDir($lib->getIncludeDir());
FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\x64\\lib\\libiconv.lib", "{$lib->getLibDir()}\\libiconv.lib");

View File

@@ -42,16 +42,13 @@ class libsodium
{
$ver = WindowsUtil::findVisualStudio();
$vs_ver_dir = match ($ver['major_version']) {
'18', // VS 2026 reuses the vs2022 solution, which msbuild builds via forward compatibility.
'17' => '\vs2022',
'16' => '\vs2019',
default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"),
};
cmd()->cd("{$lib->getSourceDir()}\\builds\\msvc{$vs_ver_dir}")
// WholeProgramOptimization (/GL) emits LTCG objects that frankenphp's lld-link cannot
// read ("is not a native COFF file"); disable it so the .lib stays plain COFF.
->exec('msbuild libsodium.sln /t:Rebuild /p:Configuration=StaticRelease /p:Platform=x64 /p:WholeProgramOptimization=false /p:PreprocessorDefinitions="SODIUM_STATIC=1"');
->exec('msbuild libsodium.sln /t:Rebuild /p:Configuration=StaticRelease /p:Platform=x64 /p:PreprocessorDefinitions="SODIUM_STATIC=1"');
FileSystem::createDir($lib->getLibDir());
FileSystem::createDir($lib->getIncludeDir());

View File

@@ -21,7 +21,6 @@ class mpir
{
$ver = WindowsUtil::findVisualStudio();
$vs_ver_dir = match ($ver['major_version']) {
'18', // VS 2026 reuses the build.vc17 solution, which msbuild builds via forward compatibility.
'17' => '\build.vc17',
'16' => '\build.vc16',
default => throw new EnvironmentException("Current VS version {$ver['major_version']} is not supported yet!"),

View File

@@ -37,10 +37,6 @@ class postgresql extends LibraryPackage
#[PatchDescription('Various patches before building PostgreSQL')]
public function patchBeforeBuild(): bool
{
// These patches target the autoconf/Make build; the Windows build uses Meson (see buildWin).
if (SystemTarget::getTargetOS() === 'Windows') {
return true;
}
// skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries
FileSystem::replaceFileStr("{$this->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';');
// disable shared libs build
@@ -57,72 +53,6 @@ class postgresql extends LibraryPackage
return true;
}
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void
{
$src = $lib->getSourceDir();
$build_root = $lib->getBuildRootPath();
$lib_dir = $lib->getLibDir();
$inc_dir = $lib->getIncludeDir();
$build = "{$src}\\build";
// Export the public pg_char_to_encoding()/pg_encoding_to_char() from libpgcommon.a so a
// statically-linked libpq.a resolves them (PHP's ext/pgsql relies on them too). This mirrors
// the Unix build's -UUSE_PRIVATE_ENCODING_FUNCS patch, but for the Meson build.
FileSystem::replaceFileStr(
"{$src}\\src\\common\\meson.build",
"'c_args': ['-DUSE_PRIVATE_ENCODING_FUNCS'],",
"'c_args': [],"
);
// Fresh Meson build dir (Meson refuses to reuse a dir configured differently).
if (is_dir($build)) {
FileSystem::removeDir($build);
}
// Meson's OpenSSL detection link-tests CRYPTO_new_ex_data; our static libcrypto needs its
// Win32 deps (and zlib, since OpenSSL was built with zlib) on the link line to succeed.
$ld = 'ws2_32.lib gdi32.lib advapi32.lib crypt32.lib user32.lib secur32.lib zlibstatic.lib';
$configure = 'meson setup build'
. ' --prefix=' . escapeshellarg($build_root)
. ' -Ddefault_library=static' // static libpq.a / libpgcommon.a / libpgport.a
. ' -Db_vscrt=mt' // /MT static CRT, matching the rest of the build
. ' -Dssl=openssl'
// Everything libpq doesn't need: keeps deps minimal and avoids server-only detection.
. ' -Dzlib=disabled -Dnls=disabled -Dreadline=disabled -Dicu=disabled'
. ' -Dlz4=disabled -Dzstd=disabled -Dtap_tests=disabled'
. ' -Dplperl=disabled -Dplpython=disabled -Dpltcl=disabled'
. ' -Dgssapi=disabled -Dldap=disabled -Dlibxml=disabled -Dlibxslt=disabled'
. ' -Dextra_include_dirs=' . escapeshellarg("{$build_root}\\include")
. ' -Dextra_lib_dirs=' . escapeshellarg($lib_dir);
// Build only the three frontend static libs (not the server) — keeps it fast and avoids
// needing every backend dependency. meson/ninja/win_bison/win_flex/perl come from PATH.
$targets = 'src/interfaces/libpq/libpq.a src/common/libpgcommon.a src/port/libpgport.a';
cmd()->cd($src)
->setEnv([
'LIB' => $lib_dir . ';' . (getenv('LIB') ?: ''),
'LDFLAGS' => $ld,
])
->exec($configure)
->exec("ninja -C build {$targets}");
// Install the static libs under the names PHP's ext/pgsql + frankenphp expect (.lib).
FileSystem::createDir($lib_dir);
FileSystem::createDir($inc_dir);
FileSystem::copy("{$build}\\src\\interfaces\\libpq\\libpq.a", "{$lib_dir}\\libpq.lib");
FileSystem::copy("{$build}\\src\\common\\libpgcommon.a", "{$lib_dir}\\libpgcommon.lib");
FileSystem::copy("{$build}\\src\\port\\libpgport.a", "{$lib_dir}\\libpgport.lib");
// Install the public libpq headers (PG18 no longer ships pg_config_ext.h).
FileSystem::copy("{$src}\\src\\interfaces\\libpq\\libpq-fe.h", "{$inc_dir}\\libpq-fe.h");
FileSystem::copy("{$src}\\src\\include\\postgres_ext.h", "{$inc_dir}\\postgres_ext.h");
FileSystem::createDir("{$inc_dir}\\libpq");
FileSystem::copy("{$src}\\src\\include\\libpq\\libpq-fs.h", "{$inc_dir}\\libpq\\libpq-fs.h");
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void

View File

@@ -20,8 +20,8 @@ class unixodbc extends LibraryPackage
{
$sysconf_selector = match ($os = SystemTarget::getTargetOS()) {
'Darwin' => match (SystemTarget::getTargetArch()) {
'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',
'x86_64' => '/usr/local/etc',
'aarch64' => '/opt/homebrew/etc',
default => throw new WrongUsageException('Unsupported architecture: ' . GNU_ARCH),
},
'Linux' => '/etc',

View File

@@ -35,22 +35,12 @@ class curl
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void
{
$lib_dir = str_replace('\\', '/', $lib->getLibDir());
// Pass zstd's import library by absolute path. A bare name ("zstd_static.lib") lands on the
// link line unresolved and MSVC looks for it relative to the curl build dir (LNK1181).
$zstd_lib = "{$lib_dir}/zstd_static.lib";
// libssh2 uses the OpenSSL crypto backend, but this curl build links Schannel and never
// find_package(OpenSSL), so libcrypto/libssl are absent from the link line and libssh2's
// EVP_*/RAND/PEM/ERR symbols go unresolved. Append them (plus the Win32 libs OpenSSL needs,
// mirroring openssl.php) to every target. MSVC's linker resolves regardless of order.
$extra_libs = "{$lib_dir}/libcrypto.lib {$lib_dir}/libssl.lib ws2_32.lib gdi32.lib advapi32.lib crypt32.lib user32.lib";
WindowsCMakeExecutor::create($lib)
->optionalPackage('zstd', ...cmake_boolean_args('CURL_ZSTD'))
->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI'))
->addConfigureArgs(
'-DBUILD_CURL_EXE=ON',
'-DCMAKE_C_STANDARD_LIBRARIES=' . escapeshellarg($extra_libs),
'-DZSTD_LIBRARY=' . escapeshellarg($zstd_lib),
'-DZSTD_LIBRARY=zstd_static.lib',
'-DBUILD_TESTING=OFF',
'-DBUILD_EXAMPLES=OFF',
'-DUSE_LIBIDN2=OFF',

View File

@@ -233,21 +233,10 @@ trait frankenphp
$dep_libs = array_unique($dep_libs);
$lib_dir = str_replace('\\', '/', BUILD_LIB_PATH);
$php_embed_lib = "-lphp{$major}embed";
// pathcch: PathCchCanonicalizeEx etc. used by frankenphp/caddy path handling.
// secur32: InitSecurityInterfaceA (curl Schannel/SSPI). crypt32/gdi32: OpenSSL + Schannel.
$win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt -lpathcch -lsecur32 -lcrypt32 -lgdi32';
$win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt';
$cgo_ldflags = clean_spaces(implode(' ', array_filter([
"-L{$lib_dir}",
$php_embed_lib,
// FrankenPHP's cgo code references PHP/lexbor/zend symbols via __declspec(dllimport).
// Their definitions live in php{N}embed.lib but are only pulled in if the plain symbol
// is referenced, so the __imp_ refs go unresolved. Force-include one symbol from each
// defining object (zend_atomic.obj, lexbor url.obj, lexbor idna.obj) to pull them in;
// lld then auto-imports the __imp_ refs. (/WHOLEARCHIVE would also drag in libxml2.res,
// which collides with Go's own resource object: "more than one resource obj file".)
'-Wl,/INCLUDE:zend_atomic_bool_store',
'-Wl,/INCLUDE:lxb_url_parse',
'-Wl,/INCLUDE:lxb_unicode_idna_init',
implode(' ', $dep_libs),
$win_sys_libs,
'-llibcmt',

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Package\Target\php;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Stage;
@@ -109,10 +108,7 @@ trait windows
throw new PatchException('Windows Makefile patching for php.exe target', 'Cannot patch windows CLI Makefile, Makefile does not contain "$(BUILD_DIR)\php.exe:" line');
}
$lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest';
// /FORCE:MULTIPLE: extensions may bundle their own static copies of common libraries (e.g.
// imagick's ImageMagick ships its own zlib/png/jpeg, duplicating gd's); let the first
// definition win instead of failing with LNK2005. /ignore:4006 silences the resulting noise.
$lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286 /FORCE:MULTIPLE /ignore:4006';
$lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286';
FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines));
}
@@ -516,7 +512,6 @@ HEADER;
$vc_matches = ['unknown', 'unknown'];
} else {
$vc_matches = match ($vc['major_version']) {
'18', // VS 2026 shares the VS2022 (v143) runtime conventions, so it reports as VS17.
'17' => ['VS17', 'Visual C++ 2022'],
'16' => ['VS16', 'Visual C++ 2019'],
default => ['unknown', 'unknown'],
@@ -700,18 +695,12 @@ C_CODE;
// MSVC cl.exe format: compiler flags must come before /link, linker flags after
// ldflags contains /LIBPATH which must be after /link
// /FORCE:MULTIPLE: in ZTS mode both zend.obj and php_embed.obj (both packed into the fat php8embed.lib) define _tsrm_ls_cache as a __declspec(thread) variable.
// /INCLUDE: php8embed.lib's ext/uri (uri_parser_whatwg.obj) references lexbor lxb_url_*/
// lxb_unicode_idna_* via __declspec(dllimport); their definitions live in url.obj/idna.obj
// but are only pulled in if the plain symbol is referenced. Force-include one symbol from
// each so the objects are linked and the __imp_ refs auto-import. (FrankenPHP needs the same.)
// System libs add pathcch (PathCchCanonicalizeEx), secur32 (curl Schannel InitSecurityInterface),
// crypt32/gdi32 (OpenSSL + Schannel) on top of the Makefile LIBS set.
$compile_cmd = sprintf(
'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /INCLUDE:lxb_url_parse /INCLUDE:lxb_unicode_idna_init /LIBPATH:"%s\lib" %s %s',
'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /LIBPATH:"%s\lib" %s %s',
$include_flags,
BUILD_ROOT_PATH,
$config['libs'],
'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib pathcch.lib secur32.lib crypt32.lib gdi32.lib' // Windows system libs (match Makefile LIBS) + curl/openssl deps
'kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib' // Windows system libs (match Makefile LIBS)
);
// Log command explicitly (workaround for cmd() not logging complex commands properly)
@@ -725,11 +714,9 @@ C_CODE;
);
}
// Run the embed test. Use a ".\" prefix: cmd.exe does not resolve a bare "embed.exe" from
// the current directory, while the cwd must remain $test_dir so the script's relative
// "embed.php" is found.
// Run the embed test
InteractiveTerm::setMessage('Running php-embed run smoke test');
[$ret, $output] = cmd()->cd($test_dir)->execWithResult('.\embed.exe');
[$ret, $output] = cmd()->cd($test_dir)->execWithResult('embed.exe');
$raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new ValidationException(

View File

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

View File

@@ -111,7 +111,7 @@ class ExceptionHandler
private static function logError($message, int $indent_space = 0, bool $output_log = true, string $color = 'red'): void
{
$spc_log = spc_log_stream(SPC_OUTPUT_LOG);
$spc_log = fopen(SPC_OUTPUT_LOG, 'a');
$msg = explode("\n", (string) $message);
foreach ($msg as $v) {
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);

View File

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

View File

@@ -189,10 +189,6 @@ class WindowsCMakeExecutor extends Executor
{
return $this->custom_default_args ?? [
'-A x64',
// CMake 4.x hard-errors on projects requesting compatibility with CMake < 3.5
// (e.g. wineditline). This is the documented escape hatch; modern projects and
// older CMake releases ignore it.
'-DCMAKE_POLICY_VERSION_MINIMUM=3.5',
'-DCMAKE_BUILD_TYPE=Release',
'-DBUILD_SHARED_LIBS=OFF',
'-DBUILD_STATIC_LIBS=ON',

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace StaticPHP\Runtime\Shell;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\InterruptException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Runtime\SystemTarget;
@@ -154,7 +153,7 @@ class DefaultShell extends Shell
$this->logCommandInfo($cmd);
logger()->debug("[TAR EXTRACT] {$cmd}");
$this->passthruTolerateSymlinks($cmd);
$this->passthru($cmd, $this->console_putput);
return true;
}
@@ -199,7 +198,7 @@ class DefaultShell extends Shell
$run = function ($cmd) {
$this->logCommandInfo($cmd);
logger()->debug("[7Z EXTRACT] {$cmd}");
$this->passthruTolerateSymlinks($cmd);
$this->passthru($cmd, $this->console_putput);
};
$extname = FileSystem::extname($archive_path);
@@ -213,67 +212,4 @@ class DefaultShell extends Shell
return true;
}
/**
* Run an extraction command, tolerating symbolic links that the host cannot create.
*
* Windows tar.exe (bsdtar) cannot create the symbolic links some archives ship (e.g. zstd's
* tests/cli-tests/bin/unzstd -> zstd), failing each with "Can't create '...': Invalid argument"
* and exiting non-zero. Those entries are never needed to build, so on Windows we swallow a
* failure whose only errors are such symlink creations and continue. Any other failure still throws.
*/
private function passthruTolerateSymlinks(string $cmd): void
{
// Symlink creation only fails on a Windows host; elsewhere extraction handles symlinks fine.
if (PHP_OS_FAMILY !== 'Windows') {
$this->passthru($cmd, $this->console_putput);
return;
}
$result = $this->passthru($cmd, $this->console_putput, capture_output: true, throw_on_error: false);
if ($result['code'] === 0) {
return;
}
if ($this->isSymlinkOnlyExtractFailure($result['output'])) {
logger()->warning('Some symbolic links could not be created during extraction and were skipped (not supported on this Windows host). This is harmless for building.');
return;
}
throw new ExecutionException(
cmd: $cmd,
message: "Command exited with non-zero code: {$result['code']}",
code: $result['code'],
cd: $this->cd,
env: $this->env,
);
}
/**
* Decide whether an extraction failure was caused solely by symbolic links that could not be
* created on Windows. Returns true only when at least one such error is present and no other
* error-looking output is found, so genuine extraction failures still propagate.
*/
private function isSymlinkOnlyExtractFailure(string $output): bool
{
$saw_symlink_error = false;
foreach (preg_split('/\r\n|\r|\n/', $output) ?: [] as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
// bsdtar's trailing summary line; not an error on its own.
if (str_contains($line, 'Error exit delayed from previous errors')) {
continue;
}
// The symlink (or other unsupported special file) that Windows refused to create.
if (str_contains($line, "Can't create") && str_contains($line, 'Invalid argument')) {
$saw_symlink_error = true;
continue;
}
// Any other error-looking line means this was not a clean symlink-only failure.
if (preg_match('/\berror\b|cannot|can\'t|failed|denied|no space|not permitted/i', $line)) {
return false;
}
}
return $saw_symlink_error;
}
}

View File

@@ -114,8 +114,8 @@ abstract class Shell
if (!$this->enable_log_file) {
return;
}
// write executed command to log file using spc_write_log (shared handle, see spc_log_stream)
$log_file = spc_log_stream(SPC_SHELL_LOG);
// write executed command to log file using spc_write_log
$log_file = fopen(SPC_SHELL_LOG, 'a');
spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n");
spc_write_log($log_file, "> Executing command: {$cmd}\n");
// get the backtrace to find the file and line number
@@ -154,8 +154,8 @@ abstract class Shell
): array {
$file_res = null;
if ($this->enable_log_file) {
// write executed command to the log file using spc_write_log (shared handle, see spc_log_stream)
$file_res = spc_log_stream(SPC_SHELL_LOG);
// write executed command to the log file using spc_write_log
$file_res = fopen(SPC_SHELL_LOG, 'a');
}
if ($console_output) {
$console_res = STDOUT;
@@ -263,7 +263,9 @@ abstract class Shell
}
fclose($pipes[1]);
fclose($pipes[2]);
// $file_res is a shared, process-wide handle (see spc_log_stream); do not close it here.
if ($file_res !== null) {
fclose($file_res);
}
proc_close($process);
}
}

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
<?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

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

View File

@@ -41,7 +41,6 @@ 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

@@ -16,7 +16,6 @@ class ZigToolchain implements UnixToolchainInterface
GlobalEnvManager::putenv('SPC_DEFAULT_CC=zig-cc');
GlobalEnvManager::putenv('SPC_DEFAULT_CXX=zig-c++');
GlobalEnvManager::putenv('SPC_DEFAULT_AR=zig-ar');
GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=zig-ranlib');
GlobalEnvManager::putenv('SPC_DEFAULT_LD=zig-ld.lld');
// Generate additional objects needed for zig toolchain

View File

@@ -134,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', '/opt/local/bin/bison'])) {
if ($bison = MacOSUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) {
self::putenv("BISON={$bison}");
}
if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin', '/opt/local/bin/yacc'])) {
if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) {
self::putenv("YACC={$yacc}");
}
}

View File

@@ -66,10 +66,11 @@ if (filter_var(getenv('SPC_ENABLE_LOG_FILE'), FILTER_VALIDATE_BOOLEAN)) {
}
}
// Use a single shared handle (see spc_log_stream) so the file is opened exactly once;
// on Windows a second concurrent open fails while child processes hold an inherited handle.
$ob_logger->addLogCallback(function ($level, $output) {
spc_write_log(spc_log_stream(SPC_OUTPUT_LOG), strip_ansi_colors($output) . "\n");
$log_file_fd = fopen(SPC_OUTPUT_LOG, 'a');
$ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) {
if ($log_file_fd) {
spc_write_log($log_file_fd, strip_ansi_colors($output) . "\n");
}
return true;
});
}

View File

@@ -137,11 +137,6 @@ function spc_add_log_filter(array|string $filter): void
function spc_write_log(mixed $stream, string $data): false|int
{
// Defensive: a log stream may be false/null when its file could not be opened
// (e.g. transient sharing violations on Windows). Never let logging crash the run.
if (!is_resource($stream)) {
return false;
}
// get filter
global $spc_log_filters;
if (is_array($spc_log_filters)) {
@@ -150,29 +145,6 @@ function spc_write_log(mixed $stream, string $data): false|int
return fwrite($stream, $data);
}
/**
* Return a single, process-wide shared append handle for the given log file.
*
* The handle is opened lazily once and reused for the lifetime of the process. This is
* important on Windows: every time a log file is opened with fopen() its handle is inherited
* by child processes spawned via proc_open() (curl, git, tar, ...). While such a child is
* alive it keeps the file open, and any *additional* open of the same file fails with a
* sharing violation ("The process cannot access the file because it is being used by another
* process."). During parallel downloads many children run at once, so opening a fresh handle
* per log line crashes. Keeping exactly one handle per file means there is never a second open
* to violate. Returns null when the file cannot be opened.
*
* @internal
*/
function spc_log_stream(string $file): mixed
{
static $streams = [];
if (!isset($streams[$file]) || !is_resource($streams[$file])) {
$streams[$file] = @fopen($file, 'a') ?: null;
}
return $streams[$file];
}
// ------- function f_* part -------
// f_ means standard function wrapper