V3 feat/win (#999)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jerry Ma 2025-12-11 16:09:50 +08:00 committed by GitHub
commit c1c31a730b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1562 additions and 688 deletions

View File

@ -1,44 +1,44 @@
{
"pre-push": {
"enabled": true,
"actions": [
{
"action": "composer analyse"
}
]
},
"pre-commit": {
"enabled": true,
"actions": [
{
"action": "composer cs-fix -- --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}",
"conditions": [
{
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
"args": ["php"]
}
]
}
]
},
"post-change": {
"enabled": true,
"actions": [
{
"action": "composer install",
"options": [],
"conditions": [
{
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any",
"args": [
[
"composer.json",
"composer.lock"
]
]
}
]
}
]
}
}
{
"pre-push": {
"enabled": true,
"actions": [
{
"action": "php vendor/bin/phpstan analyse --memory-limit 300M"
}
]
},
"pre-commit": {
"enabled": true,
"actions": [
{
"action": "php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential",
"conditions": [
{
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
"args": ["php"]
}
]
}
]
},
"post-change": {
"enabled": true,
"actions": [
{
"action": "composer install",
"options": [],
"conditions": [
{
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any",
"args": [
[
"composer.json",
"composer.lock"
]
]
}
]
}
]
}
}

View File

@ -80,7 +80,11 @@
},
"strawberry-perl": {
"binary": {
"windows-x86_64": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip"
"windows-x86_64": {
"type": "url",
"url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip",
"extract": "{pkg_root_path}/strawberry-perl"
}
}
},
"upx": {
@ -395,7 +399,7 @@
"binary": "hosted",
"source": {
"type": "git",
"rev": "v1.75.x",
"regex": "v(?<version>1.\\d+).x",
"url": "https://github.com/grpc/grpc.git"
}
},

View File

@ -1,7 +1,10 @@
{
"vswhere": {
"type": "target",
"artifact": "vswhere"
"artifact": "vswhere",
"static-bins@windows": [
"vswhere.exe"
]
},
"pkg-config": {
"type": "target",

View File

@ -14,7 +14,7 @@ use StaticPHP\Util\FileSystem;
#[Library('imap')]
class imap
{
#[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')]
#[AfterStage('php', [php::class, 'patchUnixEmbedScripts'], 'imap')]
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
public function afterPatchScripts(): void
{

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\WindowsCMakeExecutor;
use StaticPHP\Util\FileSystem;
#[Library('onig')]
class onig
{
#[BuildFor('Windows')]
public function buildWin(LibraryPackage $package): void
{
WindowsCMakeExecutor::create($package)
->addConfigureArgs('-DMSVC_STATIC_RUNTIME=ON')
->build();
FileSystem::copy("{$package->getLibDir()}\\onig.lib", "{$package->getLibDir()}\\onig_a.lib");
}
}

View File

@ -4,18 +4,16 @@ declare(strict_types=1);
namespace Package\Target;
use Package\Target\php\unix;
use Package\Target\php\windows;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Info;
use StaticPHP\Attribute\Package\InitPackage;
use StaticPHP\Attribute\Package\ResolveBuild;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Attribute\Package\Validate;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageBuilder;
@ -27,16 +25,11 @@ use StaticPHP\Registry\PackageLoader;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\SourcePatcher;
use StaticPHP\Util\SPCConfigUtil;
use StaticPHP\Util\System\UnixUtil;
use StaticPHP\Util\V2CompatLayer;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use ZM\Logger\ConsoleColor;
#[Target('php')]
#[Target('php-cli')]
@ -47,6 +40,9 @@ use ZM\Logger\ConsoleColor;
#[Target('frankenphp')]
class php extends TargetPackage
{
use unix;
use windows;
public static function getPHPVersionID(): int
{
$artifact = ArtifactLoader::getArtifactInstance('php-src');
@ -241,337 +237,6 @@ class php extends TargetPackage
FileSystem::removeDir(BUILD_MODULES_PATH);
}
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
#[PatchDescription('Patch configure.ac for musl and musl-toolchain')]
#[PatchDescription('Let php m4 tools use static pkg-config')]
public function patchBeforeBuildconf(TargetPackage $package): void
{
// patch configure.ac for musl and musl-toolchain
$musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl';
FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac');
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/configure.ac',
'if command -v ldd >/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1',
'if ' . ($musl ? 'true' : 'false')
);
// let php m4 tools use static pkg-config
FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC(');
}
#[Stage]
public function buildconfForUnix(TargetPackage $package): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf'));
V2CompatLayer::emitPatchPoint('before-php-buildconf');
shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF'));
}
#[Stage]
public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure'));
V2CompatLayer::emitPatchPoint('before-php-configure');
$cmd = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE');
$args = [];
$version_id = self::getPHPVersionID();
// PHP JSON extension is built-in since PHP 8.0
if ($version_id < 80000) {
$args[] = '--enable-json';
}
// zts
if ($package->getBuildOption('enable-zts', false)) {
$args[] = '--enable-zts --disable-zend-signals';
if ($version_id >= 80100 && SystemTarget::getTargetOS() === 'Linux') {
$args[] = '--enable-zend-max-execution-timers';
}
}
// config-file-path and config-file-scan-dir
if ($option = $package->getBuildOption('with-config-file-path', false)) {
$args[] = "--with-config-file-path={$option}";
}
if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) {
$args[] = "--with-config-file-scan-dir={$option}";
}
// perform enable cli options
$args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli';
$args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm';
$args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) {
'Linux' => '--enable-micro=all-static',
default => '--enable-micro',
} : null;
$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';
$args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null;
$args = implode(' ', array_filter($args));
$static_extension_str = $this->makeStaticExtensionString($installer);
// run ./configure with args
$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()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir());
}
#[Stage]
public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void
{
V2CompatLayer::emitPatchPoint('before-php-make');
logger()->info('cleaning up php-src build files');
shell()->cd($package->getSourceDir())->exec('make clean');
if ($installer->isPackageResolved('php-cli')) {
$package->runStage([self::class, 'makeCliForUnix']);
}
if ($installer->isPackageResolved('php-cgi')) {
$package->runStage([self::class, 'makeCgiForUnix']);
}
if ($installer->isPackageResolved('php-fpm')) {
$package->runStage([self::class, 'makeFpmForUnix']);
}
if ($installer->isPackageResolved('php-micro')) {
$package->runStage([self::class, 'makeMicroForUnix']);
}
if ($installer->isPackageResolved('php-embed')) {
$package->runStage([self::class, 'makeEmbedForUnix']);
}
}
#[Stage]
public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cli");
$builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php');
$package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php');
}
#[Stage]
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cgi");
$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');
}
#[Stage]
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} fpm");
$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');
}
#[Stage]
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$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' : '';
// build
shell()->cd($package->getSourceDir())
->setEnv($vars)
->exec("make -j{$builder->concurrency} micro");
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx');
$package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx');
} finally {
if ($phar_patched) {
SourcePatcher::unpatchMicroPhar();
}
}
}
#[Stage]
public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed'));
$shared_exts = array_filter(
$installer->getResolvedPackages(),
static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp()
);
$install_modules = $shared_exts ? 'install-modules' : '';
// detect changes in module path
$diff = new DirDiff(BUILD_MODULES_PATH, true);
$root = BUILD_ROOT_PATH;
$sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i';
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile')
->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 -------------
// process libphp.so for shared embed
$suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so';
$libphp_so = "{$package->getLibDir()}/libphp.{$suffix}";
if (file_exists($libphp_so)) {
// 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);
}
// process shared extensions that built-with-php
$increment_files = $diff->getChangedFiles();
$files = [];
foreach ($increment_files as $increment_file) {
$builder->deployBinary($increment_file, $increment_file, false);
$files[] = basename($increment_file);
}
if (!empty($files)) {
$package->setOutput('Built shared extensions', implode(', ', $files));
}
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
// process libphp.a for static embed
if (!file_exists("{$package->getLibDir()}/libphp.a")) {
return;
}
$ar = getenv('AR') ?: 'ar';
$libphp_a = "{$package->getLibDir()}/libphp.a";
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, 'patchEmbedScripts']);
}
#[Stage]
public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
// collect shared extensions
/** @var PhpExtensionPackage[] $shared_extensions */
$shared_extensions = array_filter(
$installer->getResolvedPackages(PhpExtensionPackage::class),
fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp()
);
if (!empty($shared_extensions)) {
if ($toolchain->isStatic()) {
throw new WrongUsageException(
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
);
}
FileSystem::createDir(BUILD_MODULES_PATH);
// backup
FileSystem::backupFile(BUILD_BIN_PATH . '/php-config');
FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"');
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes');
}
try {
logger()->debug('Building shared extensions...');
foreach ($shared_extensions as $extension) {
InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName()));
$extension->buildShared();
}
} finally {
// restore php-config
if (!empty($shared_extensions)) {
FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config');
FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
}
}
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(TargetPackage $package): void
{
// virtual target, do nothing
if ($package->getName() !== 'php') {
return;
}
$package->runStage([$this, 'buildconfForUnix']);
$package->runStage([$this, 'configureForUnix']);
$package->runStage([$this, 'makeForUnix']);
$package->runStage([$this, 'unixBuildSharedExt']);
}
/**
* Patch phpize and php-config if needed
*/
#[Stage]
public function patchEmbedScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
$this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
}
}
/**
* Seek php-src/config.log when building PHP, add it to exception.
*/
protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void
{
try {
$callback();
} catch (SPCException $e) {
if (file_exists("{$source_dir}/config.log")) {
$e->addExtraLogFile('php-src config.log', 'php-src.config.log');
copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log');
}
throw $e;
}
}
private function makeStaticExtensionString(PackageInstaller $installer): string
{
$arg = [];
@ -592,93 +257,4 @@ class php extends TargetPackage
logger()->debug("Static extension configure args: {$str}");
return $str;
}
/**
* 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(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages()));
$static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : '';
$pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : '';
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}",
'EXTRA_LDFLAGS' => $config['ldflags'],
'EXTRA_LIBS' => $config['libs'],
]);
}
/**
* 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)
));
}
}
}
}
}

View File

@ -0,0 +1,450 @@
<?php
declare(strict_types=1);
namespace Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\SourcePatcher;
use StaticPHP\Util\SPCConfigUtil;
use StaticPHP\Util\System\UnixUtil;
use StaticPHP\Util\V2CompatLayer;
use ZM\Logger\ConsoleColor;
trait unix
{
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
#[PatchDescription('Patch configure.ac for musl and musl-toolchain')]
#[PatchDescription('Let php m4 tools use static pkg-config')]
public function patchBeforeBuildconf(TargetPackage $package): void
{
// patch configure.ac for musl and musl-toolchain
$musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl';
FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac');
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/configure.ac',
'if command -v ldd >/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1',
'if ' . ($musl ? 'true' : 'false')
);
// let php m4 tools use static pkg-config
FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC(');
}
#[Stage]
public function buildconfForUnix(TargetPackage $package): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf'));
V2CompatLayer::emitPatchPoint('before-php-buildconf');
shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF'));
}
#[Stage]
public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure'));
V2CompatLayer::emitPatchPoint('before-php-configure');
$cmd = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE');
$args = [];
$version_id = self::getPHPVersionID();
// PHP JSON extension is built-in since PHP 8.0
if ($version_id < 80000) {
$args[] = '--enable-json';
}
// zts
if ($package->getBuildOption('enable-zts', false)) {
$args[] = '--enable-zts --disable-zend-signals';
if ($version_id >= 80100 && SystemTarget::getTargetOS() === 'Linux') {
$args[] = '--enable-zend-max-execution-timers';
}
}
// config-file-path and config-file-scan-dir
if ($option = $package->getBuildOption('with-config-file-path', false)) {
$args[] = "--with-config-file-path={$option}";
}
if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) {
$args[] = "--with-config-file-scan-dir={$option}";
}
// perform enable cli options
$args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli';
$args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm';
$args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) {
'Linux' => '--enable-micro=all-static',
default => '--enable-micro',
} : null;
$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';
$args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null;
$args = implode(' ', array_filter($args));
$static_extension_str = $this->makeStaticExtensionString($installer);
// run ./configure with args
$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()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir());
}
#[Stage]
public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void
{
V2CompatLayer::emitPatchPoint('before-php-make');
logger()->info('cleaning up php-src build files');
shell()->cd($package->getSourceDir())->exec('make clean');
if ($installer->isPackageResolved('php-cli')) {
$package->runStage([self::class, 'makeCliForUnix']);
}
if ($installer->isPackageResolved('php-cgi')) {
$package->runStage([self::class, 'makeCgiForUnix']);
}
if ($installer->isPackageResolved('php-fpm')) {
$package->runStage([self::class, 'makeFpmForUnix']);
}
if ($installer->isPackageResolved('php-micro')) {
$package->runStage([self::class, 'makeMicroForUnix']);
}
if ($installer->isPackageResolved('php-embed')) {
$package->runStage([self::class, 'makeEmbedForUnix']);
}
}
#[Stage]
public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cli");
$builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php');
$package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php');
}
#[Stage]
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cgi");
$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');
}
#[Stage]
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} fpm");
$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');
}
#[Stage]
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$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' : '';
// build
shell()->cd($package->getSourceDir())
->setEnv($vars)
->exec("make -j{$builder->concurrency} micro");
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx');
$package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx');
} finally {
if ($phar_patched) {
SourcePatcher::unpatchMicroPhar();
}
}
}
#[Stage]
public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed'));
$shared_exts = array_filter(
$installer->getResolvedPackages(),
static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp()
);
$install_modules = $shared_exts ? 'install-modules' : '';
// detect changes in module path
$diff = new DirDiff(BUILD_MODULES_PATH, true);
$root = BUILD_ROOT_PATH;
$sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i';
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile')
->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 -------------
// process libphp.so for shared embed
$suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so';
$libphp_so = "{$package->getLibDir()}/libphp.{$suffix}";
if (file_exists($libphp_so)) {
// 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);
}
// process shared extensions that built-with-php
$increment_files = $diff->getChangedFiles();
$files = [];
foreach ($increment_files as $increment_file) {
$builder->deployBinary($increment_file, $increment_file, false);
$files[] = basename($increment_file);
}
if (!empty($files)) {
$package->setOutput('Built shared extensions', implode(', ', $files));
}
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
// process libphp.a for static embed
if (!file_exists("{$package->getLibDir()}/libphp.a")) {
return;
}
$ar = getenv('AR') ?: 'ar';
$libphp_a = "{$package->getLibDir()}/libphp.a";
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, 'patchEmbedScripts']);
}
#[Stage]
public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
// collect shared extensions
/** @var PhpExtensionPackage[] $shared_extensions */
$shared_extensions = array_filter(
$installer->getResolvedPackages(PhpExtensionPackage::class),
fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp()
);
if (!empty($shared_extensions)) {
if ($toolchain->isStatic()) {
throw new WrongUsageException(
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
);
}
FileSystem::createDir(BUILD_MODULES_PATH);
// backup
FileSystem::backupFile(BUILD_BIN_PATH . '/php-config');
FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"');
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes');
}
try {
logger()->debug('Building shared extensions...');
foreach ($shared_extensions as $extension) {
InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName()));
$extension->buildShared();
}
} finally {
// restore php-config
if (!empty($shared_extensions)) {
FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config');
FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
}
}
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(TargetPackage $package): void
{
// virtual target, do nothing
if ($package->getName() !== 'php') {
return;
}
$package->runStage([$this, 'buildconfForUnix']);
$package->runStage([$this, 'configureForUnix']);
$package->runStage([$this, 'makeForUnix']);
$package->runStage([$this, 'unixBuildSharedExt']);
}
/**
* Patch phpize and php-config if needed
*/
#[Stage]
public function patchUnixEmbedScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
$this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
}
}
/**
* Seek php-src/config.log when building PHP, add it to exception.
*/
protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void
{
try {
$callback();
} catch (SPCException $e) {
if (file_exists("{$source_dir}/config.log")) {
$e->addExtraLogFile('php-src config.log', 'php-src.config.log');
copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log');
}
throw $e;
}
}
/**
* 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(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages()));
$static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : '';
$pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : '';
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}",
'EXTRA_LDFLAGS' => $config['ldflags'],
'EXTRA_LIBS' => $config['libs'],
]);
}
}

View File

@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
namespace Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Exception\PatchException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\SourcePatcher;
use StaticPHP\Util\System\WindowsUtil;
use StaticPHP\Util\V2CompatLayer;
use ZM\Logger\ConsoleColor;
trait windows
{
#[Stage]
public function buildconfForWindows(TargetPackage $package): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat'));
V2CompatLayer::emitPatchPoint('before-php-buildconf');
cmd()->cd($package->getSourceDir())->exec('.\buildconf.bat');
}
#[Stage]
public function configureForWindows(TargetPackage $package, PackageInstaller $installer): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure.bat'));
V2CompatLayer::emitPatchPoint('before-php-configure');
$args = [
'--disable-all',
"--with-php-build={$package->getBuildRootPath()}",
"--with-extra-includes={$package->getIncludeDir()}",
"--with-extra-libs={$package->getLibDir()}",
];
// sapis
$cli = $installer->isPackageResolved('php-cli');
$cgi = $installer->isPackageResolved('php-cgi');
$micro = $installer->isPackageResolved('php-micro');
$args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no';
$args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no';
$args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no';
// zts
$args[] = $package->getBuildOption('enable-zts', false) ? '--enable-zts=yes' : '--enable-zts=no';
// opcache-jit
$args[] = !$package->getBuildOption('disable-opcache-jit', false) ? '--enable-opcache-jit=yes' : '--enable-opcache-jit=no';
// micro win32
if ($micro && $package->getBuildOption('enable-micro-win32', false)) {
$args[] = '--enable-micro-win32=yes';
}
// config-file-scan-dir
if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) {
$args[] = "--with-config-file-scan-dir={$option}";
}
// micro logo
if ($micro && ($logo = $this->getBuildOption('with-micro-logo')) !== null) {
$args[] = "--enable-micro-logo={$logo}";
copy($logo, SOURCE_PATH . '\php-src\\' . $logo);
}
$args = implode(' ', $args);
$static_extension_str = $this->makeStaticExtensionString($installer);
cmd()->cd($package->getSourceDir())->exec(".\\configure.bat {$args} {$static_extension_str}");
}
#[BeforeStage('php', [self::class, 'makeCliForWindows'])]
#[PatchDescription('Patch Windows Makefile for CLI target')]
public function patchCLITarget(TargetPackage $package): void
{
// search Makefile code line contains "$(BUILD_DIR)\php.exe:"
$content = FileSystem::readFile("{$package->getSourceDir()}\\Makefile");
$lines = explode("\r\n", $content);
$line_num = 0;
$found = false;
foreach ($lines as $v) {
if (str_contains($v, '$(BUILD_DIR)\php.exe:')) {
$found = $line_num;
break;
}
++$line_num;
}
if ($found === false) {
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';
$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));
}
#[Stage]
public function makeCliForWindows(TargetPackage $package, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe'));
// extra lib
$extra_libs = getenv('SPC_EXTRA_LIBS') ?: '';
// Add debug symbols for release build if --no-strip is specified
// We need to modify CFLAGS to replace /Ox with /Zi and add /DEBUG to LDFLAGS
$debug_overrides = '';
if ($package->getBuildOption('no-strip', false)) {
// Read current CFLAGS from Makefile and replace optimization flags
$makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile");
if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) {
$cflags = $matches[1];
// Replace /Ox (full optimization) with /Zi (debug info) and /Od (disable optimization)
// Keep optimization for speed: /O2 /Zi instead of /Od /Zi
$cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags);
$debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CLI=/DEBUG" ';
}
}
cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php.exe");
$this->deployWindowsBinary($builder, $package, 'php-cli');
}
#[Stage]
public function makeForWindows(TargetPackage $package, PackageInstaller $installer): void
{
V2CompatLayer::emitPatchPoint('before-php-make');
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('nmake clean'));
cmd()->cd($package->getSourceDir())->exec('nmake clean');
if ($installer->isPackageResolved('php-cli')) {
$package->runStage([$this, 'makeCliForWindows']);
}
if ($installer->isPackageResolved('php-cgi')) {
$package->runStage([$this, 'makeCgiForWindows']);
}
if ($installer->isPackageResolved('php-micro')) {
$package->runStage([$this, 'makeMicroForWindows']);
}
}
#[BuildFor('Windows')]
public function buildWin(TargetPackage $package): void
{
if ($package->getName() !== 'php') {
return;
}
$package->runStage([$this, 'buildconfForWindows']);
$package->runStage([$this, 'configureForWindows']);
$package->runStage([$this, 'makeForWindows']);
}
#[BeforeStage('php', [self::class, 'buildconfForWindows'])]
#[PatchDescription('Patch SPC_MICRO_PATCHES defined patches')]
#[PatchDescription('Fix PHP 8.1 static build bug on Windows')]
#[PatchDescription('Fix PHP Visual Studio version detection')]
public function patchBeforeBuildconfForWindows(TargetPackage $package): void
{
// php-src patches from micro
SourcePatcher::patchPhpSrc();
// php 8.1 bug
if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) {
logger()->info('Patching PHP 8.1 windows Fiber bug');
FileSystem::replaceFileStr(
"{$package->getSourceDir()}\\win32\\build\\config.w32",
"ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');",
"ADD_FLAG('ASM_OBJS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj $(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');"
);
FileSystem::replaceFileStr(
"{$package->getSourceDir()}\\win32\\build\\config.w32",
"ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');",
''
);
}
// Fix PHP VS version
// get vs version
$vc = WindowsUtil::findVisualStudio();
if ($vc === false) {
$vc_matches = ['unknown', 'unknown'];
} else {
$vc_matches = match ($vc['major_version']) {
'17' => ['VS17', 'Visual C++ 2022'],
'16' => ['VS16', 'Visual C++ 2019'],
default => ['unknown', 'unknown'],
};
}
// patch php-src/win32/build/confutils.js
FileSystem::replaceFileStr(
"{$package->getSourceDir()}\\win32\\build\\confutils.js",
'var name = "unknown";',
"var name = short ? \"{$vc_matches[0]}\" : \"{$vc_matches[1]}\";return name;"
);
// patch micro win32
if ($package->getBuildOption('enable-micro-win32') && !file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) {
copy("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak");
FileSystem::replaceFileStr("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1");
} else {
if (file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) {
rename("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c");
}
}
}
protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $package, string $sapi): void
{
$rel_type = 'Release'; // TODO: Debug build support
$ts = $builder->getOption('enable-zts') ? '_TS' : '';
$debug_dir = BUILD_ROOT_PATH . '\debug';
$src = match ($sapi) {
'php-cli' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'],
'php-micro' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'],
'php-cgi' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'],
default => throw new SPCInternalException("Deployment does not accept type {$sapi}"),
};
$src_file = "{$src[0]}\\{$src[1]}";
$dst_file = BUILD_BIN_PATH . '\\' . basename($src_file);
$builder->deployBinary($src_file, $dst_file);
// make debug info file path
if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) {
FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}");
}
}
}

View File

@ -167,11 +167,22 @@ class Artifact
return false;
}
// For standalone mode, check directory and hash
// For standalone mode, check directory or file and hash
$target_path = $extract_config['path'];
if (!is_dir($target_path)) {
return false;
// Check if target is a file or directory
$is_file_target = !is_dir($target_path) && (pathinfo($target_path, PATHINFO_EXTENSION) !== '');
if ($is_file_target) {
// For single file extraction (e.g., vswhere.exe)
if (!file_exists($target_path)) {
return false;
}
} else {
// For directory extraction
if (!is_dir($target_path)) {
return false;
}
}
if (!$compare_hash) {
@ -320,7 +331,7 @@ class Artifact
* For merge mode, returns the base path.
* For standalone mode, returns the specific directory.
*/
public function getBinaryDir(): string
public function getBinaryDir(): ?string
{
$config = $this->getBinaryExtractConfig();
return $config['path'];

View File

@ -293,7 +293,7 @@ class ArtifactExtractor
// Process file mappings
foreach ($file_map as $src_pattern => $dst_path) {
$dst_path = $this->replacePathVariables($dst_path);
$src_full = "{$temp_path}/{$src_pattern}";
$src_full = FileSystem::convertPath("{$temp_path}/{$src_pattern}");
// Handle glob patterns
if (str_contains($src_pattern, '*')) {
@ -460,40 +460,36 @@ class ArtifactExtractor
$target = FileSystem::convertPath($target);
$filename = FileSystem::convertPath($filename);
FileSystem::createDir($target);
$extname = FileSystem::extname($filename);
if (PHP_OS_FAMILY === 'Windows') {
// Use 7za.exe for Windows
$is_txz = str_ends_with($filename, '.txz') || str_ends_with($filename, '.tar.xz');
default_shell()->execute7zExtract($filename, $target, $is_txz);
return;
}
// Unix-like systems: determine compression type
if (str_ends_with($filename, '.tar.gz') || str_ends_with($filename, '.tgz')) {
default_shell()->executeTarExtract($filename, $target, 'gz');
} elseif (str_ends_with($filename, '.tar.bz2')) {
default_shell()->executeTarExtract($filename, $target, 'bz2');
} elseif (str_ends_with($filename, '.tar.xz') || str_ends_with($filename, '.txz')) {
default_shell()->executeTarExtract($filename, $target, 'xz');
} elseif (str_ends_with($filename, '.tar')) {
default_shell()->executeTarExtract($filename, $target, 'none');
} elseif (str_ends_with($filename, '.zip')) {
// Zip requires special handling for strip-components
$this->unzipWithStrip($filename, $target);
} elseif (str_ends_with($filename, '.exe')) {
// exe just copy to target
$dest_file = FileSystem::convertPath("{$target}/" . basename($filename));
FileSystem::copy($filename, $dest_file);
} else {
throw new FileSystemException("Unknown archive format: {$filename}");
if ($extname !== 'exe' && !is_dir($target)) {
FileSystem::createDir($target);
}
match (SystemTarget::getTargetOS()) {
'Windows' => match ($extname) {
'tar' => default_shell()->executeTarExtract($filename, $target, 'none'),
'xz', 'txz', 'gz', 'tgz', 'bz2' => default_shell()->execute7zExtract($filename, $target),
'zip' => $this->unzipWithStrip($filename, $target),
'exe' => $this->copyFile($filename, $target),
default => throw new FileSystemException("Unknown archive format: {$filename}"),
},
'Linux', 'Darwin' => match ($extname) {
'tar' => default_shell()->executeTarExtract($filename, $target, 'none'),
'gz', 'tgz' => default_shell()->executeTarExtract($filename, $target, 'gz'),
'bz2' => default_shell()->executeTarExtract($filename, $target, 'bz2'),
'xz', 'txz' => default_shell()->executeTarExtract($filename, $target, 'xz'),
'zip' => $this->unzipWithStrip($filename, $target),
'exe' => $this->copyFile($filename, $target),
default => throw new FileSystemException("Unknown archive format: {$filename}"),
},
default => throw new SPCInternalException('Unsupported OS for archive extraction')
};
}
/**
* Unzip file with stripping top-level directory.
*/
protected function unzipWithStrip(string $zip_file, string $extract_path): void
protected function unzipWithStrip(string $zip_file, string $extract_path): bool
{
$temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16)));
$zip_file = FileSystem::convertPath($zip_file);
@ -572,6 +568,8 @@ class ArtifactExtractor
// Clean up temp directory
FileSystem::removeDir($temp_dir);
return true;
}
/**
@ -585,6 +583,7 @@ class ArtifactExtractor
'{source_path}' => SOURCE_PATH,
'{download_path}' => DOWNLOAD_PATH,
'{working_dir}' => WORKING_DIR,
'{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: '',
];
return str_replace(array_keys($replacement), array_values($replacement), $path);
}
@ -627,9 +626,9 @@ class ArtifactExtractor
}
}
private function copyFile(string $source_file, string $target_path): void
private function copyFile(string $source_file, string $target_path): bool
{
FileSystem::createDir(dirname($target_path));
FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
return FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
}
}

View File

@ -30,7 +30,8 @@ class DownloadResult
) {
switch ($this->cache_type) {
case 'archive':
$this->filename !== null ?: throw new DownloaderException('Archive download result must have a filename.');
case 'file':
$this->filename !== null ?: throw new DownloaderException('Archive/file download result must have a filename.');
$fn = FileSystem::isRelativePath($this->filename) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->filename) : $this->filename;
file_exists($fn) ?: throw new DownloaderException("Downloaded archive file does not exist: {$fn}");
break;
@ -60,7 +61,20 @@ class DownloadResult
?string $version = null,
array $metadata = []
): DownloadResult {
return new self('archive', config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
// judge if it is archive or just a pure file
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
}
public static function file(
string $filename,
array $config,
bool $verified = false,
?string $version = null,
array $metadata = []
): DownloadResult {
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata);
}
/**
@ -143,4 +157,16 @@ class DownloadResult
array_merge($this->metadata, [$key => $value])
);
}
/**
* Check
*/
private static function isArchiveFile(string $filename): bool
{
$archive_extensions = [
'zip', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz', 'txz', 'rar', '7z',
];
$lower_filename = strtolower($filename);
return array_any($archive_extensions, fn ($ext) => str_ends_with($lower_filename, '.' . $ext));
}
}

View File

@ -6,6 +6,8 @@ namespace StaticPHP\Artifact\Downloader\Type;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Util\FileSystem;
/** git */
class Git implements DownloadTypeInterface
@ -15,8 +17,55 @@ class Git implements DownloadTypeInterface
$path = DOWNLOAD_PATH . "/{$name}";
logger()->debug("Cloning git repository for {$name} from {$config['url']}");
$shallow = !$downloader->getOption('no-shallow-clone', false);
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null);
$version = "dev-{$config['rev']}";
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version);
// direct branch clone
if (isset($config['rev'])) {
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null);
$version = "dev-{$config['rev']}";
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version);
}
if (!isset($config['regex'])) {
throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.');
}
// regex matches branch first, we need to fetch all refs in emptyfirst
$gitdir = sys_get_temp_dir() . '/' . $name;
FileSystem::resetDir($gitdir);
$shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false);
$result = $shell->cd($gitdir)
->exec(SPC_GIT_EXEC . ' init')
->exec(SPC_GIT_EXEC . ' remote add origin ' . escapeshellarg($config['url']))
->execWithResult(SPC_GIT_EXEC . ' ls-remote origin');
if ($result[0] !== 0) {
throw new DownloaderException("Failed to ls-remote from {$config['url']}");
}
$refs = $result[1];
$matched_version_branch = [];
$matched_count = 0;
$regex = '/^' . $config['regex'] . '$/';
foreach ($refs as $ref) {
$matches = null;
if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) {
++$matched_count;
$branch = $matches[1];
if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) {
$matched_version_branch[$vermatch['version']] = $vermatch[0];
}
}
}
// sort versions
uksort($matched_version_branch, function ($a, $b) {
return version_compare($b, $a);
});
if (!empty($matched_version_branch)) {
// use the highest version
$version = array_key_first($matched_version_branch);
$branch = $matched_version_branch[$version];
logger()->info("Matched version {$version} from branch {$branch} for {$name}");
default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null);
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version);
}
throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches).");
}
}

View File

@ -94,7 +94,7 @@ abstract class BaseCommand extends Command
}
// Set debug mode in ApplicationContext
$isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
$isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
ApplicationContext::setDebug($isDebug);
// show raw argv list for logger()->debug

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command\Dev;
use StaticPHP\Command\BaseCommand;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('dev:env', 'Returns the internally defined environment variables')]
class EnvCommand extends BaseCommand
{
public function configure(): void
{
$this->addArgument('env', InputArgument::OPTIONAL, 'The environment variable to show, if not set, all will be shown');
}
public function initialize(InputInterface $input, OutputInterface $output): void
{
$this->no_motd = true;
parent::initialize($input, $output);
}
public function handle(): int
{
$env = $this->getArgument('env');
if (($val = getenv($env)) === false) {
$this->output->writeln("<error>Environment variable '{$env}' is not set.</error>");
return static::FAILURE;
}
if (is_array($val)) {
foreach ($val as $k => $v) {
$this->output->writeln("<info>{$k}={$v}</info>");
}
return static::SUCCESS;
}
$this->output->writeln("<info>{$val}</info>");
return static::SUCCESS;
}
}

View File

@ -18,6 +18,7 @@ class DoctorCommand extends BaseCommand
public function handle(): int
{
f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes');
$fix_policy = match ($this->input->getOption('auto-fix')) {
'never' => FIX_POLICY_DIE,
true, null => FIX_POLICY_AUTOFIX,

View File

@ -79,7 +79,7 @@ class ConfigValidator
public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields]
'filelist' => [['url', 'regex'], ['extract']],
'git' => [['url', 'rev'], ['extract', 'submodules']],
'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']],
'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']],

View File

@ -6,6 +6,7 @@ namespace StaticPHP;
use StaticPHP\Command\BuildLibsCommand;
use StaticPHP\Command\BuildTargetCommand;
use StaticPHP\Command\Dev\EnvCommand;
use StaticPHP\Command\Dev\IsInstalledCommand;
use StaticPHP\Command\Dev\ShellCommand;
use StaticPHP\Command\DoctorCommand;
@ -57,6 +58,7 @@ class ConsoleApplication extends Application
// dev commands
new ShellCommand(),
new IsInstalledCommand(),
new EnvCommand(),
]);
// add additional commands from registries

View File

@ -130,6 +130,7 @@ readonly class Doctor
$this->output?->writeln('<error>Fix failed: ' . $e->getMessage() . '</error>');
return false;
} catch (\Throwable $e) {
logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString());
$this->output?->writeln('<error>Fix failed with an unexpected error: ' . $e->getMessage() . '</error>');
return false;
} finally {

View File

@ -10,7 +10,7 @@ use StaticPHP\Util\System\LinuxUtil;
class OSCheck
{
#[CheckItem('if current OS are supported', level: 1000)]
#[CheckItem('if current OS is supported', level: 1000)]
public function checkOS(): ?CheckResult
{
if (!in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'Windows'])) {

View File

@ -0,0 +1,142 @@
<?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\Doctor\CheckResult;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\System\WindowsUtil;
#[OptionalCheck([self::class, 'optional'])]
class WindowsToolCheck
{
public static function optional(): bool
{
return SystemTarget::getTargetOS() === 'Windows';
}
#[CheckItem('if vswhere is installed', level: 999)]
public function findVSWhere(): ?CheckResult
{
$installer = new PackageInstaller();
$installer->addInstallPackage('vswhere');
$is_installed = $installer->isPackageInstalled('vswhere');
if ($is_installed) {
return CheckResult::ok();
}
return CheckResult::fail('vswhere is not installed', 'install-vswhere');
}
#[CheckItem('if Visual Studio is installed', level: 998)]
public function findVS(): ?CheckResult
{
$a = WindowsUtil::findVisualStudio();
if ($a !== false) {
return CheckResult::ok("{$a['version']} at {$a['dir']}");
}
return CheckResult::fail('Visual Studio with C++ tools is not installed. Please install Visual Studio with C++ tools.');
}
#[CheckItem('if git associated command exists', level: 997)]
public function checkGitPatch(): ?CheckResult
{
if (WindowsUtil::findCommand('patch.exe') === null) {
return CheckResult::fail('Git patch (minGW command) not found in path. You need to add "C:\Program Files\Git\usr\bin" in Path.');
}
return CheckResult::ok();
}
#[CheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 996)]
public function checkSDK(): ?CheckResult
{
if (!file_exists(getenv('PHP_SDK_PATH') . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) {
return CheckResult::fail('php-sdk-binary-tools not downloaded', 'install-php-sdk');
}
return CheckResult::ok(getenv('PHP_SDK_PATH'));
}
#[CheckItem('if nasm installed', level: 995)]
public function checkNasm(): ?CheckResult
{
if (($a = WindowsUtil::findCommand('nasm.exe')) === null) {
return CheckResult::fail('nasm.exe not found in path.', 'install-nasm');
}
return CheckResult::ok($a);
}
#[CheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)]
public function checkPerl(): ?CheckResult
{
if (($path = WindowsUtil::findCommand('perl.exe')) === null) {
return CheckResult::fail('perl not found in path.', 'install-perl');
}
if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) {
return CheckResult::fail($path . ' is not built for msvc.', 'install-perl');
}
return CheckResult::ok($path);
}
#[CheckItem('if environment is properly set up', level: 1)]
public function checkenv(): ?CheckResult
{
// manually trigger after init
try {
ToolchainManager::afterInitToolchain();
} catch (\Exception $e) {
return CheckResult::fail('Environment setup failed: ' . $e->getMessage());
}
$required_cmd = ['cl.exe', 'link.exe', 'lib.exe', 'dumpbin.exe', 'msbuild.exe', 'nmake.exe'];
foreach ($required_cmd as $cmd) {
if (WindowsUtil::findCommand($cmd) === null) {
return CheckResult::fail("{$cmd} not found in path. Please make sure Visual Studio with C++ tools is properly installed.");
}
}
return CheckResult::ok();
}
#[FixItem('install-perl')]
public function installPerl(): bool
{
$installer = new PackageInstaller();
$installer->addInstallPackage('strawberry-perl');
$installer->run(false);
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl');
return true;
}
#[FixItem('install-php-sdk')]
public function installSDK(): bool
{
FileSystem::removeDir(getenv('PHP_SDK_PATH'));
$installer = new PackageInstaller();
$installer->addInstallPackage('php-sdk-binary-tools');
$installer->run(false);
return true;
}
#[FixItem('install-nasm')]
public function installNasm(): bool
{
$installer = new PackageInstaller();
$installer->addInstallPackage('nasm');
$installer->run(false);
return true;
}
#[FixItem('install-vswhere')]
public function installVSWhere(): bool
{
$installer = new PackageInstaller();
$installer->addInstallPackage('vswhere');
$installer->run(false);
return true;
}
}

View File

@ -190,7 +190,7 @@ class ExceptionHandler
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);
fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL);
if ($output_log) {
InteractiveTerm::plain(ConsoleColor::red($line) . '');
InteractiveTerm::plain(ConsoleColor::red($line) . '', 'error');
}
}
}

View File

@ -127,6 +127,14 @@ abstract class Package
return false;
}
/**
* Check if the package has a build function for the current OS.
*/
public function hasBuildFunctionForCurrentOS(): bool
{
return isset($this->build_functions[PHP_OS_FAMILY]);
}
/**
* Get the name of the package.
*/

View File

@ -11,7 +11,6 @@ use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\System\LinuxUtil;
@ -27,9 +26,6 @@ class PackageBuilder
{
ApplicationContext::set(PackageBuilder::class, $this);
// apply build toolchain envs
GlobalEnvManager::afterInit();
$this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1;
}
@ -103,7 +99,7 @@ class PackageBuilder
// ignore copy to self
if (realpath($src) !== realpath($dst)) {
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst));
FileSystem::copy($src, $dst);
}
// file exist
@ -115,7 +111,7 @@ class PackageBuilder
$this->extractDebugInfo($dst);
// strip
if (!$this->getOption('no-strip')) {
if (!$this->getOption('no-strip') && SystemTarget::isUnix()) {
$this->stripBinary($dst);
}
@ -127,6 +123,9 @@ class PackageBuilder
}
logger()->info("Compressing {$dst} with UPX");
shell()->exec(getenv('UPX_EXEC') . " --best {$dst}");
} elseif ($upx_option && SystemTarget::getTargetOS() === 'Windows' && $executable) {
logger()->info("Compressing {$dst} with UPX");
shell()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst));
}
return $dst;
@ -140,12 +139,13 @@ class PackageBuilder
public function extractDebugInfo(string $binary_path): string
{
$target_dir = BUILD_ROOT_PATH . '/debug';
FileSystem::createDir($target_dir);
$basename = basename($binary_path);
$debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug');
if (SystemTarget::getTargetOS() === 'Darwin') {
FileSystem::createDir($target_dir);
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
} elseif (SystemTarget::getTargetOS() === 'Linux') {
FileSystem::createDir($target_dir);
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
shell()
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
@ -156,7 +156,8 @@ class PackageBuilder
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
}
} else {
throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS');
logger()->debug('extractDebugInfo is only supported on Linux and macOS');
return '';
}
return $debug_file;
}

View File

@ -15,6 +15,7 @@ use StaticPHP\Registry\PackageLoader;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\V2CompatLayer;
use ZM\Logger\ConsoleColor;
@ -120,6 +121,9 @@ class PackageInstaller
*/
public function run(bool $interactive = true, bool $disable_delay_msg = false): void
{
// apply build toolchain envs
GlobalEnvManager::afterInit();
if (empty($this->packages)) {
// resolve input, make dependency graph
$this->resolvePackages();

View File

@ -306,7 +306,9 @@ class PackageLoader
}
}
// check stage exists
if (!$pkg->hasStage($stage_name)) {
// Skip validation if the package has no build function for current OS
// (e.g., libedit has BeforeStage for 'build' but only BuildFor('Darwin'/'Linux'))
if (!$pkg->hasStage($stage_name) && $pkg->hasBuildFunctionForCurrentOS()) {
throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}].");
}
}

View File

@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Runtime\Executor;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\Shell\WindowsCmd;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\System\WindowsUtil;
use ZM\Logger\ConsoleColor;
class WindowsCMakeExecutor extends Executor
{
protected WindowsCmd $cmd;
protected array $configure_args = [];
protected array $ignore_args = [];
protected ?string $build_dir = null;
protected ?array $custom_default_args = null;
protected int $steps = 2;
protected bool $reset = true;
protected PackageBuilder $builder;
protected PackageInstaller $installer;
public function __construct(protected LibraryPackage $package)
{
parent::__construct($this->package);
$this->builder = ApplicationContext::get(PackageBuilder::class);
$this->installer = ApplicationContext::get(PackageInstaller::class);
$this->initCmd();
// judge that this package has artifact.source and defined build stage
if (!$this->package->hasStage('build')) {
throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined.");
}
}
public function build(): static
{
$this->initBuildDir();
if ($this->reset) {
FileSystem::resetDir($this->build_dir);
}
// configure
if ($this->steps >= 1) {
$args = array_merge($this->configure_args, $this->getDefaultCMakeArgs());
$args = array_diff($args, $this->ignore_args);
$configure_args = implode(' ', $args);
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake configure)'));
$this->cmd->exec("cmake {$configure_args}");
}
// make
if ($this->steps >= 2) {
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake build)'));
$this->cmd->cd($this->build_dir)->exec("cmake --build {$this->build_dir} --config Release --target install -j{$this->builder->concurrency}");
}
return $this;
}
/**
* Add optional package configuration.
* This method checks if a package is available and adds the corresponding arguments to the CMake configuration.
*
* @param string $name package name to check
* @param \Closure|string $true_args arguments to use if the package is available (allow closure, returns string)
* @param string $false_args arguments to use if the package is not available
* @return $this
*/
public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static
{
if ($get = $this->installer->getResolvedPackages()[$name] ?? null) {
logger()->info("Building package [{$this->package->getName()}] with {$name} support");
$args = $true_args instanceof \Closure ? $true_args($get) : $true_args;
} else {
logger()->info("Building package [{$this->package->getName()}] without {$name} support");
$args = $false_args;
}
$this->addConfigureArgs($args);
return $this;
}
/**
* Add configure args.
*/
public function addConfigureArgs(...$args): static
{
$this->configure_args = [...$this->configure_args, ...$args];
return $this;
}
/**
* Remove some configure args, to bypass the configure option checking for some libs.
*/
public function removeConfigureArgs(...$args): static
{
$this->ignore_args = [...$this->ignore_args, ...$args];
return $this;
}
public function setEnv(array $env): static
{
$this->cmd->setEnv($env);
return $this;
}
public function appendEnv(array $env): static
{
$this->cmd->appendEnv($env);
return $this;
}
/**
* To build steps.
*
* @param int $step Step number, accept 1-3
* @return $this
*/
public function toStep(int $step): static
{
$this->steps = $step;
return $this;
}
/**
* Set custom CMake build directory.
*
* @param string $dir custom CMake build directory
*/
public function setBuildDir(string $dir): static
{
$this->build_dir = $dir;
return $this;
}
/**
* Set the custom default args.
*/
public function setCustomDefaultArgs(...$args): static
{
$this->custom_default_args = $args;
return $this;
}
/**
* Set the reset status.
* If we set it to false, it will not clean and create the specified cmake working directory.
*/
public function setReset(bool $reset): static
{
$this->reset = $reset;
return $this;
}
/**
* Get configure argument string.
*/
public function getConfigureArgsString(): string
{
return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs()));
}
/**
* Returns the default CMake args.
*/
private function getDefaultCMakeArgs(): array
{
return $this->custom_default_args ?? [
'-A x64',
'-DCMAKE_BUILD_TYPE=Release',
'-DBUILD_SHARED_LIBS=OFF',
'-DBUILD_STATIC_LIBS=ON',
"-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}",
'-DCMAKE_INSTALL_PREFIX=' . escapeshellarg($this->package->getBuildRootPath()),
'-B ' . escapeshellarg(FileSystem::convertPath($this->build_dir)),
];
}
private function makeCmakeToolchainFile(): string
{
if (file_exists(SOURCE_PATH . '\toolchain.cmake')) {
return SOURCE_PATH . '\toolchain.cmake';
}
return WindowsUtil::makeCmakeToolchainFile();
}
/**
* Initialize the CMake build directory.
* If the directory is not set, it defaults to the package's source directory with '/build' appended.
*/
private function initBuildDir(): void
{
if ($this->build_dir === null) {
$this->build_dir = "{$this->package->getSourceDir()}\\build";
}
}
private function initCmd(): void
{
$this->cmd = cmd()->cd($this->package->getSourceDir());
}
}

View File

@ -42,7 +42,7 @@ class DefaultShell extends Shell
$cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}";
$this->logCommandInfo($cmd);
$result = $this->passthru($cmd, console_output: false, capture_output: true, throw_on_error: false);
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
$ret = $result['code'];
$output = $result['output'];
if ($ret !== 0) {
@ -96,15 +96,15 @@ class DefaultShell extends Shell
$cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}");
$this->logCommandInfo($cmd);
logger()->debug("[GIT CLONE] {$cmd}");
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
$this->passthru($cmd, $this->console_putput);
if ($submodules !== null) {
$depth_flag = $shallow ? '--depth 1' : '';
foreach ($submodules as $submodule) {
$submodule = escapeshellarg($submodule);
$submodule_cmd = clean_spaces("cd {$path_arg} && {$git} submodule update --init {$depth_flag} {$submodule}");
$submodule_cmd = clean_spaces("{$git} submodule update --init {$depth_flag} {$submodule}");
$this->logCommandInfo($submodule_cmd);
logger()->debug("[GIT SUBMODULE] {$submodule_cmd}");
$this->passthru($submodule_cmd, $this->console_putput, capture_output: false, throw_on_error: true);
$this->passthru($submodule_cmd, $this->console_putput, cwd: $path_arg);
}
}
}
@ -117,7 +117,7 @@ class DefaultShell extends Shell
* @param string $compression Compression type: 'gz', 'bz2', 'xz', or 'none'
* @param int $strip Number of leading components to strip (default: 1)
*/
public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): void
public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): bool
{
$archive_arg = escapeshellarg(FileSystem::convertPath($archive_path));
$target_arg = escapeshellarg(FileSystem::convertPath($target_path));
@ -135,7 +135,8 @@ class DefaultShell extends Shell
$this->logCommandInfo($cmd);
logger()->debug("[TAR EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
$this->passthru($cmd, $this->console_putput);
return true;
}
/**
@ -154,7 +155,7 @@ class DefaultShell extends Shell
$this->logCommandInfo($cmd);
logger()->debug("[UNZIP] {$cmd}");
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
$this->passthru($cmd, $this->console_putput);
}
/**
@ -162,9 +163,8 @@ class DefaultShell extends Shell
*
* @param string $archive_path Path to the archive file
* @param string $target_path Path to extract to
* @param bool $is_txz Whether this is a .txz/.tar.xz file that needs double extraction
*/
public function execute7zExtract(string $archive_path, string $target_path, bool $is_txz = false): void
public function execute7zExtract(string $archive_path, string $target_path): bool
{
$sdk_path = getenv('PHP_SDK_PATH');
if ($sdk_path === false) {
@ -177,15 +177,19 @@ class DefaultShell extends Shell
$mute = $this->console_putput ? '' : ' > NUL';
if ($is_txz) {
// txz/tar.xz contains a tar file inside, extract twice
$cmd = "{$_7z} x {$archive_arg} -so | {$_7z} x -si -ttar -o{$target_arg} -y{$mute}";
} else {
$cmd = "{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}";
}
$run = function ($cmd) {
$this->logCommandInfo($cmd);
logger()->debug("[7Z EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput);
};
$this->logCommandInfo($cmd);
logger()->debug("[7Z EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
$extname = FileSystem::extname($archive_path);
match ($extname) {
'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'),
'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar -f - -x -C {$target_arg} --strip-components 1"),
default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"),
};
return true;
}
}

View File

@ -148,7 +148,9 @@ abstract class Shell
bool $console_output = false,
?string $original_command = null,
bool $capture_output = false,
bool $throw_on_error = true
bool $throw_on_error = true,
?string $cwd = null,
?array $env = null,
): array {
$file_res = null;
if ($this->enable_log_file) {
@ -160,10 +162,16 @@ abstract class Shell
}
$descriptors = [
0 => ['file', 'php://stdin', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
1 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stdout
2 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stderr
];
$process = proc_open($cmd, $descriptors, $pipes);
if ($env !== null && $env !== []) {
// merge current PHP envs
$env = array_merge(getenv(), $env);
} else {
$env = null;
}
$process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env, options: PHP_OS_FAMILY === 'Windows' ? ['create_process_group' => true] : null);
$output_value = '';
try {

View File

@ -33,7 +33,7 @@ class UnixShell extends Shell
$original_command = $cmd;
$this->logCommandInfo($original_command);
$this->last_cmd = $cmd = $this->getExecString($cmd);
$this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true);
$this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd);
return $this;
}
@ -71,7 +71,7 @@ class UnixShell extends Shell
}
$cmd = $this->getExecString($cmd);
$this->logCommandInfo($cmd);
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd);
$out = explode("\n", $result['output']);
return [$result['code'], $out];
}
@ -83,9 +83,6 @@ class UnixShell extends Shell
if (!empty($env_str)) {
$cmd = "{$env_str} {$cmd}";
}
if ($this->cd !== null) {
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
}
return $cmd;
}
}

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace StaticPHP\Runtime\Shell;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\SPCInternalException;
use ZM\Logger\ConsoleColor;
@ -28,7 +27,7 @@ class WindowsCmd extends Shell
$this->last_cmd = $cmd = $this->getExecString($cmd);
// echo $cmd . PHP_EOL;
$this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true);
$this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd);
return $this;
}
@ -46,111 +45,18 @@ class WindowsCmd extends Shell
logger()->debug('Running command with result: ' . $cmd);
}
$cmd = $this->getExecString($cmd);
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env);
$out = explode("\n", $result['output']);
return [$result['code'], $out];
}
public function setEnv(array $env): static
{
// windows currently does not support setting environment variables
throw new SPCInternalException('Windows does not support setting environment variables in shell commands.');
}
public function appendEnv(array $env): static
{
// windows currently does not support appending environment variables
throw new SPCInternalException('Windows does not support appending environment variables in shell commands.');
}
public function getLastCommand(): string
{
return $this->last_cmd;
}
/**
* Executes a command with console and log file output.
*
* @param string $cmd Full command to execute (including cd and env vars)
* @param bool $console_output If true, output will be printed to console
* @param null|string $original_command Original command string for logging
* @param bool $capture_output If true, capture and return output
* @param bool $throw_on_error If true, throw exception on non-zero exit code
*
* @return array{code: int, output: string} Returns exit code and captured output
*/
protected function passthru(
string $cmd,
bool $console_output = false,
?string $original_command = null,
bool $capture_output = false,
bool $throw_on_error = true
): array {
$file_res = null;
if ($this->enable_log_file) {
$file_res = fopen(SPC_SHELL_LOG, 'a');
}
$output_value = '';
try {
$process = popen($cmd . ' 2>&1', 'r');
if (!$process) {
throw new ExecutionException(
cmd: $original_command ?? $cmd,
message: 'Failed to open process for command, popen() failed.',
code: -1,
cd: $this->cd,
env: $this->env
);
}
while (($line = fgets($process)) !== false) {
if (static::$passthru_callback !== null) {
$callback = static::$passthru_callback;
$callback();
}
if ($console_output) {
echo $line;
}
if ($file_res !== null) {
fwrite($file_res, $line);
}
if ($capture_output) {
$output_value .= $line;
}
}
$result_code = pclose($process);
if ($throw_on_error && $result_code !== 0) {
if ($file_res !== null) {
fwrite($file_res, "Command exited with non-zero code: {$result_code}\n");
}
throw new ExecutionException(
cmd: $original_command ?? $cmd,
message: "Command exited with non-zero code: {$result_code}",
code: $result_code,
cd: $this->cd,
env: $this->env,
);
}
return [
'code' => $result_code,
'output' => $output_value,
];
} finally {
if ($file_res !== null) {
fclose($file_res);
}
}
}
private function getExecString(string $cmd): string
{
if ($this->cd !== null) {
$cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd;
}
return $cmd;
}
}

View File

@ -4,16 +4,57 @@ declare(strict_types=1);
namespace StaticPHP\Toolchain;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\System\WindowsUtil;
class MSVCToolchain implements ToolchainInterface
{
public function initEnv(): void {}
public function initEnv(): void
{
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\bin');
$sdk = getenv('PHP_SDK_PATH');
if ($sdk !== false) {
GlobalEnvManager::addPathIfNotExists($sdk . '\bin');
GlobalEnvManager::addPathIfNotExists($sdk . '\msys2\usr\bin');
}
// strawberry-perl
if (is_dir(PKG_ROOT_PATH . '\strawberry-perl')) {
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl\perl\bin');
}
}
public function afterInit(): void {}
public function afterInit(): void
{
$count = count(getenv());
$vs = WindowsUtil::findVisualStudio();
if ($vs === false || !file_exists($vcvarsall = "{$vs['dir']}\\VC\\Auxiliary\\Build\\vcvarsall.bat")) {
throw new EnvironmentException(
'Visual Studio with C++ tools not found',
'Please install Visual Studio with C++ tools'
);
}
if (getenv('VCINSTALLDIR') === false) {
if (file_exists(DOWNLOAD_PATH . '/.vcenv-cache') && (time() - filemtime(DOWNLOAD_PATH . '/.vcenv-cache')) < 3600) {
$output = file(DOWNLOAD_PATH . '/.vcenv-cache', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
} else {
exec('call "' . $vcvarsall . '" x64 > NUL && set', $output);
file_put_contents(DOWNLOAD_PATH . '/.vcenv-cache', implode("\n", $output));
}
array_map(fn ($x) => putenv($x), $output);
}
$after = count(getenv());
if ($after > $count) {
logger()->debug('Applied ' . ($after - $count) . ' environment variables from Visual Studio setup');
}
}
public function getCompilerInfo(): ?string
{
if ($vcver = getenv('VisualStudioVersion')) {
return "Visual Studio {$vcver}";
}
return null;
}

View File

@ -120,7 +120,7 @@ class FileSystem
$src_path = FileSystem::convertPath($from);
switch (PHP_OS_FAMILY) {
case 'Windows':
f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
break;
case 'Linux':
case 'Darwin':
@ -137,7 +137,7 @@ class FileSystem
* @param string $from Source file path
* @param string $to Destination file path
*/
public static function copy(string $from, string $to): void
public static function copy(string $from, string $to): bool
{
logger()->debug("Copying file from {$from} to {$to}");
$dst_path = FileSystem::convertPath($to);
@ -145,6 +145,7 @@ class FileSystem
if (!copy($src_path, $dst_path)) {
throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path);
}
return true;
}
/**
@ -266,6 +267,9 @@ class FileSystem
if ($auto_require && !class_exists($class_name, false)) {
require_once $file_path;
}
if (class_exists($class_name, false) === false) {
continue;
}
if (is_string($return_path_value)) {
$classes[$class_name] = $return_path_value . '/' . $v;
@ -317,7 +321,12 @@ class FileSystem
}
} elseif (is_link($sub_file) || is_file($sub_file)) {
if (!unlink($sub_file)) {
return false;
$cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f';
f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret);
if ($ret !== 0) {
logger()->warning('Remove file failed: ' . $sub_file);
return false;
}
}
}
}

View File

@ -107,6 +107,8 @@ class GlobalEnvManager
{
if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) {
self::putenv("PATH={$path}:" . getenv('PATH'));
} elseif (SystemTarget::getTargetOS() === 'Windows' && !str_contains(getenv('PATH'), $path)) {
self::putenv("PATH={$path};" . getenv('PATH'));
}
}

View File

@ -23,6 +23,7 @@ class InteractiveTerm
logger()->notice(strip_ansi_colors($message));
} else {
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message));
logger()->debug(strip_ansi_colors($message));
}
}
@ -34,15 +35,22 @@ class InteractiveTerm
logger()->info(strip_ansi_colors($message));
} else {
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message));
logger()->debug(strip_ansi_colors($message));
}
}
public static function plain(string $message): void
public static function plain(string $message, string $level = 'info'): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
match ($level) {
'debug' => logger()->debug(strip_ansi_colors($message)),
'notice' => logger()->notice(strip_ansi_colors($message)),
'warning' => logger()->warning(strip_ansi_colors($message)),
'error' => logger()->error(strip_ansi_colors($message)),
default => logger()->info(strip_ansi_colors($message)),
};
} else {
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
}
@ -66,6 +74,7 @@ class InteractiveTerm
logger()->error(strip_ansi_colors($message));
} else {
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message)));
logger()->debug(strip_ansi_colors($message));
}
}
@ -78,6 +87,7 @@ class InteractiveTerm
{
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
logger()->debug(strip_ansi_colors($message));
}
public static function finish(string $message, bool $status = true): void
@ -117,6 +127,7 @@ class InteractiveTerm
self::$indicator->advance();
return;
}
logger()->debug(strip_ansi_colors($message));
// if no ansi, use a dot instead of spinner
if ($no_ansi) {
self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' •', ' •']);

View File

@ -6,6 +6,7 @@ namespace StaticPHP\Util;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Exception\PatchException;
use StaticPHP\Registry\PackageLoader;
/**
* SourcePatcher provides static utility methods for patching source files.
@ -194,4 +195,69 @@ class SourcePatcher
{
FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c');
}
public static function patchPhpSrc(?array $items = null): bool
{
$patch_dir = ROOT_DIR . '/src/globals/patch/php-src-patches';
// in phar mode, we need to extract all the patch files
if (str_starts_with($patch_dir, 'phar://')) {
$tmp_dir = sys_get_temp_dir() . '/php-src-patches';
FileSystem::createDir($tmp_dir);
foreach (FileSystem::scanDirFiles($patch_dir) as $file) {
FileSystem::writeFile("{$tmp_dir}/" . basename($file), file_get_contents($file));
}
$patch_dir = $tmp_dir;
}
$php_package = PackageLoader::getTargetPackage('php');
if (!file_exists("{$php_package->getSourceDir()}/sapi/micro/php_micro.c")) {
return false;
}
$ver_file = "{$php_package->getSourceDir()}/main/php_version.h";
if (!file_exists($ver_file)) {
throw new PatchException('php-src patcher (original micro patches)', 'Patch failed, cannot find php source files');
}
$version_h = FileSystem::readFile("{$php_package->getSourceDir()}/main/php_version.h");
preg_match('/#\s*define\s+PHP_MAJOR_VERSION\s+(\d+)\s+#\s*define\s+PHP_MINOR_VERSION\s+(\d+)\s+/m', $version_h, $match);
// $ver = "{$match[1]}.{$match[2]}";
$major_ver = $match[1] . $match[2];
if ($major_ver === '74') {
return false;
}
// $check = !defined('DEBUG_MODE') ? ' -q' : '';
// f_passthru('cd ' . SOURCE_PATH . '/php-src && git checkout' . $check . ' HEAD');
if ($items !== null) {
$spc_micro_patches = $items;
} else {
$spc_micro_patches = getenv('SPC_MICRO_PATCHES');
$spc_micro_patches = $spc_micro_patches === false ? [] : explode(',', $spc_micro_patches);
}
$spc_micro_patches = array_filter($spc_micro_patches, fn ($item) => trim((string) $item) !== '');
$patch_list = $spc_micro_patches;
$patches = [];
$serial = ['80', '81', '82', '83', '84', '85'];
foreach ($patch_list as $patchName) {
if (file_exists("{$patch_dir}/{$patchName}.patch")) {
$patches[] = "{$patch_dir}/{$patchName}.patch";
continue;
}
for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) {
$tryMajMin = $serial[$i];
if (!file_exists("{$patch_dir}/{$patchName}_{$tryMajMin}.patch")) {
continue;
}
$patches[] = "{$patch_dir}/{$patchName}_{$tryMajMin}.patch";
continue 2;
}
throw new PatchException('phpmicro patches', "Failed finding patch file or versioned file {$patchName} !");
}
foreach ($patches as $patch) {
logger()->info("Patching micro with {$patch}");
self::patchFile($patch, $php_package->getSourceDir());
}
return true;
}
}

View File

@ -15,13 +15,10 @@ class WindowsUtil
* @param array $paths search path (default use env path)
* @return null|string null if not found, string is absolute path
*/
public static function findCommand(string $name, array $paths = [], bool $include_sdk_bin = false): ?string
public static function findCommand(string $name, array $paths = []): ?string
{
if (!$paths) {
$paths = explode(PATH_SEPARATOR, getenv('Path'));
if ($include_sdk_bin) {
$paths[] = getenv('PHP_SDK_PATH') . '\bin';
}
}
foreach ($paths as $path) {
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
@ -34,29 +31,35 @@ class WindowsUtil
/**
* Find Visual Studio installation.
*
* @return array<string, string>|false False if not installed, array contains 'version' and 'dir'
* @return array{
* version: string,
* major_version: string,
* dir: string
* }|false False if not installed, array contains 'version' and 'dir'
*/
public static function findVisualStudio(): array|false
{
$check_path = [
'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
'C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
// call vswhere (need VS and C++ tools installed), output is json
$vswhere_exec = PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'vswhere.exe';
$args = [
'-latest',
'-format', 'json',
'-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
];
foreach ($check_path as $path => $vs_version) {
if (file_exists($path)) {
$vs_ver = $vs_version;
$d_dir = dirname($path, 4);
return [
'version' => $vs_ver,
'dir' => $d_dir,
];
}
$cmd = escapeshellarg($vswhere_exec) . ' ' . implode(' ', $args);
$result = f_exec($cmd, $out, $code);
if ($code !== 0 || !$result) {
return false;
}
return false;
$json = json_decode(implode("\n", $out), true);
if (!is_array($json) || count($json) === 0) {
return false;
}
return [
'version' => $json[0]['installationVersion'],
'major_version' => explode('.', $json[0]['installationVersion'])[0],
'dir' => $json[0]['installationPath'],
];
}
/**

View File

@ -377,6 +377,10 @@ class PackageLoaderTest extends TestCase
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
// Add a build function for current OS so the stage validation is triggered
$package = PackageLoader::getPackage('test-lib');
$package->addBuildFunction(PHP_OS_FAMILY, fn () => null);
// Manually add a before_stage for non-existent stage
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
@ -417,6 +421,33 @@ class PackageLoaderTest extends TestCase
PackageLoader::checkLoadedStageEvents();
}
public function testCheckLoadedStageEventsDoesNotThrowForNonCurrentOSPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
// Add a build function for a different OS (not current OS)
$package = PackageLoader::getPackage('test-lib');
$otherOS = PHP_OS_FAMILY === 'Windows' ? 'Linux' : 'Windows';
$package->addBuildFunction($otherOS, fn () => null);
// Manually add a before_stage for 'build' stage
// This should NOT throw an exception because the package has no build function for current OS
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-lib' => [
'build' => [[fn () => null, null]],
],
]);
// This should not throw an exception
PackageLoader::checkLoadedStageEvents();
$this->assertTrue(true); // If we get here, the test passed
}
public function testGetBeforeStageCallbacksReturnsCallbacks(): void
{
PackageLoader::initPackageInstances();
@ -502,13 +533,13 @@ class PackageLoaderTest extends TestCase
mkdir($psr4Dir, 0755, true);
// Create test class file
$classContent = '<?php
namespace Test\Package;
use StaticPHP\Attribute\Package\Library;
#[Library("test-lib")]
class TestPackage1 {
$classContent = '<?php
namespace Test\Package;
use StaticPHP\Attribute\Package\Library;
#[Library("test-lib")]
class TestPackage1 {
}';
file_put_contents($psr4Dir . '/TestPackage1.php', $classContent);