mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-08 01:15:37 +08:00
v3 base
This commit is contained in:
160
src/StaticPHP/Util/DependencyResolver.php
Normal file
160
src/StaticPHP/Util/DependencyResolver.php
Normal 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;
|
||||
}
|
||||
}
|
||||
495
src/StaticPHP/Util/FileSystem.php
Normal file
495
src/StaticPHP/Util/FileSystem.php
Normal 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);
|
||||
}
|
||||
}
|
||||
178
src/StaticPHP/Util/GlobalEnvManager.php
Normal file
178
src/StaticPHP/Util/GlobalEnvManager.php
Normal 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;
|
||||
}
|
||||
}
|
||||
112
src/StaticPHP/Util/InteractiveTerm.php
Normal file
112
src/StaticPHP/Util/InteractiveTerm.php
Normal 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);
|
||||
}
|
||||
}
|
||||
124
src/StaticPHP/Util/PkgConfigUtil.php
Normal file
124
src/StaticPHP/Util/PkgConfigUtil.php
Normal 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);
|
||||
}
|
||||
}
|
||||
302
src/StaticPHP/Util/SPCConfigUtil.php
Normal file
302
src/StaticPHP/Util/SPCConfigUtil.php
Normal 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);
|
||||
}
|
||||
}
|
||||
162
src/StaticPHP/Util/SourcePatcher.php
Normal file
162
src/StaticPHP/Util/SourcePatcher.php
Normal 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;
|
||||
}
|
||||
}
|
||||
148
src/StaticPHP/Util/System/LinuxUtil.php
Normal file
148
src/StaticPHP/Util/System/LinuxUtil.php
Normal 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;
|
||||
}
|
||||
}
|
||||
42
src/StaticPHP/Util/System/MacOSUtil.php
Normal file
42
src/StaticPHP/Util/System/MacOSUtil.php
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
128
src/StaticPHP/Util/System/UnixUtil.php
Normal file
128
src/StaticPHP/Util/System/UnixUtil.php
Normal 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;
|
||||
}
|
||||
}
|
||||
106
src/StaticPHP/Util/System/WindowsUtil.php
Normal file
106
src/StaticPHP/Util/System/WindowsUtil.php
Normal 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');
|
||||
}
|
||||
}
|
||||
140
src/StaticPHP/Util/V2CompatLayer.php
Normal file
140
src/StaticPHP/Util/V2CompatLayer.php
Normal 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', '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user