This commit is contained in:
crazywhalecc
2025-11-30 15:35:04 +08:00
parent f6c818d3c0
commit 14bfb4198a
179 changed files with 19502 additions and 655 deletions

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\Package;
class DependencyResolver
{
/**
* Resolve dependencies for given packages.
* It will return an array of package names in the order they should be built/installed.
*
* @param Package[]|string[] $packages Package instance
* @param array<string, string[]> $dependency_overrides Override dependencies (e.g. ['php' => ['ext-gd', 'ext-curl']])
* @param bool $include_suggests Include suggested packages in the resolution
* @param null|array<string, string[]> &$why If provided, will be filled with a reverse dependency map
* @return array<string> Resolved package names in order
*/
public static function resolve(array $packages, array $dependency_overrides = [], bool $include_suggests = false, ?array &$why = null): array
{
$dep_list = PackageConfig::getAll();
$dep_list_clean = [];
// clear array for further step
foreach ($dep_list as $k => $v) {
$dep_list_clean[$k] = [
'depends' => PackageConfig::get($k, 'depends', []),
'suggests' => PackageConfig::get($k, 'suggests', []),
];
}
// apply dependency overrides
foreach ($dependency_overrides as $target_name => $deps) {
$dep_list_clean[$target_name]['depends'] = array_merge($dep_list_clean[$target_name]['depends'] ?? [], $deps);
}
// mark suggests as depends
if ($include_suggests) {
foreach ($dep_list_clean as $pkg_name => $pkg_item) {
$dep_list_clean[$pkg_name]['depends'] = array_merge($pkg_item['depends'], $pkg_item['suggests']);
$dep_list_clean[$pkg_name]['suggests'] = [];
}
}
$resolved = self::doVisitPlat($packages, $dep_list_clean);
// Build reverse dependency map if $why is requested
if ($why !== null) {
$why = self::buildReverseDependencyMap($resolved, $dep_list_clean, $include_suggests);
}
return $resolved;
}
/**
* Build a reverse dependency map for the resolved packages.
* For each package that is depended upon, list which packages depend on it.
*
* @param array<string> $resolved Resolved package names
* @param array<string, array{depends: string[], suggests: string[]}> $dep_list Dependency declaration list
* @param bool $include_suggests Whether suggests are treated as depends
* @return array<string, string[]> Reverse dependency map [depended_pkg => [dependant1, dependant2, ...]]
*/
private static function buildReverseDependencyMap(array $resolved, array $dep_list, bool $include_suggests): array
{
$why = [];
$resolved_set = array_flip($resolved);
foreach ($resolved as $pkg_name) {
$deps = $dep_list[$pkg_name]['depends'] ?? [];
if ($include_suggests) {
$deps = array_merge($deps, $dep_list[$pkg_name]['suggests'] ?? []);
}
foreach ($deps as $dep) {
// Only include dependencies that are in the resolved set
if (isset($resolved_set[$dep])) {
if (!isset($why[$dep])) {
$why[$dep] = [];
}
$why[$dep][] = $pkg_name;
}
}
}
return $why;
}
/**
* Visitor pattern implementation for dependency resolution.
*
* @param Package[]|string[] $packages Packages list (input)
* @param array<string, array{depends: string[], suggests: string[]}> $dep_list Dependency declaration list
* @return array Resolved packages array
*/
private static function doVisitPlat(array $packages, array $dep_list): array
{
$sorted = [];
$visited = [];
foreach ($packages as $pkg) {
$pkg_name = is_string($pkg) ? $pkg : $pkg->getName();
if (!isset($dep_list[$pkg_name])) {
throw new WrongUsageException("Package '{$pkg_name}' does not exist in config, please check your package name !");
}
if (!isset($visited[$pkg_name])) {
self::visitPlatDeps($pkg_name, $dep_list, $visited, $sorted);
}
}
$sorted_suggests = [];
$visited_suggests = [];
$final = [];
foreach ($packages as $pkg) {
$pkg_name = is_string($pkg) ? $pkg : $pkg->getName();
if (!isset($visited_suggests[$pkg_name])) {
self::visitPlatAllDeps($pkg_name, $dep_list, $visited_suggests, $sorted_suggests);
}
}
foreach ($sorted_suggests as $suggest) {
if (in_array($suggest, $sorted)) {
$final[] = $suggest;
}
}
return $final;
}
private static function visitPlatAllDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void
{
// 如果已经识别到了,那就不管
if (isset($visited[$pkg_name])) {
return;
}
$visited[$pkg_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
foreach (array_merge($dep_list[$pkg_name]['depends'], $dep_list[$pkg_name]['suggests']) as $dep) {
self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted);
}
$sorted[] = $pkg_name;
}
private static function visitPlatDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void
{
// 如果已经识别到了,那就不管
if (isset($visited[$pkg_name])) {
return;
}
$visited[$pkg_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
if (!isset($dep_list[$pkg_name])) {
throw new WrongUsageException("{$pkg_name} not exist !");
}
foreach ($dep_list[$pkg_name]['depends'] as $dep) {
self::visitPlatDeps($dep, $dep_list, $visited, $sorted);
}
$sorted[] = $pkg_name;
}
}

View File

@@ -0,0 +1,495 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Exception\FileSystemException;
class FileSystem
{
/**
* Read file contents and throw exception on failure
*
* @param string $filename The file path to read
* @return string The file contents
*/
public static function readFile(string $filename): string
{
// logger()->debug('Reading file: ' . $filename);
$r = file_get_contents(self::convertPath($filename));
if ($r === false) {
throw new FileSystemException('Reading file ' . $filename . ' failed');
}
return $r;
}
/**
* Replace string content in file
*
* @param string $filename The file path
* @param mixed $search The search string
* @param mixed $replace The replacement string
* @return false|int Number of replacements or false on failure
*/
public static function replaceFileStr(string $filename, mixed $search = null, mixed $replace = null): false|int
{
return self::replaceFile($filename, REPLACE_FILE_STR, $search, $replace);
}
/**
* Replace content in file using regex
*
* @param string $filename The file path
* @param mixed $search The regex pattern
* @param mixed $replace The replacement string
* @return false|int Number of replacements or false on failure
*/
public static function replaceFileRegex(string $filename, mixed $search = null, mixed $replace = null): false|int
{
return self::replaceFile($filename, REPLACE_FILE_PREG, $search, $replace);
}
/**
* Replace content in file using custom callback
*
* @param string $filename The file path
* @param mixed $callback The callback function
* @return false|int Number of replacements or false on failure
*/
public static function replaceFileUser(string $filename, mixed $callback = null): false|int
{
return self::replaceFile($filename, REPLACE_FILE_USER, $callback);
}
/**
* Get file extension from filename
*
* @param string $fn The filename
* @return string The file extension (without dot)
*/
public static function extname(string $fn): string
{
$parts = explode('.', basename($fn));
if (count($parts) < 2) {
return '';
}
return array_pop($parts);
}
/**
* Find command path in system PATH (similar to which command)
*
* @param string $name The command name
* @param array $paths Optional array of paths to search
* @return null|string The full path to the command or null if not found
*/
public static function findCommandPath(string $name, array $paths = []): ?string
{
if (!$paths) {
$paths = explode(PATH_SEPARATOR, getenv('PATH'));
}
if (PHP_OS_FAMILY === 'Windows') {
foreach ($paths as $path) {
foreach (['.exe', '.bat', '.cmd'] as $suffix) {
if (file_exists($path . DIRECTORY_SEPARATOR . $name . $suffix)) {
return $path . DIRECTORY_SEPARATOR . $name . $suffix;
}
}
}
return null;
}
foreach ($paths as $path) {
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
return $path . DIRECTORY_SEPARATOR . $name;
}
}
return null;
}
/**
* Copy directory recursively
*
* @param string $from Source directory path
* @param string $to Destination directory path
*/
public static function copyDir(string $from, string $to): void
{
logger()->debug("Copying directory from {$from} to {$to}");
$dst_path = FileSystem::convertPath($to);
$src_path = FileSystem::convertPath($from);
switch (PHP_OS_FAMILY) {
case 'Windows':
f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
break;
case 'Linux':
case 'Darwin':
case 'BSD':
f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"');
break;
}
}
/**
* Copy file from one location to another.
* This method will throw an exception if the copy operation fails.
*
* @param string $from Source file path
* @param string $to Destination file path
*/
public static function copy(string $from, string $to): void
{
logger()->debug("Copying file from {$from} to {$to}");
$dst_path = FileSystem::convertPath($to);
$src_path = FileSystem::convertPath($from);
if (!copy($src_path, $dst_path)) {
throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path);
}
}
/**
* Convert path to system-specific format
*
* @param string $path The path to convert
* @return string The converted path
*/
public static function convertPath(string $path): string
{
if (str_starts_with($path, 'phar://')) {
return $path;
}
return str_replace('/', DIRECTORY_SEPARATOR, $path);
}
/**
* Convert Windows path to MinGW format
*
* @param string $path The Windows path
* @return string The MinGW format path
*/
public static function convertWinPathToMinGW(string $path): string
{
if (preg_match('/^[A-Za-z]:/', $path)) {
$path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2));
}
return $path;
}
/**
* Scan directory files recursively
*
* @param string $dir Directory to scan
* @param bool $recursive Whether to scan recursively
* @param bool|string $relative Whether to return relative paths
* @param bool $include_dir Whether to include directories in result
* @return array|false Array of files or false on failure
*/
public static function scanDirFiles(string $dir, bool $recursive = true, bool|string $relative = false, bool $include_dir = false): array|false
{
$dir = self::convertPath($dir);
if (!is_dir($dir)) {
return false;
}
logger()->debug('scanning directory ' . $dir);
$scan_list = scandir($dir);
if ($scan_list === false) {
logger()->warning('Scan dir failed, cannot scan directory: ' . $dir);
return false;
}
$list = [];
// 将 relative 置为相对目录的前缀
if ($relative === true) {
$relative = $dir;
}
// 遍历目录
foreach ($scan_list as $v) {
// Unix 系统排除这俩目录
if ($v == '.' || $v == '..') {
continue;
}
$sub_file = self::convertPath($dir . '/' . $v);
if (is_dir($sub_file) && $recursive) {
# 如果是 目录 且 递推 , 则递推添加下级文件
$sub_list = self::scanDirFiles($sub_file, $recursive, $relative);
if (is_array($sub_list)) {
foreach ($sub_list as $item) {
$list[] = $item;
}
}
} elseif (is_file($sub_file) || (is_dir($sub_file) && !$recursive && $include_dir)) {
# 如果是 文件 或 (是 目录 且 不递推 且 包含目录)
if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) {
$list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\');
} elseif ($relative === false) {
$list[] = $sub_file;
}
}
}
return $list;
}
/**
* Get PSR-4 classes from directory
*
* @param string $dir Directory to scan
* @param string $base_namespace Base namespace
* @param mixed $rule Optional filtering rule
* @param bool|string $return_path_value Whether to return path as value
* @param bool $auto_require Whether to auto-require files (useful for external plugins)
* @return array Array of class names or class=>path pairs
*/
public static function getClassesPsr4(string $dir, string $base_namespace, mixed $rule = null, bool|string $return_path_value = false, bool $auto_require = false): array
{
$classes = [];
$files = FileSystem::scanDirFiles($dir, true, true);
if ($files === false) {
throw new FileSystemException('Cannot scan dir files during get classes psr-4 from dir: ' . $dir);
}
foreach ($files as $v) {
$pathinfo = pathinfo($v);
if (($pathinfo['extension'] ?? '') == 'php') {
if ($rule === null) {
if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) {
continue;
}
if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') {
continue;
}
} elseif (is_callable($rule) && !$rule($dir, $pathinfo)) {
continue;
}
$dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\');
$class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename'];
$file_path = self::convertPath($dir . '/' . $v);
// Auto-require file if class is not loaded (for external plugins not in composer autoload)
if ($auto_require && !class_exists($class_name, false)) {
require_once $file_path;
}
if (is_string($return_path_value)) {
$classes[$class_name] = $return_path_value . '/' . $v;
} else {
$classes[] = $class_name;
}
}
}
return $classes;
}
/**
* Remove directory recursively
*
* @param string $dir Directory to remove
* @return bool Success status
*/
public static function removeDir(string $dir): bool
{
$dir = self::convertPath($dir);
logger()->debug('Removing path recursively: "' . $dir . '"');
if (!file_exists($dir)) {
logger()->debug('Scan dir failed, no such file or directory.');
return false;
}
if (!is_dir($dir)) {
logger()->warning('Scan dir failed, not directory.');
return false;
}
logger()->debug('scanning directory ' . $dir);
// 套上 zm_dir
$scan_list = scandir($dir);
if ($scan_list === false) {
logger()->warning('Scan dir failed, cannot scan directory: ' . $dir);
return false;
}
// 遍历目录
foreach ($scan_list as $v) {
InteractiveTerm::advance();
// Unix 系统排除这俩目录
if ($v == '.' || $v == '..') {
continue;
}
$sub_file = self::convertPath($dir . '/' . $v);
if (is_dir($sub_file)) {
# 如果是 目录 且 递推 , 则递推添加下级文件
if (!self::removeDir($sub_file)) {
return false;
}
} elseif (is_link($sub_file) || is_file($sub_file)) {
if (!unlink($sub_file)) {
return false;
}
}
}
if (is_link($dir)) {
return @unlink($dir);
}
return @rmdir($dir);
}
/**
* Create directory recursively
*
* @param string $path Directory path to create
*/
public static function createDir(string $path): void
{
if (!is_dir($path) && !f_mkdir($path, 0755, true) && !is_dir($path)) {
throw new FileSystemException(sprintf('Unable to create dir: %s', $path));
}
}
/**
* Write content to file
*
* @param string $path File path
* @param mixed $content Content to write
* @param mixed ...$args Additional arguments passed to file_put_contents
* @return bool|int|string Result of file writing operation
*/
public static function writeFile(string $path, mixed $content, ...$args): bool|int|string
{
$dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME);
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir);
}
return file_put_contents($path, $content, ...$args);
}
/**
* Reset directory by removing and recreating it
*
* @param string $dir_name Directory name
*/
public static function resetDir(string $dir_name): void
{
$dir_name = self::convertPath($dir_name);
if (is_dir($dir_name)) {
self::removeDir($dir_name);
}
self::createDir($dir_name);
}
/**
* Add source extraction hook
*
* @param string $name Source name
* @param callable $callback Callback function
*/
public static function addSourceExtractHook(string $name, callable $callback): void
{
self::$_extract_hook[$name][] = $callback;
}
/**
* Check if path is relative
*
* @param string $path Path to check
* @return bool True if path is relative
*/
public static function isRelativePath(string $path): bool
{
if (DIRECTORY_SEPARATOR === '\\') {
return !(strlen($path) > 2 && ctype_alpha($path[0]) && $path[1] === ':');
}
return strlen($path) > 0 && $path[0] !== '/';
}
/**
* Replace path variables with actual values
*
* @param string $path Path with variables
* @return string Path with replaced variables
*/
public static function replacePathVariable(string $path): string
{
$replacement = [
'{pkg_root_path}' => PKG_ROOT_PATH,
'{php_sdk_path}' => getenv('PHP_SDK_PATH') ? getenv('PHP_SDK_PATH') : WORKING_DIR . '/php-sdk-binary-tools',
'{working_dir}' => WORKING_DIR,
'{download_path}' => DOWNLOAD_PATH,
'{source_path}' => SOURCE_PATH,
];
return str_replace(array_keys($replacement), array_values($replacement), $path);
}
/**
* Create backup of file
*
* @param string $path File path
* @return string Backup file path
*/
public static function backupFile(string $path): string
{
copy($path, $path . '.bak');
return $path . '.bak';
}
/**
* Restore file from backup
*
* @param string $path Original file path
*/
public static function restoreBackupFile(string $path): void
{
if (!file_exists($path . '.bak')) {
throw new FileSystemException("Backup restore failed: Cannot find bak file for {$path}");
}
copy($path . '.bak', $path);
unlink($path . '.bak');
}
/**
* Remove file if it exists
*
* @param string $string File path
*/
public static function removeFileIfExists(string $string): void
{
$string = self::convertPath($string);
if (file_exists($string)) {
unlink($string);
}
}
/**
* Replace line in file that contains specific string
*
* @param string $file File path
* @param string $find String to find in line
* @param string $line New line content
* @return false|int Number of replacements or false on failure
*/
public static function replaceFileLineContainsString(string $file, string $find, string $line): false|int
{
$lines = file($file);
if ($lines === false) {
throw new FileSystemException('Cannot read file: ' . $file);
}
foreach ($lines as $key => $value) {
if (str_contains($value, $find)) {
$lines[$key] = $line . PHP_EOL;
}
}
return file_put_contents($file, implode('', $lines));
}
private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int
{
logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename);
$file = self::readFile($filename);
switch ($replace_type) {
case REPLACE_FILE_STR:
default:
$file = str_replace($callback_or_search, $to_replace, $file);
break;
case REPLACE_FILE_PREG:
$file = preg_replace($callback_or_search, $to_replace, $file);
break;
case REPLACE_FILE_USER:
$file = $callback_or_search($file);
break;
}
return file_put_contents($filename, $file);
}
}

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Util\System\MacOSUtil;
/**
* Environment variable manager
*/
class GlobalEnvManager
{
private static array $env_cache = [];
private static bool $initialized = false;
public static function getInitializedEnv(): array
{
return self::$env_cache;
}
/**
* Initialize the environment variables.
*/
public static function init(): void
{
if (self::$initialized) {
return;
}
// Check pre-defined env vars exists
if (getenv('BUILD_ROOT_PATH') === false) {
throw new SPCInternalException('You must include src/globals/internal-env.php before using GlobalEnvManager');
}
// Define env vars for unix
if (SystemTarget::isUnix()) {
self::addPathIfNotExists(BUILD_BIN_PATH);
self::addPathIfNotExists(PKG_ROOT_PATH . '/bin');
$pkgConfigPath = getenv('PKG_CONFIG_PATH');
if ($pkgConfigPath !== false) {
self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . "/pkgconfig:{$pkgConfigPath}");
} else {
self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig');
}
}
$ini = self::readIniFile();
$default_put_list = [];
foreach ($ini['global'] as $k => $v) {
if (getenv($k) === false) {
$default_put_list[$k] = $v;
self::putenv("{$k}={$v}");
}
}
$os_ini = match (PHP_OS_FAMILY) {
'Windows' => $ini['windows'] ?? [],
'Darwin' => $ini['macos'] ?? [],
'Linux' => $ini['linux'] ?? [],
'BSD' => $ini['freebsd'] ?? [],
default => [],
};
foreach ($os_ini as $k => $v) {
if (getenv($k) === false) {
$default_put_list[$k] = $v;
self::putenv("{$k}={$v}");
}
}
ToolchainManager::initToolchain();
// apply second time
$ini2 = self::readIniFile();
foreach ($ini2['global'] as $k => $v) {
if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) {
self::putenv("{$k}={$v}");
}
}
$os_ini2 = match (PHP_OS_FAMILY) {
'Windows' => $ini2['windows'] ?? [],
'Darwin' => $ini2['macos'] ?? [],
'Linux' => $ini2['linux'] ?? [],
'BSD' => $ini2['freebsd'] ?? [],
default => [],
};
foreach ($os_ini2 as $k => $v) {
if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) {
self::putenv("{$k}={$v}");
}
}
self::$initialized = true;
}
public static function putenv(string $val): void
{
f_putenv($val);
self::$env_cache[] = $val;
}
public static function addPathIfNotExists(string $path): void
{
if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) {
self::putenv("PATH={$path}:" . getenv('PATH'));
}
}
/**
* Initialize the toolchain after the environment variables are set.
* The toolchain or environment availability check is done here.
*/
public static function afterInit(): void
{
if (!filter_var(getenv('SPC_SKIP_TOOLCHAIN_CHECK'), FILTER_VALIDATE_BOOL)) {
ToolchainManager::afterInitToolchain();
}
// test bison
if (PHP_OS_FAMILY === 'Darwin') {
if ($bison = MacOSUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) {
self::putenv("BISON={$bison}");
}
if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) {
self::putenv("YACC={$yacc}");
}
}
}
private static function readIniFile(): array
{
// Init env.ini file, read order:
// WORKING_DIR/config/env.ini
// ROOT_DIR/config/env.ini
$ini_files = [
WORKING_DIR . '/config/env.ini',
ROOT_DIR . '/config/env.ini',
];
$ini_custom = [
WORKING_DIR . '/config/env.custom.ini',
ROOT_DIR . '/config/env.custom.ini',
];
$ini = null;
foreach ($ini_files as $ini_file) {
if (file_exists($ini_file)) {
$ini = parse_ini_file($ini_file, true);
break;
}
}
if ($ini === null) {
throw new WrongUsageException('env.ini not found');
}
if ($ini === false || !isset($ini['global'])) {
throw new WrongUsageException('Failed to parse ' . $ini_file);
}
// apply custom env
foreach ($ini_custom as $ini_file) {
if (file_exists($ini_file)) {
$ini_custom = parse_ini_file($ini_file, true);
if ($ini_custom !== false) {
$ini['global'] = array_merge($ini['global'], $ini_custom['global'] ?? []);
match (PHP_OS_FAMILY) {
'Windows' => $ini['windows'] = array_merge($ini['windows'], $ini_custom['windows'] ?? []),
'Darwin' => $ini['macos'] = array_merge($ini['macos'], $ini_custom['macos'] ?? []),
'Linux' => $ini['linux'] = array_merge($ini['linux'], $ini_custom['linux'] ?? []),
'BSD' => $ini['freebsd'] = array_merge($ini['freebsd'], $ini_custom['freebsd'] ?? []),
default => null,
};
}
break;
}
}
return $ini;
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\DI\ApplicationContext;
use Symfony\Component\Console\Helper\ProgressIndicator;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;
class InteractiveTerm
{
private static ?ProgressIndicator $indicator = null;
public static function notice(string $message, bool $indent = false): void
{
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->notice(strip_ansi_colors($message));
} else {
$output->writeln(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message);
}
}
public static function success(string $message, bool $indent = false): void
{
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
} else {
$output->writeln(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message);
}
}
public static function plain(string $message): void
{
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
} else {
$output->writeln($message);
}
}
public static function info(string $message): void
{
$output = ApplicationContext::get(OutputInterface::class);
if (!$output->isVerbose()) {
$output->writeln(ConsoleColor::green('▶ ') . $message);
}
logger()->info(strip_ansi_colors($message));
}
public static function error(string $message, bool $indent = true): void
{
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->error(strip_ansi_colors($message));
} else {
$output->writeln('' . ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message));
}
}
public static function advance(): void
{
self::$indicator?->advance();
}
public static function setMessage(string $message): void
{
self::$indicator?->setMessage($message);
}
public static function finish(string $message, bool $status = true): void
{
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
if ($status) {
logger()->info($message);
} else {
logger()->error($message);
}
return;
}
if (self::$indicator !== null) {
if (!$status) {
self::$indicator->finish($message, '' . ConsoleColor::red(' ✘'));
} else {
self::$indicator->finish($message, '' . ConsoleColor::green(' ✔'));
}
self::$indicator = null;
}
}
public static function indicateProgress(string $message): void
{
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
return;
}
if (self::$indicator !== null) {
// just reuse existing indicator, change
self::setMessage($message);
self::$indicator->advance();
return;
}
self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' ⠏', ' ⠛', ' ⠹', ' ⢸', ' ⣰', ' ⣤', ' ⣆', ' ⡇']);
self::$indicator->start($message);
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Exception\ExecutionException;
/**
* Utility class for pkg-config operations
*
* This class provides methods to interact with pkg-config to get
* compilation flags and library information for building extensions.
*/
class PkgConfigUtil
{
/**
* Find the pkg-config executable which is compatible with static builds.
*
* @return null|string Path to pkg-config executable, or null if not found
*/
public static function findPkgConfig(): ?string
{
// Find pkg-config executable
$find_list = [
PKG_ROOT_PATH . '/bin/pkg-config',
BUILD_BIN_PATH . '/pkg-config',
];
$found = null;
foreach ($find_list as $file) {
if (file_exists($file) && is_executable($file)) {
$found = $file;
break;
}
}
return $found;
}
/**
* Returns the version of a module.
* This method uses `pkg-config --modversion` to get the version of the specified module.
*
* @param string $pkg_config_str .pc file str, accepts multiple files
* @return string version string, e.g. "1.2.3"
*/
public static function getModuleVersion(string $pkg_config_str): string
{
$result = self::execWithResult("pkg-config --modversion {$pkg_config_str}");
return trim($result);
}
/**
* Get CFLAGS from pkg-config
*
* Returns --cflags-only-other output from pkg-config.
* The reason we return the string is we cannot use array_unique() on cflags,
* some cflags may contains spaces.
*
* @param string $pkg_config_str .pc file string, accepts multiple files
* @return string CFLAGS string, e.g. "-Wno-implicit-int-float-conversion ..."
*/
public static function getCflags(string $pkg_config_str): string
{
// get other things
$result = self::execWithResult("pkg-config --static --cflags-only-other {$pkg_config_str}");
return trim($result);
}
/**
* Get library flags from pkg-config
*
* Returns --libs-only-l and --libs-only-other output.
* The reason we return the array is to avoid duplicate lib defines.
*
* @param string $pkg_config_str .pc file string, accepts multiple files
* @return array Unique libs array, e.g. [-lz, -lxml, ...]
*/
public static function getLibsArray(string $pkg_config_str): array
{
// Use this instead of shell() to avoid unnecessary outputs
$result = self::execWithResult("pkg-config --static --libs-only-l {$pkg_config_str}");
$libs = explode(' ', trim($result));
// get other things
$result = self::execWithResult("pkg-config --static --libs-only-other {$pkg_config_str}");
// convert libxxx.a to -L{path} -lxxx
$exp = explode(' ', trim($result));
foreach ($exp as $item) {
if (str_starts_with($item, '-L')) {
$libs[] = $item;
continue;
}
// if item ends with .a, convert it to -lxxx
if (str_ends_with($item, '.a') && (str_starts_with($item, 'lib') || str_starts_with($item, BUILD_LIB_PATH))) {
$name = pathinfo($item, PATHINFO_BASENAME);
$name = substr($name, 3, -2); // remove 'lib' prefix and '.a' suffix
$shortlib = "-l{$name}";
if (!in_array($shortlib, $libs)) {
$libs[] = $shortlib;
}
} elseif (!in_array($item, $libs)) {
$libs[] = $item;
}
}
// enhancement for linker
return array_reverse(array_unique(array_reverse($libs)));
}
/**
* Execute pkg-config command and return result
*
* @param string $cmd The pkg-config command to execute
* @return string The command output
*/
private static function execWithResult(string $cmd): string
{
f_exec($cmd, $output, $result_code);
if ($result_code !== 0) {
throw new ExecutionException($cmd, "pkg-config command failed with code: {$result_code}");
}
return implode("\n", $output);
}
}

View File

@@ -0,0 +1,302 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
class SPCConfigUtil
{
private bool $no_php;
private bool $libs_only_deps;
private bool $absolute_libs;
/**
* @param array{
* no_php?: bool,
* libs_only_deps?: bool,
* absolute_libs?: bool
* } $options Options pass to spc-config
*/
public function __construct(array $options = [])
{
$this->no_php = $options['no_php'] ?? false;
$this->libs_only_deps = $options['libs_only_deps'] ?? false;
$this->absolute_libs = $options['absolute_libs'] ?? false;
}
public function config(array $packages = [], bool $include_suggests = false): array
{
// if have php, make php as all extension's dependency
if (!$this->no_php) {
$dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))];
} else {
$dep_override = [];
}
$resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests);
$ldflags = $this->getLdflagsString();
$cflags = $this->getIncludesString($resolved);
$libs = $this->getLibsString($resolved, !$this->absolute_libs);
// additional OS-specific libraries (e.g. macOS -lresolv)
// embed
if ($extra_libs = SystemTarget::getRuntimeLibs()) {
$libs .= " {$extra_libs}";
}
$extra_env = getenv('SPC_EXTRA_LIBS');
if (is_string($extra_env) && !empty($extra_env)) {
$libs .= " {$extra_env}";
}
// package frameworks
if (SystemTarget::getTargetOS() === 'Darwin') {
$libs .= " {$this->getFrameworksString($resolved)}";
}
// C++
if ($this->hasCpp($resolved)) {
$libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++';
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
}
if ($this->libs_only_deps) {
// mimalloc must come first
if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs);
}
return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs),
];
}
// embed
if (!$this->no_php) {
$libs = "-lphp {$libs} -lc";
}
$allLibs = getenv('LIBS') . ' ' . $libs;
// mimalloc must come first
if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs);
}
return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces($allLibs),
];
}
/**
* [Helper function]
* Get configuration for a specific extension(s) dependencies.
*
* @param Extension|Extension[] $extension Extension instance or list
* @param bool $include_suggest_ext Whether to include suggested extensions
* @param bool $include_suggest_lib Whether to include suggested libraries
* @return array{
* cflags: string,
* ldflags: string,
* libs: string
* }
*/
public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array
{
if (!is_array($extension)) {
$extension = [$extension];
}
$libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension)));
return $this->config(
extensions: array_map(fn ($x) => $x->getName(), $extension),
libraries: $libs,
include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false,
include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false,
);
}
/**
* [Helper function]
* Get configuration for a specific library(s) dependencies.
*
* @param LibraryBase|LibraryBase[] $lib Library instance or list
* @param bool $include_suggest_lib Whether to include suggested libraries
* @return array{
* cflags: string,
* ldflags: string,
* libs: string
* }
*/
public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array
{
if (!is_array($lib)) {
$lib = [$lib];
}
$save_no_php = $this->no_php;
$this->no_php = true;
$save_libs_only_deps = $this->libs_only_deps;
$this->libs_only_deps = true;
$ret = $this->config(
libraries: array_map(fn ($x) => $x->getName(), $lib),
include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false,
);
$this->no_php = $save_no_php;
$this->libs_only_deps = $save_libs_only_deps;
return $ret;
}
private function hasCpp(array $packages): bool
{
foreach ($packages as $package) {
$lang = PackageConfig::get($package, 'lang', 'c');
if ($lang === 'cpp') {
return true;
}
}
return false;
}
private function getIncludesString(array $packages): string
{
$base = BUILD_INCLUDE_PATH;
$includes = ["-I{$base}"];
// link with libphp
if (!$this->no_php) {
$includes = [
...$includes,
"-I{$base}/php",
"-I{$base}/php/main",
"-I{$base}/php/TSRM",
"-I{$base}/php/Zend",
"-I{$base}/php/ext",
];
}
// parse pkg-configs
foreach ($packages as $package) {
$pc = PackageConfig::get($package, 'pkg-configs', []);
foreach ($pc as $file) {
if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$file}.pc")) {
throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first.");
}
}
$pc_cflags = implode(' ', $pc);
if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') {
$arr = explode(' ', $pc_cflags);
$arr = array_unique($arr);
$arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_'));
$pc_cflags = implode(' ', $arr);
$includes[] = $pc_cflags;
}
}
$includes = array_unique($includes);
return implode(' ', $includes);
}
private function getLdflagsString(): string
{
return '-L' . BUILD_LIB_PATH;
}
private function getLibsString(array $packages, bool $use_short_libs = true): string
{
$lib_names = [];
$frameworks = [];
foreach ($packages as $package) {
// add pkg-configs libs
$pkg_configs = PackageConfig::get($package, 'pkg-configs', []);
foreach ($pkg_configs as $pkg_config) {
if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) {
throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first.");
}
}
$pkg_configs = implode(' ', $pkg_configs);
if ($pkg_configs !== '') {
// static libs with dependencies come in reverse order, so reverse this too
$pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs));
$lib_names = [...$lib_names, ...$pc_libs];
}
// convert all static-libs to short names
$libs = array_reverse(PackageConfig::get($package, 'static-libs', []));
foreach ($libs as $lib) {
// check file existence
if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) {
throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first.");
}
$lib_names[] = $this->getShortLibName($lib);
}
// add frameworks for macOS
if (SystemTarget::getTargetOS() === 'Darwin') {
$frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', []));
}
}
// post-process
$lib_names = array_filter($lib_names, fn ($x) => $x !== '');
$lib_names = array_reverse(array_unique($lib_names));
$frameworks = array_unique($frameworks);
// process frameworks to short_name
if (SystemTarget::getTargetOS() === 'Darwin') {
foreach ($frameworks as $fw) {
$ks = '-framework ' . $fw;
if (!in_array($ks, $lib_names)) {
$lib_names[] = $ks;
}
}
}
if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') {
$lib_names[] = '-lcrypt';
}
if (!$use_short_libs) {
$lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names);
}
return implode(' ', $lib_names);
}
private function getShortLibName(string $lib): string
{
if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) {
return BUILD_LIB_PATH . '/' . $lib;
}
// get short name
return '-l' . substr($lib, 3, -2);
}
private function getFullLibName(string $lib): string
{
if (!str_starts_with($lib, '-l')) {
return $lib;
}
$libname = substr($lib, 2);
$staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a";
if (file_exists($staticLib)) {
return $staticLib;
}
return $lib;
}
private function getFrameworksString(array $extensions): string
{
$list = [];
foreach ($extensions as $extension) {
foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) {
$ks = '-framework ' . $fw;
if (!in_array($ks, $list)) {
$list[] = $ks;
}
}
}
return implode(' ', $list);
}
}

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Exception\PatchException;
/**
* SourcePatcher provides static utility methods for patching source files.
*/
class SourcePatcher
{
/**
* Use existing patch file for patching.
*
* @param string $patch_name Patch file name in src/globals/patch/ or absolute path
* @param string $cwd Working directory for patch command
* @param bool $reverse Reverse patches (default: false)
* @return bool True if patch was applied or already applied
*/
public static function patchFile(string $patch_name, string $cwd, bool $reverse = false): bool
{
logger()->debug('Applying ' . ($reverse ? 'reverse ' : '') . "patch [{$patch_name}] at [{$cwd}]");
if (FileSystem::isRelativePath($patch_name)) {
$patch_file = ROOT_DIR . "/src/globals/patch/{$patch_name}";
} else {
$patch_file = $patch_name;
}
if (!file_exists($patch_file)) {
throw new PatchException($patch_name, "Patch file [{$patch_file}] does not exist");
}
$patch_str = FileSystem::convertPath($patch_file);
if (!file_exists($patch_str)) {
throw new PatchException($patch_name, "Patch file [{$patch_str}] does not exist");
}
// Copy patch from phar
if (str_starts_with($patch_str, 'phar://')) {
$filename = pathinfo($patch_file, PATHINFO_BASENAME);
file_put_contents(SOURCE_PATH . "/{$filename}", file_get_contents($patch_file));
$patch_str = FileSystem::convertPath(SOURCE_PATH . "/{$filename}");
}
// Detect if patch is already applied (reverse detection)
$detect_reverse = !$reverse;
$detect_cmd = 'cd ' . escapeshellarg($cwd) . ' && '
. (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str)
. ' | patch --dry-run -p1 -s -f ' . ($detect_reverse ? '-R' : '')
. ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1';
exec($detect_cmd, $output, $detect_status);
if ($detect_status === 0) {
// Patch already applied
return true;
}
// Apply patch
$apply_cmd = 'cd ' . escapeshellarg($cwd) . ' && '
. (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str)
. ' | patch -p1 ' . ($reverse ? '-R' : '');
exec($apply_cmd, $apply_output, $apply_status);
if ($apply_status !== 0) {
throw new PatchException($patch_name, "Patch file [{$patch_name}] failed to apply");
}
return true;
}
/**
* Patch hardcoded INI values into PHP SAPI files.
*
* @param string $php_source_dir PHP source directory path
* @param array $ini Associative array of INI key-value pairs
* @return bool True if patch was applied successfully
*/
#[PatchDescription('Patch hardcoded INI values into PHP SAPI files')]
public static function patchHardcodedINI(string $php_source_dir, array $ini = []): bool
{
$sapi_files = [
'cli' => "{$php_source_dir}/sapi/cli/php_cli.c",
'micro' => "{$php_source_dir}/sapi/micro/php_micro.c",
'embed' => "{$php_source_dir}/sapi/embed/php_embed.c",
];
// Build patch string
$find_str = 'const char HARDCODED_INI[] =';
$patch_str = '';
foreach ($ini as $key => $value) {
$patch_str .= "\"{$key}={$value}\\n\"\n";
}
$patch_str = "const char HARDCODED_INI[] =\n{$patch_str}";
// Detect and restore from backup if exists
$has_backup = false;
foreach ($sapi_files as $file) {
if (file_exists("{$file}.bak")) {
$has_backup = true;
break;
}
}
if ($has_backup) {
self::unpatchHardcodedINI($php_source_dir);
}
// Backup and patch each SAPI file
$result = true;
foreach ($sapi_files as $file) {
if (!file_exists($file)) {
continue;
}
// Backup
$result = $result && file_put_contents("{$file}.bak", file_get_contents($file)) !== false;
// Patch
FileSystem::replaceFileStr($file, $find_str, $patch_str);
}
return $result;
}
/**
* Restore PHP SAPI files from backup (unpatch hardcoded INI).
*
* @param string $php_source_dir PHP source directory path
* @return bool True if backup was restored successfully
*/
public static function unpatchHardcodedINI(string $php_source_dir): bool
{
$sapi_files = [
'cli' => "{$php_source_dir}/sapi/cli/php_cli.c",
'micro' => "{$php_source_dir}/sapi/micro/php_micro.c",
'embed' => "{$php_source_dir}/sapi/embed/php_embed.c",
];
$has_backup = false;
foreach ($sapi_files as $file) {
if (file_exists("{$file}.bak")) {
$has_backup = true;
break;
}
}
if (!$has_backup) {
return false;
}
$result = true;
foreach ($sapi_files as $file) {
$backup = "{$file}.bak";
if (file_exists($backup)) {
$result = $result && file_put_contents($file, file_get_contents($backup)) !== false;
@unlink($backup);
}
}
return $result;
}
}

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util\System;
class LinuxUtil extends UnixUtil
{
/** @var null|string libc version cache */
public static ?string $libc_version = null;
/**
* Get current linux distro name and version.
*
* @noinspection PhpMissingBreakStatementInspection
* @return array{dist: string, ver: string} Linux distro info (unknown if not found)
*/
public static function getOSRelease(): array
{
$ret = [
'dist' => 'unknown',
'ver' => 'unknown',
];
switch (true) {
case file_exists('/etc/centos-release'):
$lines = file('/etc/centos-release');
$centos = true;
goto rh;
case file_exists('/etc/redhat-release'):
$lines = file('/etc/redhat-release');
$centos = false;
rh:
foreach ($lines as $line) {
if (preg_match('/release\s+(\d*(\.\d+)*)/', $line, $matches)) {
/* @phpstan-ignore-next-line */
$ret['dist'] = $centos ? 'centos' : 'redhat';
$ret['ver'] = $matches[1];
}
}
break;
case file_exists('/etc/os-release'):
$lines = file('/etc/os-release');
foreach ($lines as $line) {
if (preg_match('/^ID=(.*)$/', $line, $matches)) {
$ret['dist'] = $matches[1];
}
if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) {
$ret['ver'] = $matches[1];
}
}
$ret['dist'] = trim($ret['dist'], '"\'');
$ret['ver'] = trim($ret['ver'], '"\'');
if (strcasecmp($ret['dist'], 'centos') === 0) {
$ret['dist'] = 'redhat';
}
break;
}
return $ret;
}
/**
* Check if current linux distro is musl-based (alpine).
*/
public static function isMuslDist(): bool
{
return static::getOSRelease()['dist'] === 'alpine';
}
/**
* Get CPU core count.
*/
public static function getCpuCount(): int
{
$ncpu = 1;
if (is_file('/proc/cpuinfo')) {
$cpuinfo = file_get_contents('/proc/cpuinfo');
preg_match_all('/^processor/m', $cpuinfo, $matches);
$ncpu = count($matches[0]);
}
return $ncpu;
}
/**
* Get fully-supported linux distros.
*
* @return string[] List of supported Linux distro name for doctor
*/
public static function getSupportedDistros(): array
{
return [
// debian-like
'debian', 'ubuntu', 'Deepin',
// rhel-like
'redhat',
// centos
'centos',
// alpine
'alpine',
// arch
'arch', 'manjaro',
];
}
/**
* Get libc version string from ldd.
*/
public static function getLibcVersionIfExists(?string $libc = null): ?string
{
if (self::$libc_version !== null) {
return self::$libc_version;
}
if ($libc === 'glibc') {
$result = shell()->execWithResult('ldd --version', false);
if ($result[0] !== 0) {
return null;
}
// get first line
$first_line = $result[1][0];
// match ldd version: "ldd (some useless text) 2.17" match 2.17
$pattern = '/ldd\s+\(.*?\)\s+(\d+\.\d+)/';
if (preg_match($pattern, $first_line, $matches)) {
self::$libc_version = $matches[1];
return self::$libc_version;
}
return null;
}
if ($libc === 'musl') {
if (self::isMuslDist()) {
$result = shell()->execWithResult('ldd 2>&1', false);
} elseif (is_file('/usr/local/musl/lib/libc.so')) {
$result = shell()->execWithResult('/usr/local/musl/lib/libc.so 2>&1', false);
} else {
$arch = php_uname('m');
$result = shell()->execWithResult("/lib/ld-musl-{$arch}.so.1 2>&1", false);
}
// Match Version * line
// match ldd version: "Version 1.2.3" match 1.2.3
$pattern = '/Version\s+(\d+\.\d+\.\d+)/';
if (preg_match($pattern, $result[1][1] ?? '', $matches)) {
self::$libc_version = $matches[1];
return self::$libc_version;
}
}
return null;
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util\System;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Exception\WrongUsageException;
class MacOSUtil extends UnixUtil
{
/**
* Get Logic CPU Count for macOS.
*/
public static function getCpuCount(): int
{
$cpu = exec('sysctl -n hw.ncpu', $output, $ret);
if ($ret !== 0) {
throw new EnvironmentException(
'Failed to get cpu count from macOS sysctl',
'Please ensure you are running this command on a macOS system and have the sysctl command available.'
);
}
return (int) $cpu;
}
/**
* Get Target Arch CFlags.
*
* @param string $arch Arch Name
* @return string return Arch CFlags string
*/
public static function getArchCFlags(string $arch): string
{
return match ($arch) {
'x86_64' => '--target=x86_64-apple-darwin',
'arm64','aarch64' => '--target=arm64-apple-darwin',
default => throw new WrongUsageException('unsupported arch: ' . $arch),
};
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util\System;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
abstract class UnixUtil
{
/**
* Export static library dynamic symbols to a .dynsym file.
* It will export to "/path/to/libxxx.a.dynsym".
*
* @param string $lib_file Static library file path (e.g. /path/to/libxxx.a)
*/
public static function exportDynamicSymbols(string $lib_file): void
{
// check
if (!is_file($lib_file)) {
throw new WrongUsageException("The lib archive file {$lib_file} does not exist, please build it first.");
}
// shell out
$cmd = 'nm -g --defined-only -P ' . escapeshellarg($lib_file);
$result = shell()->execWithResult($cmd);
if ($result[0] !== 0) {
throw new ExecutionException($cmd, 'Failed to get defined symbols from ' . $lib_file);
}
// parse shell output and filter
$defined = [];
foreach ($result[1] as $line) {
$line = trim($line);
if ($line === '' || str_ends_with($line, '.o:') || str_ends_with($line, '.o]:')) {
continue;
}
$name = strtok($line, " \t");
if (!$name) {
continue;
}
$name = preg_replace('/@.*$/', '', $name);
if ($name !== '' && $name !== false) {
$defined[] = $name;
}
}
$defined = array_unique($defined);
sort($defined);
// export
if (SystemTarget::getTargetOS() === 'Linux') {
file_put_contents("{$lib_file}.dynsym", "{\n" . implode("\n", array_map(fn ($x) => " {$x};", $defined)) . "};\n");
} else {
file_put_contents("{$lib_file}.dynsym", implode("\n", $defined) . "\n");
}
}
/**
* Get linker flag to export dynamic symbols from a static library.
*
* @param string $lib_file Static library file path (e.g. /path/to/libxxx.a)
* @return null|string Linker flag to export dynamic symbols, null if no .dynsym file found
*/
public static function getDynamicExportedSymbols(string $lib_file): ?string
{
$symbol_file = "{$lib_file}.dynsym";
if (!is_file($symbol_file)) {
self::exportDynamicSymbols($lib_file);
}
if (!is_file($symbol_file)) {
throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available.");
}
// https://github.com/ziglang/zig/issues/24662
if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) {
return '-Wl,--export-dynamic';
}
// macOS
if (SystemTarget::getTargetOS() !== 'Linux') {
return "-Wl,-exported_symbols_list,{$symbol_file}";
}
return "-Wl,--dynamic-list={$symbol_file}";
}
/**
* Find a command in given paths or system PATH.
* If $name is an absolute path, check if it exists.
*
* @param string $name Command name or absolute path
* @param array $paths Paths to search, if empty, use system PATH
* @return null|string Absolute path of the command if found, null otherwise
*/
public static function findCommand(string $name, array $paths = []): ?string
{
if (!$paths) {
$paths = explode(PATH_SEPARATOR, getenv('PATH'));
}
if (str_starts_with($name, '/')) {
return file_exists($name) ? $name : null;
}
foreach ($paths as $path) {
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
return $path . DIRECTORY_SEPARATOR . $name;
}
}
return null;
}
/**
* Make environment variable string for shell command.
*
* @param array $vars Variables, like: ["CFLAGS" => "-Ixxx"]
* @return string like: CFLAGS="-Ixxx"
*/
public static function makeEnvVarString(array $vars): string
{
$str = '';
foreach ($vars as $key => $value) {
if ($str !== '') {
$str .= ' ';
}
$str .= $key . '=' . escapeshellarg($value);
}
return $str;
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util\System;
use StaticPHP\Util\FileSystem;
class WindowsUtil
{
/**
* Find windows program using executable name.
*
* @param string $name command name (xxx.exe)
* @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
{
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)) {
return $path . DIRECTORY_SEPARATOR . $name;
}
}
return null;
}
/**
* Find Visual Studio installation.
*
* @return array<string, 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',
];
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,
];
}
}
return false;
}
/**
* Get CPU count for concurrency.
*/
public static function getCpuCount(): int
{
$result = f_exec('echo %NUMBER_OF_PROCESSORS%', $out, $code);
if ($code !== 0 || !$result) {
return 1;
}
return intval($result);
}
/**
* Create CMake toolchain file.
*
* @param null|string $cflags CFLAGS for cmake, default use '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP='
* @param null|string $ldflags LDFLAGS for cmake, default use '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'
*/
public static function makeCmakeToolchainFile(?string $cflags = null, ?string $ldflags = null): string
{
if ($cflags === null) {
$cflags = '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP=';
}
if ($ldflags === null) {
$ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt';
}
$buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH);
$toolchain = <<<CMAKE
set(CMAKE_SYSTEM_NAME Windows)
SET(CMAKE_SYSTEM_PROCESSOR x64)
SET(CMAKE_C_FLAGS "{$cflags}")
SET(CMAKE_C_FLAGS_DEBUG "{$cflags}")
SET(CMAKE_CXX_FLAGS "{$cflags}")
SET(CMAKE_CXX_FLAGS_DEBUG "{$cflags}")
SET(CMAKE_EXE_LINKER_FLAGS "{$ldflags}")
SET(CMAKE_FIND_ROOT_PATH "{$buildroot}")
SET(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded)
CMAKE;
if (!is_dir(SOURCE_PATH)) {
FileSystem::createDir(SOURCE_PATH);
}
FileSystem::writeFile(SOURCE_PATH . '\toolchain.cmake', $toolchain);
return realpath(SOURCE_PATH . '\toolchain.cmake');
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Util;
use StaticPHP\Artifact\Artifact;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\InterruptException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\TargetPackage;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* Compatibility layer for static-php-cli 2.x version.
* Internal use only.
*/
class V2CompatLayer
{
/**
* Mapping legacy build options to new build options.
*
* - `with-suggested-libs` and `with-suggested-exts` are mapped to `with-suggests`.
* - `with-libs` is mapped to `with-packages`.
*/
public static function convertOptions(InputInterface $input): void
{
if ($input->getOption('with-suggested-libs')) {
$input->setOption('with-suggests', true);
}
if ($input->getOption('with-suggested-exts')) {
$input->setOption('with-suggests', true);
}
if ($input->getOption('with-libs')) {
$existing = $input->getOption('with-packages');
$additional = $input->getOption('with-libs');
if (!empty($existing)) {
$input->setOption('with-packages', $existing . ',' . $additional);
} else {
$input->setOption('with-packages', $additional);
}
}
}
public static function getLegacyBuildOptions(): array
{
return [
new InputOption('with-suggested-libs', null, InputOption::VALUE_NONE, 'Resolve and install suggested libraries as well (legacy)'),
new InputOption('with-suggested-exts', null, InputOption::VALUE_NONE, 'Resolve and install suggested extensions as well (legacy)'),
new InputOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries to install/build, comma separated (legacy)', ''),
];
}
/**
* Add legacy build options for the 'php' target package.
*/
public static function addLegacyBuildOptionsForPhp(TargetPackage $package): void
{
if ($package->getName() === 'php') {
$package->addBuildOption('build-micro', null, null, 'Build micro SAPI');
$package->addBuildOption('build-cli', null, null, 'Build cli SAPI');
$package->addBuildOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)');
$package->addBuildOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)');
$package->addBuildOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)');
$package->addBuildOption('build-cgi', null, null, 'Build cgi SAPI');
$package->addBuildOption('build-all', null, null, 'Build all SAPI');
}
}
public static function beforeExtractHook(Artifact $artifact): void
{
self::emitPatchPoint(match ($artifact->getName()) {
'php-src' => 'before-php-extract',
'micro' => 'before-micro-extract',
default => '',
});
}
public static function afterExtractHook(Artifact $artifact): void
{
self::emitPatchPoint(match ($artifact->getName()) {
'php-src' => 'after-php-extract',
'micro' => 'after-micro-extract',
default => '',
});
}
public static function beforeExtsExtractHook(): void
{
self::emitPatchPoint('before-exts-extract');
}
public static function afterExtsExtractHook(): void
{
self::emitPatchPoint('after-exts-extract');
}
public static function beforeLibExtractHook(string $pkg): void
{
self::emitPatchPoint("before-library[{$pkg}]-extract");
}
public static function afterLibExtractHook(string $pkg): void
{
self::emitPatchPoint("after-library[{$pkg}]-extract");
}
public static function emitPatchPoint(string $point_name): void
{
if ($point_name === '') {
return;
}
if (!ApplicationContext::has(PackageInstaller::class)) {
return;
}
$builder = ApplicationContext::get(PackageBuilder::class);
$patch_points = $builder->getOption('with-added-patch', []);
ApplicationContext::set('patch_point', $point_name);
foreach ($patch_points as $patch_point) {
if (!file_exists($patch_point)) {
throw new WrongUsageException("Additional patch script {$patch_point} does not exist!");
}
logger()->debug("Applying additional patch script {$patch_point}");
try {
require $patch_point;
} catch (InterruptException $e) {
if ($e->getCode() === 0) {
logger()->notice('Patch script ' . $patch_point . ' interrupted' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.'));
} else {
logger()->error('Patch script ' . $patch_point . ' interrupted with error code [' . $e->getCode() . ']' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.'));
}
}
}
ApplicationContext::set('patch_point', '');
}
}