mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-17 20:34:51 +08:00
initial framework commit
This commit is contained in:
parent
36ce06c3ca
commit
5d347adbcf
13
.gitignore
vendored
13
.gitignore
vendored
@ -3,3 +3,16 @@ runtime/
|
||||
docker/libraries/
|
||||
docker/extensions/
|
||||
docker/source/
|
||||
|
||||
# Composer file
|
||||
composer.lock
|
||||
/vendor/
|
||||
|
||||
# source extract directory
|
||||
/source/
|
||||
|
||||
# source download directory
|
||||
/downloads/
|
||||
|
||||
# source build root directory
|
||||
/buildroot/
|
||||
|
||||
69
.php-cs-fixer.php
Normal file
69
.php-cs-fixer.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PSR12' => true,
|
||||
'@Symfony' => true,
|
||||
'@PhpCsFixer' => true,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'list_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'concat_space' => [
|
||||
'spacing' => 'one',
|
||||
],
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => [
|
||||
'declare',
|
||||
],
|
||||
],
|
||||
'ordered_imports' => [
|
||||
'imports_order' => [
|
||||
'class',
|
||||
'function',
|
||||
'const',
|
||||
],
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'single_line_comment_style' => [
|
||||
'comment_types' => [
|
||||
],
|
||||
],
|
||||
'yoda_style' => [
|
||||
'always_move_variable' => false,
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
],
|
||||
'multiline_whitespace_before_semicolons' => [
|
||||
'strategy' => 'no_multi_line',
|
||||
],
|
||||
'constant_case' => [
|
||||
'case' => 'lower',
|
||||
],
|
||||
'class_attributes_separation' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'declare_strict_types' => true,
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => false,
|
||||
'not_operator_with_space' => false,
|
||||
'ordered_class_elements' => true,
|
||||
'php_unit_strict' => false,
|
||||
'phpdoc_separation' => false,
|
||||
'single_quote' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
'phpdoc_summary' => false,
|
||||
'php_unit_test_class_requires_covers' => false,
|
||||
'phpdoc_var_without_name' => false,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()->in(__DIR__ . '/src')
|
||||
);
|
||||
16
bin/static-php-cli
Executable file
16
bin/static-php-cli
Executable file
@ -0,0 +1,16 @@
|
||||
#!php
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// 防止 Micro 打包状态下不支持中文的显示(虽然这个项目目前好像没输出过中文?)
|
||||
if (PHP_OS_FAMILY === 'Windows' && Phar::running()) {
|
||||
exec('CHCP 65001');
|
||||
}
|
||||
|
||||
// 跑,反正一条命令跑就对了
|
||||
try {
|
||||
(new \SPC\ConsoleApplication())->run();
|
||||
} catch (Exception $e) {
|
||||
\SPC\exception\ExceptionHandler::getInstance()->handle($e);
|
||||
}
|
||||
47
captainhook.json
Normal file
47
captainhook.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"pre-push": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer analyse"
|
||||
},
|
||||
{
|
||||
"action": "composer test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pre-commit": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer cs-fix -- --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}",
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
|
||||
"args": ["php"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"post-change": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer install",
|
||||
"options": [],
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any",
|
||||
"args": [
|
||||
[
|
||||
"composer.json",
|
||||
"composer.lock"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
55
composer.json
Normal file
55
composer.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"require": {
|
||||
"php": ">= 8.0",
|
||||
"ext-tokenizer": "*",
|
||||
"ext-iconv": "*",
|
||||
"symfony/console": "^6 || ^5 || ^4",
|
||||
"zhamao/logger": "^1.0",
|
||||
"crazywhalecc/cli-helper": "^0.1.0",
|
||||
"nunomaduro/collision": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.2 != 3.7.0",
|
||||
"phpstan/phpstan": "^1.1",
|
||||
"captainhook/captainhook": "^5.10",
|
||||
"captainhook/plugin-composer": "^5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"SPC\\": "src/SPC"
|
||||
},
|
||||
"files": [
|
||||
"src/globals/defines.php",
|
||||
"src/globals/functions.php"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"hooks": {
|
||||
"post-merge": "composer install",
|
||||
"pre-commit": [
|
||||
"echo committing as $(git config user.name)",
|
||||
"composer cs-fix -- --diff"
|
||||
],
|
||||
"pre-push": [
|
||||
"composer cs-fix -- --dry-run --diff",
|
||||
"composer analyse"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/static-php-cli"
|
||||
],
|
||||
"scripts": {
|
||||
"analyse": "phpstan analyse --memory-limit 300M",
|
||||
"cs-fix": "php-cs-fixer fix",
|
||||
"test": "bin/phpunit --no-coverage"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"phpstan/extension-installer": true,
|
||||
"captainhook/plugin-composer": true
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
12
phpstan.neon
Normal file
12
phpstan.neon
Normal file
@ -0,0 +1,12 @@
|
||||
parameters:
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
level: 4
|
||||
paths:
|
||||
- ./src/
|
||||
ignoreErrors:
|
||||
- '#Constant .* not found#'
|
||||
- '#Unsafe usage of new static#'
|
||||
- '#class Fiber#'
|
||||
- '#Attribute class JetBrains\\PhpStorm\\ArrayShape does not exist#'
|
||||
dynamicConstantNames:
|
||||
- PHP_OS_FAMILY
|
||||
52
src/SPC/ConsoleApplication.php
Normal file
52
src/SPC/ConsoleApplication.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC;
|
||||
|
||||
use SPC\command\DeployCommand;
|
||||
use SPC\store\FileSystem;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Command\HelpCommand;
|
||||
use Symfony\Component\Console\Command\ListCommand;
|
||||
|
||||
/**
|
||||
* spc 应用究级入口
|
||||
*/
|
||||
class ConsoleApplication extends Application
|
||||
{
|
||||
public const VERSION = '2.0-alpha1';
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
* @throws exception\FileSystemException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('static-php-cli', self::VERSION);
|
||||
|
||||
global $argv;
|
||||
|
||||
// 生产环境不显示详细的调试错误,只使用 symfony console 自带的错误显示
|
||||
$this->setCatchExceptions(file_exists(ROOT_DIR . '/.prod') || !in_array('--debug', $argv));
|
||||
|
||||
// 通过扫描目录 src/static-php-cli/command/ 添加子命令
|
||||
$commands = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/command', 'SPC\\command');
|
||||
$this->addCommands(array_map(function ($x) { return new $x(); }, array_filter($commands, function ($y) {
|
||||
if (is_a($y, DeployCommand::class, true) && (class_exists('\\Phar') && \Phar::running() || !class_exists('\\Phar'))) {
|
||||
return false;
|
||||
}
|
||||
$reflection = new \ReflectionClass($y);
|
||||
return !$reflection->isAbstract() && !$reflection->isInterface();
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载以去除一些不必要的默认命令
|
||||
*/
|
||||
protected function getDefaultCommands(): array
|
||||
{
|
||||
return [new HelpCommand(), new ListCommand()];
|
||||
}
|
||||
}
|
||||
59
src/SPC/command/BaseCommand.php
Normal file
59
src/SPC/command/BaseCommand.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\command;
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
use SPC\ConsoleApplication;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Logger\ConsoleLogger;
|
||||
|
||||
abstract class BaseCommand extends Command
|
||||
{
|
||||
public function __construct(string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->addOption('debug', null, null, 'Enable debug mode');
|
||||
}
|
||||
|
||||
public function initialize(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
// 注册全局错误处理器
|
||||
set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) {
|
||||
$tips = [
|
||||
E_WARNING => ['PHP Warning: ', 'warning'],
|
||||
E_NOTICE => ['PHP Notice: ', 'notice'],
|
||||
E_USER_ERROR => ['PHP Error: ', 'error'],
|
||||
E_USER_WARNING => ['PHP Warning: ', 'warning'],
|
||||
E_USER_NOTICE => ['PHP Notice: ', 'notice'],
|
||||
E_STRICT => ['PHP Strict: ', 'notice'],
|
||||
E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'],
|
||||
E_DEPRECATED => ['PHP Deprecated: ', 'notice'],
|
||||
E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'],
|
||||
];
|
||||
$level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error'];
|
||||
$error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
|
||||
logger()->{$level_tip[1]}($error);
|
||||
// 如果 return false 则错误会继续递交给 PHP 标准错误处理
|
||||
return true;
|
||||
}, E_ALL | E_STRICT);
|
||||
if ($input->getOption('debug')) {
|
||||
global $ob_logger;
|
||||
$ob_logger = new ConsoleLogger(LogLevel::DEBUG);
|
||||
define('DEBUG_MODE', true);
|
||||
}
|
||||
$version = ConsoleApplication::VERSION;
|
||||
if (!isset($this->no_motd)) {
|
||||
echo " _ _ _ _
|
||||
___| |_ __ _| |_(_) ___ _ __ | |__ _ __
|
||||
/ __| __/ _` | __| |/ __|____| '_ \\| '_ \\| '_ \\
|
||||
\\__ \\ || (_| | |_| | (_|_____| |_) | | | | |_) |
|
||||
|___/\\__\\__,_|\\__|_|\\___| | .__/|_| |_| .__/ v{$version}
|
||||
|_| |_|
|
||||
";
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/SPC/exception/DownloaderException.php
Normal file
9
src/SPC/exception/DownloaderException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\exception;
|
||||
|
||||
class DownloaderException extends \Exception
|
||||
{
|
||||
}
|
||||
53
src/SPC/exception/ExceptionHandler.php
Normal file
53
src/SPC/exception/ExceptionHandler.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\exception;
|
||||
|
||||
class ExceptionHandler
|
||||
{
|
||||
protected $whoops;
|
||||
|
||||
private static $obj;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$whoops_class = 'Whoops\Run';
|
||||
$collision_class = 'NunoMaduro\Collision\Handler';
|
||||
if (class_exists($collision_class) && class_exists($whoops_class)) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->whoops = new $whoops_class();
|
||||
$this->whoops->allowQuit(false);
|
||||
$this->whoops->writeToOutput(false);
|
||||
$this->whoops->pushHandler(new $collision_class());
|
||||
$this->whoops->register();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance(): ExceptionHandler
|
||||
{
|
||||
if (self::$obj === null) {
|
||||
self::$obj = new self();
|
||||
}
|
||||
return self::$obj;
|
||||
}
|
||||
|
||||
public function getWhoops()
|
||||
{
|
||||
return $this->whoops;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常
|
||||
*/
|
||||
public function handle(\Throwable $e): void
|
||||
{
|
||||
if (is_null($this->whoops)) {
|
||||
logger()->error('Uncaught ' . get_class($e) . ': ' . $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')');
|
||||
logger()->error($e->getTraceAsString());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->whoops->handleException($e);
|
||||
}
|
||||
}
|
||||
9
src/SPC/exception/FileSystemException.php
Normal file
9
src/SPC/exception/FileSystemException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\exception;
|
||||
|
||||
class FileSystemException extends \Exception
|
||||
{
|
||||
}
|
||||
9
src/SPC/exception/InvalidArgumentException.php
Normal file
9
src/SPC/exception/InvalidArgumentException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\exception;
|
||||
|
||||
class InvalidArgumentException extends \Exception
|
||||
{
|
||||
}
|
||||
9
src/SPC/exception/RuntimeException.php
Normal file
9
src/SPC/exception/RuntimeException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\exception;
|
||||
|
||||
class RuntimeException extends \Exception
|
||||
{
|
||||
}
|
||||
126
src/SPC/store/Config.php
Normal file
126
src/SPC/store/Config.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* 一个读取 config 配置的操作类
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
public static ?array $source = null;
|
||||
|
||||
public static ?array $lib = null;
|
||||
|
||||
public static ?array $ext = null;
|
||||
|
||||
/**
|
||||
* 从配置文件读取一个资源(source)的元信息
|
||||
*
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getSource(string $name): ?array
|
||||
{
|
||||
if (self::$source === null) {
|
||||
self::$source = FileSystem::loadConfigArray('source');
|
||||
}
|
||||
return self::$source[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据不同的操作系统分别选择不同的 lib 库依赖项
|
||||
* 如果 key 为 null,那么直接返回整个 meta。
|
||||
* 如果 key 不为 null,则可以使用的 key 有 static-libs、headers、lib-depends、lib-suggests。
|
||||
* 对于 macOS 平台,支持 frameworks。
|
||||
*
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getLib(string $name, ?string $key = null, mixed $default = null)
|
||||
{
|
||||
if (self::$lib === null) {
|
||||
self::$lib = FileSystem::loadConfigArray('lib');
|
||||
}
|
||||
if (!isset(self::$lib[$name])) {
|
||||
throw new RuntimeException('lib [' . $name . '] is not supported yet for get');
|
||||
}
|
||||
$supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks'];
|
||||
if ($key !== null && in_array($key, $supported_sys_based)) {
|
||||
$m_key = match (PHP_OS_FAMILY) {
|
||||
'Windows' => ['-windows', '-win', ''],
|
||||
'Darwin' => ['-macos', '-unix', ''],
|
||||
'Linux' => ['-linux', '-unix', ''],
|
||||
default => throw new RuntimeException('OS ' . PHP_OS_FAMILY . ' is not supported'),
|
||||
};
|
||||
foreach ($m_key as $v) {
|
||||
if (isset(self::$lib[$name][$key . $v])) {
|
||||
return self::$lib[$name][$key . $v];
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
if ($key !== null) {
|
||||
return self::$lib[$name][$key] ?? $default;
|
||||
}
|
||||
return self::$lib[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getLibs(): array
|
||||
{
|
||||
if (self::$lib === null) {
|
||||
self::$lib = FileSystem::loadConfigArray('lib');
|
||||
}
|
||||
return self::$lib;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getExt(string $name, ?string $key = null, mixed $default = null)
|
||||
{
|
||||
if (self::$ext === null) {
|
||||
self::$ext = FileSystem::loadConfigArray('ext');
|
||||
}
|
||||
if (!isset(self::$ext[$name])) {
|
||||
throw new RuntimeException('ext [' . $name . '] is not supported yet for get');
|
||||
}
|
||||
$supported_sys_based = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests', 'arg-type'];
|
||||
if ($key !== null && in_array($key, $supported_sys_based)) {
|
||||
$m_key = match (PHP_OS_FAMILY) {
|
||||
'Windows' => ['-windows', '-win', ''],
|
||||
'Darwin' => ['-macos', '-unix', ''],
|
||||
'Linux' => ['-linux', '-unix', ''],
|
||||
default => throw new RuntimeException('OS ' . PHP_OS_FAMILY . ' is not supported'),
|
||||
};
|
||||
foreach ($m_key as $v) {
|
||||
if (isset(self::$ext[$name][$key . $v])) {
|
||||
return self::$ext[$name][$key . $v];
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
if ($key !== null) {
|
||||
return self::$ext[$name][$key] ?? $default;
|
||||
}
|
||||
return self::$ext[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getExts(): array
|
||||
{
|
||||
if (self::$ext === null) {
|
||||
self::$ext = FileSystem::loadConfigArray('ext');
|
||||
}
|
||||
return self::$ext;
|
||||
}
|
||||
}
|
||||
328
src/SPC/store/Downloader.php
Normal file
328
src/SPC/store/Downloader.php
Normal file
@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use SPC\exception\DownloaderException;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* 资源下载器
|
||||
*/
|
||||
class Downloader
|
||||
{
|
||||
/**
|
||||
* 获取 BitBucket 仓库的最新 Tag
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param array $source 资源的元信息,包含字段 repo
|
||||
* @return array<int, string> 返回下载 url 链接和文件名
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getLatestBitbucketTag(string $name, array $source): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from bitbucket tag");
|
||||
$data = json_decode(self::curlExec(
|
||||
url: "https://api.bitbucket.org/2.0/repositories/{$source['repo']}/refs/tags"
|
||||
), true);
|
||||
$ver = $data['values'][0]['name'];
|
||||
if (!$ver) {
|
||||
throw new DownloaderException("failed to find {$name} bitbucket source");
|
||||
}
|
||||
$url = "https://bitbucket.org/{$source['repo']}/get/{$ver}.tar.gz";
|
||||
$headers = self::curlExec(
|
||||
url: $url,
|
||||
method: 'HEAD'
|
||||
);
|
||||
preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $headers, $matches);
|
||||
if ($matches) {
|
||||
$filename = $matches['filename'];
|
||||
} else {
|
||||
$filename = "{$name}-{$data['tag_name']}.tar.gz";
|
||||
}
|
||||
|
||||
return [$url, $filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GitHub 最新的打包地址和文件名
|
||||
*
|
||||
* @param string $name 包名称
|
||||
* @param array $source 源信息
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getLatestGithubTarball(string $name, array $source, string $type = 'releases'): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from github {$type} tarball");
|
||||
$data = json_decode(self::curlExec(
|
||||
url: "https://api.github.com/repos/{$source['repo']}/{$type}",
|
||||
hooks: [[CurlHook::class, 'setupGithubToken']]
|
||||
), true);
|
||||
$url = $data[0]['tarball_url'];
|
||||
if (!$url) {
|
||||
throw new DownloaderException("failed to find {$name} source");
|
||||
}
|
||||
$headers = self::curlExec(
|
||||
url: $url,
|
||||
method: 'HEAD',
|
||||
hooks: [[CurlHook::class, 'setupGithubToken']],
|
||||
);
|
||||
preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $headers, $matches);
|
||||
if ($matches) {
|
||||
$filename = $matches['filename'];
|
||||
} else {
|
||||
$filename = "{$name}-" . ($type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz';
|
||||
}
|
||||
|
||||
return [$url, $filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GitHub 最新的 Release 下载信息
|
||||
*
|
||||
* @param string $name 资源名
|
||||
* @param array $source 资源的元信息,包含字段 repo、match
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getLatestGithubRelease(string $name, array $source): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from github releases assests");
|
||||
$data = json_decode(self::curlExec(
|
||||
url: "https://api.github.com/repos/{$source['repo']}/releases",
|
||||
hooks: [[CurlHook::class, 'setupGithubToken']],
|
||||
), true);
|
||||
$url = null;
|
||||
foreach ($data[0]['assets'] as $asset) {
|
||||
if (preg_match('|' . $source['match'] . '|', $asset['name'])) {
|
||||
$url = $asset['browser_download_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$url) {
|
||||
throw new DownloaderException("failed to find {$name} source");
|
||||
}
|
||||
$filename = basename($url);
|
||||
|
||||
return [$url, $filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表的资源链接和名称
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param array $source 资源元信息,包含 url、regex
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function getFromFileList(string $name, array $source): array
|
||||
{
|
||||
logger()->debug("finding {$name} source from file list");
|
||||
$page = self::curlExec($source['url']);
|
||||
preg_match_all($source['regex'], $page, $matches);
|
||||
if (!$matches) {
|
||||
throw new DownloaderException("Failed to get {$name} version");
|
||||
}
|
||||
$versions = [];
|
||||
foreach ($matches['version'] as $i => $version) {
|
||||
$lowerVersion = strtolower($version);
|
||||
foreach ([
|
||||
'alpha',
|
||||
'beta',
|
||||
'rc',
|
||||
'pre',
|
||||
'nightly',
|
||||
'snapshot',
|
||||
'dev',
|
||||
] as $betaVersion) {
|
||||
if (str_contains($lowerVersion, $betaVersion)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$versions[$version] = $matches['file'][$i];
|
||||
}
|
||||
uksort($versions, 'version_compare');
|
||||
|
||||
return [$source['url'] . end($versions), end($versions)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过链接下载资源到本地并解压
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param string $url 下载链接
|
||||
* @param string $filename 下载到下载目录的目标文件名称,例如 xz.tar.gz
|
||||
* @param null|string $path 如果指定了此参数,则会移动该资源目录到目标目录
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function downloadUrl(string $name, string $url, string $filename, ?string $path = null): void
|
||||
{
|
||||
if (!file_exists(DOWNLOAD_PATH . "/{$filename}")) {
|
||||
logger()->debug("downloading {$url}");
|
||||
self::curlDown(url: $url, path: DOWNLOAD_PATH . "/{$filename}");
|
||||
} else {
|
||||
logger()->notice("{$filename} already exists");
|
||||
}
|
||||
FileSystem::extractSource($name, DOWNLOAD_PATH . "/{$filename}");
|
||||
if ($path) {
|
||||
$path = FileSystem::convertPath(SOURCE_PATH . "/{$path}");
|
||||
$src_path = FileSystem::convertPath(SOURCE_PATH . "/{$name}");
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Windows':
|
||||
f_passthru('move "' . $src_path . '" "' . $path . '"');
|
||||
break;
|
||||
case 'Linux':
|
||||
case 'Darwin':
|
||||
f_passthru('mv "' . $src_path . '" "' . $path . '"');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取资源
|
||||
*
|
||||
* @param string $name 资源名称
|
||||
* @param array $source 资源参数,包含 type、path、rev、url、filename、regex、license
|
||||
* @throws DownloaderException
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function fetchSource(string $name, array $source): void
|
||||
{
|
||||
// 避免重复 fetch
|
||||
if (is_dir(FileSystem::convertPath(SOURCE_PATH . "/{$name}")) || isset($source['path']) && is_dir(FileSystem::convertPath(SOURCE_PATH . "/{$source['path']}"))) {
|
||||
logger()->notice("{$name} source already extracted");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
switch ($source['type']) {
|
||||
case 'bitbuckettag': // 从 BitBucket 的 Tag 拉取
|
||||
[$url, $filename] = self::getLatestBitbucketTag($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'ghtar': // 从 GitHub 的 TarBall 拉取
|
||||
[$url, $filename] = self::getLatestGithubTarball($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'ghtagtar': // 根据 GitHub 的 Tag 拉取相应版本的 Tar
|
||||
[$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags');
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'ghrel': // 通过 GitHub Release 来拉取
|
||||
[$url, $filename] = self::getLatestGithubRelease($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'filelist': // 通过网站提供的 filelist 使用正则提取后拉取
|
||||
[$url, $filename] = self::getFromFileList($name, $source);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'url': // 通过直链拉取
|
||||
$url = $source['url'];
|
||||
$filename = $source['filename'] ?? basename($source['url']);
|
||||
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
|
||||
break;
|
||||
case 'git': // 通过拉回 Git 仓库的形式拉取
|
||||
if ($source['path'] ?? null) {
|
||||
$path = SOURCE_PATH . "/{$source['path']}";
|
||||
} else {
|
||||
$path = SOURCE_PATH . "/{$name}";
|
||||
}
|
||||
if (file_exists($path)) {
|
||||
logger()->notice("{$name} source already exists");
|
||||
break;
|
||||
}
|
||||
logger()->debug("cloning {$name} source");
|
||||
$check = !defined('DEBUG_MODE') ? ' -q' : '';
|
||||
f_passthru(
|
||||
'git clone' . $check .
|
||||
' --config core.autocrlf=false ' .
|
||||
"--branch \"{$source['rev']}\" " . (defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '') . " --recursive \"{$source['url']}\" \"{$path}\""
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new DownloaderException('unknown source type: ' . $source['type']);
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
// 因为某些时候通过命令行下载的文件在失败后不会删除,这里检测到文件存在需要手动删一下
|
||||
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
|
||||
logger()->warning('Deleting download file: ' . $filename);
|
||||
unlink(DOWNLOAD_PATH . '/' . $filename);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 PHP x.y 的具体版本号,例如通过 8.1 来获取 8.1.10
|
||||
*
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
#[ArrayShape(['type' => 'string', 'path' => 'string', 'rev' => 'string', 'url' => 'string'])]
|
||||
public static function getLatestPHPInfo(string $major_version): array
|
||||
{
|
||||
// 查找最新的小版本号
|
||||
$info = json_decode(self::curlExec(url: "https://www.php.net/releases/index.php?json&version={$major_version}"), true);
|
||||
$version = $info['version'];
|
||||
|
||||
// 从官网直接下载
|
||||
return [
|
||||
'type' => 'url',
|
||||
'url' => "https://www.php.net/distributions/php-{$version}.tar.gz",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 curl 命令拉取元信息
|
||||
*
|
||||
* @throws DownloaderException
|
||||
*/
|
||||
public static function curlExec(string $url, string $method = 'GET', array $headers = [], array $hooks = []): string
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$hook($method, $url, $headers);
|
||||
}
|
||||
|
||||
FileSystem::findCommandPath('curl');
|
||||
|
||||
$methodArg = match ($method) {
|
||||
'GET' => '',
|
||||
'HEAD' => '-I',
|
||||
default => "-X \"{$method}\"",
|
||||
};
|
||||
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||
|
||||
$cmd = "curl -sfSL {$methodArg} {$headerArg} \"{$url}\"";
|
||||
f_exec($cmd, $output, $ret);
|
||||
if ($ret !== 0) {
|
||||
throw new DownloaderException('failed http fetch');
|
||||
}
|
||||
return implode("\n", $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 curl 命令下载文件
|
||||
*
|
||||
* @throws DownloaderException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function curlDown(string $url, string $path, string $method = 'GET', array $headers = [], array $hooks = []): void
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$hook($method, $url, $headers);
|
||||
}
|
||||
|
||||
$methodArg = match ($method) {
|
||||
'GET' => '',
|
||||
'HEAD' => '-I',
|
||||
default => "-X \"{$method}\"",
|
||||
};
|
||||
$headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||
$check = !defined('DEBUG_MODE') ? 's' : '#';
|
||||
$cmd = "curl -{$check}fSL -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\"";
|
||||
f_passthru($cmd);
|
||||
}
|
||||
}
|
||||
379
src/SPC/store/FileSystem.php
Normal file
379
src/SPC/store/FileSystem.php
Normal file
@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator;
|
||||
|
||||
class FileSystem
|
||||
{
|
||||
private static $source_cache;
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function loadConfigArray(string $config): array
|
||||
{
|
||||
$whitelist = ['ext', 'lib', 'source'];
|
||||
if (!in_array($config, $whitelist)) {
|
||||
throw new FileSystemException('Reading ' . $config . '.json is not allowed');
|
||||
}
|
||||
$tries = [
|
||||
WORKING_DIR . '/config/' . $config . '.json',
|
||||
ROOT_DIR . '/config/' . $config . '.json',
|
||||
];
|
||||
foreach ($tries as $try) {
|
||||
if (file_exists($try)) {
|
||||
$json = json_decode(self::readFile($try), true);
|
||||
if (!is_array($json)) {
|
||||
throw new FileSystemException('Reading ' . $try . ' failed');
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
throw new FileSystemException('Reading ' . $config . '.json failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件,读不出来直接抛出异常
|
||||
*
|
||||
* @param string $filename 文件路径
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): bool|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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀
|
||||
*
|
||||
* @param string $fn 文件名
|
||||
*/
|
||||
public static function extname(string $fn): string
|
||||
{
|
||||
$parts = explode('.', basename($fn));
|
||||
if (count($parts) < 2) {
|
||||
return '';
|
||||
}
|
||||
return array_pop($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找命令的真实路径,效果类似 which
|
||||
*
|
||||
* @param string $name 命令名称
|
||||
* @param array $paths 路径列表,如果为空则默认从 PATH 系统变量搜索
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
public static function copyDir(string $from, string $to): void
|
||||
{
|
||||
$iterator = new \RecursiveIteratorIterator(new RecursiveDirectoryIterator($from, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO), \RecursiveIteratorIterator::SELF_FIRST);
|
||||
foreach ($iterator as $item) {
|
||||
/**
|
||||
* @var \SplFileInfo $item
|
||||
*/
|
||||
$target = $to . substr($item->getPathname(), strlen($from));
|
||||
if ($item->isDir()) {
|
||||
logger()->info("mkdir {$target}");
|
||||
f_mkdir($target, recursive: true);
|
||||
} else {
|
||||
logger()->info("copying {$item} to {$target}");
|
||||
@f_mkdir(dirname($target), recursive: true);
|
||||
copy($item->getPathname(), $target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压缩下载的资源包到 source 目录
|
||||
*
|
||||
* @param string $name 资源名
|
||||
* @param string $filename 文件名
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function extractSource(string $name, string $filename): void
|
||||
{
|
||||
logger()->info("extracting {$name} source");
|
||||
try {
|
||||
if (PHP_OS_FAMILY !== 'Windows') {
|
||||
if (f_mkdir(directory: SOURCE_PATH . "/{$name}", recursive: true) !== true) {
|
||||
throw new FileSystemException('create ' . $name . 'source dir failed');
|
||||
}
|
||||
switch (self::extname($filename)) {
|
||||
case 'xz':
|
||||
case 'txz':
|
||||
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
// f_passthru("cat {$filename} | xz -d | tar -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'gz':
|
||||
case 'tgz':
|
||||
f_passthru("tar -xzf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'bz2':
|
||||
f_passthru("tar -xjf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'zip':
|
||||
f_passthru("unzip {$filename} -d " . SOURCE_PATH . "/{$name}");
|
||||
break;
|
||||
// case 'zstd':
|
||||
// case 'zst':
|
||||
// passthru('cat ' . $filename . ' | zstd -d | tar -x -C ".SOURCE_PATH . "/' . $name . ' --strip-components 1', $ret);
|
||||
// break;
|
||||
case 'tar':
|
||||
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
default:
|
||||
throw new FileSystemException('unknown archive format: ' . $filename);
|
||||
}
|
||||
} else {
|
||||
// find 7z
|
||||
$_7zExe = self::findCommandPath('7z', [
|
||||
'C:\Program Files\7-Zip-Zstandard',
|
||||
'C:\Program Files (x86)\7-Zip-Zstandard',
|
||||
'C:\Program Files\7-Zip',
|
||||
'C:\Program Files (x86)\7-Zip',
|
||||
]);
|
||||
if (!$_7zExe) {
|
||||
throw new FileSystemException('windows needs 7z to unpack');
|
||||
}
|
||||
f_mkdir(SOURCE_PATH . "/{$name}", recursive: true);
|
||||
switch (self::extname($filename)) {
|
||||
case 'zstd':
|
||||
case 'zst':
|
||||
if (!str_contains($_7zExe, 'Zstandard')) {
|
||||
throw new FileSystemException("zstd is not supported: {$filename}");
|
||||
}
|
||||
// no break
|
||||
case 'xz':
|
||||
case 'txz':
|
||||
case 'gz':
|
||||
case 'tgz':
|
||||
case 'bz2':
|
||||
f_passthru("\"{$_7zExe}\" x -so {$filename} | tar -f - -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'tar':
|
||||
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
|
||||
break;
|
||||
case 'zip':
|
||||
f_passthru("\"{$_7zExe}\" x {$filename} -o" . SOURCE_PATH . "/{$name}");
|
||||
break;
|
||||
default:
|
||||
throw new FileSystemException("unknown archive format: {$filename}");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
f_passthru('rmdir /s /q ' . SOURCE_PATH . "/{$name}");
|
||||
} else {
|
||||
f_passthru('rm -r ' . SOURCE_PATH . "/{$name}");
|
||||
}
|
||||
throw new FileSystemException('Cannot extract source ' . $name, $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据系统环境的不同,自动转换路径的分隔符
|
||||
*
|
||||
* @param string $path 路径
|
||||
*/
|
||||
public static function convertPath(string $path): string
|
||||
{
|
||||
if (str_starts_with($path, 'phar://')) {
|
||||
return $path;
|
||||
}
|
||||
return str_replace('/', DIRECTORY_SEPARATOR, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归或非递归扫描目录,可返回相对目录的文件列表或绝对目录的文件列表
|
||||
*
|
||||
* @param string $dir 目录
|
||||
* @param bool $recursive 是否递归扫描子目录
|
||||
* @param bool|string $relative 是否返回相对目录,如果为true则返回相对目录,如果为false则返回绝对目录
|
||||
* @param bool $include_dir 非递归模式下,是否包含目录
|
||||
* @return array|false
|
||||
* @since 2.5
|
||||
*/
|
||||
public static function scanDirFiles(string $dir, bool $recursive = true, bool|string $relative = false, bool $include_dir = false): bool|array
|
||||
{
|
||||
$dir = self::convertPath($dir);
|
||||
// 不是目录不扫,直接 false 处理
|
||||
if (!is_dir($dir)) {
|
||||
logger()->warning('Scan dir failed, no such 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;
|
||||
}
|
||||
$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) {
|
||||
# 如果是 目录 且 递推 , 则递推添加下级文件
|
||||
$list = array_merge($list, self::scanDirFiles($sub_file, $recursive, $relative));
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|mixed $rule
|
||||
* @param mixed $return_path_value
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getClassesPsr4(string $dir, string $base_namespace, $rule = null, $return_path_value = false): array
|
||||
{
|
||||
$classes = [];
|
||||
// 扫描目录,使用递归模式,相对路径模式,因为下面此路径要用作转换成namespace
|
||||
$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') {
|
||||
$path = rtrim($dir, '/') . '/' . rtrim($pathinfo['dirname'], './') . '/' . $pathinfo['basename'];
|
||||
|
||||
// 过滤不包含类的文件
|
||||
$tokens = token_get_all(self::readFile($path));
|
||||
$found = false;
|
||||
foreach ($tokens as $token) {
|
||||
if (!is_array($token)) {
|
||||
continue;
|
||||
}
|
||||
if ($token[0] === T_CLASS) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($rule === null) { // 规则未设置回调时候,使用默认的识别过滤规则
|
||||
/*if (substr(file_get_contents($dir . '/' . $v), 6, 6) == '#plain') {
|
||||
continue;
|
||||
}*/
|
||||
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'];
|
||||
if (is_string($return_path_value)) {
|
||||
$classes[$class_name] = $return_path_value . '/' . $v;
|
||||
} else {
|
||||
$classes[] = $class_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录及目录下的所有文件(危险操作)
|
||||
*
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function removeDir(string $dir, bool $throw_on_fail = false): bool
|
||||
{
|
||||
$dir = FileSystem::convertPath($dir);
|
||||
logger()->warning('Removing path recursively: "' . $dir . '"');
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Windows':
|
||||
case 'WINNT':
|
||||
case 'Cygwin':
|
||||
f_exec('rmdir /s /g "' . $dir . '"', $out, $ret);
|
||||
break;
|
||||
case 'Darwin':
|
||||
case 'Linux':
|
||||
f_exec('rm -rf ' . escapeshellarg($dir), $out, $ret);
|
||||
break;
|
||||
default:
|
||||
throw new FileSystemException('Unsupported OS type: ' . PHP_OS_FAMILY);
|
||||
}
|
||||
if ($ret !== 0 && $throw_on_fail) {
|
||||
throw new FileSystemException('Cannot remove dir "' . $dir . '"');
|
||||
}
|
||||
return $ret === 0;
|
||||
}
|
||||
}
|
||||
71
src/SPC/util/ConfigValidator.php
Normal file
71
src/SPC/util/ConfigValidator.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\util;
|
||||
|
||||
use SPC\exception\ValidationException;
|
||||
|
||||
class ConfigValidator
|
||||
{
|
||||
/**
|
||||
* 验证 source.json
|
||||
*
|
||||
* @param array $data source.json 加载后的数据
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function validateSource(array $data): void
|
||||
{
|
||||
foreach ($data as $name => $src) {
|
||||
isset($src['type']) || throw new ValidationException("source {$name} must have prop: [type]");
|
||||
is_string($src['type']) || throw new ValidationException("source {$name} type prop must be string");
|
||||
in_array($src['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url']) || throw new ValidationException("source {$name} type [{$src['type']}] is invalid");
|
||||
switch ($src['type']) {
|
||||
case 'filelist':
|
||||
isset($src['url'], $src['regex']) || throw new ValidationException("source {$name} needs [url] and [regex] props");
|
||||
is_string($src['url']) && is_string($src['regex']) || throw new ValidationException("source {$name} [url] and [regex] must be string");
|
||||
break;
|
||||
case 'git':
|
||||
isset($src['url'], $src['rev']) || throw new ValidationException("source {$name} needs [url] and [rev] props");
|
||||
is_string($src['url']) && is_string($src['rev']) || throw new ValidationException("source {$name} [url] and [rev] must be string");
|
||||
is_string($src['path'] ?? '') || throw new ValidationException("source {$name} [path] must be string");
|
||||
break;
|
||||
case 'ghtagtar':
|
||||
case 'ghtar':
|
||||
isset($src['repo']) || throw new ValidationException("source {$name} needs [repo] prop");
|
||||
is_string($src['repo']) || throw new ValidationException("source {$name} [repo] must be string");
|
||||
is_string($src['path'] ?? '') || throw new ValidationException("source {$name} [path] must be string");
|
||||
break;
|
||||
case 'ghrel':
|
||||
isset($src['repo'], $src['match']) || throw new ValidationException("source {$name} needs [repo] and [match] props");
|
||||
is_string($src['repo']) && is_string($src['match']) || throw new ValidationException("source {$name} [repo] and [match] must be string");
|
||||
break;
|
||||
case 'url':
|
||||
isset($src['url']) || throw new ValidationException("source {$name} needs [url] prop");
|
||||
is_string($src['url']) || throw new ValidationException("source {$name} [url] must be string");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function validateLibs($data, array $source_data = []): void
|
||||
{
|
||||
is_array($data) || throw new ValidationException('lib.json is broken');
|
||||
foreach ($data as $name => $lib) {
|
||||
isset($lib['source']) || throw new ValidationException("lib {$name} does not assign any source");
|
||||
is_string($lib['source']) || throw new ValidationException("lib {$name} source must be string");
|
||||
empty($source_data) || isset($source_data[$lib['source']]) || throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}");
|
||||
!isset($lib['lib-depends']) || !is_assoc_array($lib['lib-depends']) || throw new ValidationException("lib {$name} dependencies must be a list");
|
||||
!isset($lib['lib-suggests']) || !is_assoc_array($lib['lib-suggests']) || throw new ValidationException("lib {$name} suggested dependencies must be a list");
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateExts($data, array $source_data = []): void
|
||||
{
|
||||
is_array($data) || throw new ValidationException('ext.json is broken');
|
||||
}
|
||||
}
|
||||
43
src/globals/defines.php
Normal file
43
src/globals/defines.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// 工作目录
|
||||
use ZM\Logger\ConsoleLogger;
|
||||
|
||||
define('WORKING_DIR', getcwd());
|
||||
const ROOT_DIR = __DIR__ . '/../..';
|
||||
|
||||
// 程序启动时间
|
||||
define('START_TIME', microtime(true));
|
||||
|
||||
// 规定目录
|
||||
define('BUILD_ROOT_PATH', is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot'));
|
||||
define('SOURCE_PATH', is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source'));
|
||||
define('DOWNLOAD_PATH', is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads'));
|
||||
define('BUILD_LIB_PATH', is_string($a = getenv('INSTALL_LIB_PATH')) ? $a : (BUILD_ROOT_PATH . '/lib'));
|
||||
const BUILD_DEPS_PATH = BUILD_ROOT_PATH;
|
||||
define('BUILD_INCLUDE_PATH', is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include'));
|
||||
define('SEPARATED_PATH', [
|
||||
'/' . pathinfo(BUILD_LIB_PATH)['basename'], // lib
|
||||
'/' . pathinfo(BUILD_INCLUDE_PATH)['basename'], // include
|
||||
BUILD_ROOT_PATH,
|
||||
]);
|
||||
|
||||
// 危险的命令额外用 notice 级别提醒
|
||||
const DANGER_CMD = [
|
||||
'rm',
|
||||
'rmdir',
|
||||
];
|
||||
|
||||
// 替换方案
|
||||
const REPLACE_FILE_STR = 1;
|
||||
const REPLACE_FILE_PREG = 2;
|
||||
const REPLACE_FILE_USER = 3;
|
||||
|
||||
const BUILD_MICRO_NONE = 0;
|
||||
const BUILD_MICRO_ONLY = 1;
|
||||
|
||||
const BUILD_MICRO_BOTH = 2;
|
||||
|
||||
ConsoleLogger::$date_format = 'H:i:s';
|
||||
97
src/globals/functions.php
Normal file
97
src/globals/functions.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Logger\ConsoleLogger;
|
||||
|
||||
/**
|
||||
* 判断传入的数组是否为关联数组
|
||||
* @param mixed $array
|
||||
*/
|
||||
function is_assoc_array($array): bool
|
||||
{
|
||||
return is_array($array) && (!empty($array) && array_keys($array) !== range(0, count($array) - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 助手方法,返回一个 Logger 实例
|
||||
*/
|
||||
function logger(): LoggerInterface
|
||||
{
|
||||
global $ob_logger;
|
||||
if ($ob_logger === null) {
|
||||
return new ConsoleLogger();
|
||||
}
|
||||
return $ob_logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \SPC\exception\RuntimeException
|
||||
*/
|
||||
function arch2gnu(string $arch): string
|
||||
{
|
||||
$arch = strtolower($arch);
|
||||
return match ($arch) {
|
||||
'x86_64', 'x64', 'amd64' => 'x86_64',
|
||||
'arm64', 'aarch64' => 'aarch64',
|
||||
default => throw new \SPC\exception\RuntimeException('Not support arch: ' . $arch),
|
||||
// 'armv7' => 'arm',
|
||||
};
|
||||
}
|
||||
|
||||
function quote(string $str, string $quote = '"'): string
|
||||
{
|
||||
return $quote . $str . $quote;
|
||||
}
|
||||
|
||||
function osfamily2dir(): string
|
||||
{
|
||||
return match (PHP_OS_FAMILY) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
'Windows', 'WINNT', 'Cygwin' => 'windows',
|
||||
'Darwin' => 'macos',
|
||||
'Linux' => 'linux',
|
||||
default => throw new \SPC\exception\RuntimeException('Not support os: ' . PHP_OS_FAMILY),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \SPC\exception\RuntimeException
|
||||
*/
|
||||
function f_passthru(string $cmd): ?bool
|
||||
{
|
||||
$danger = false;
|
||||
foreach (DANGER_CMD as $danger_cmd) {
|
||||
if (str_starts_with($cmd, $danger_cmd . ' ')) {
|
||||
$danger = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($danger) {
|
||||
logger()->notice('Running dangerous command: ' . $cmd);
|
||||
} else {
|
||||
logger()->debug('Running command with direct output: ' . $cmd);
|
||||
}
|
||||
$ret = passthru($cmd, $code);
|
||||
if ($code !== 0) {
|
||||
throw new \SPC\exception\RuntimeException('Command run failed with code[' . $code . ']: ' . $cmd, $code);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function f_exec(string $command, &$output, &$result_code)
|
||||
{
|
||||
logger()->debug('Running command (no output) : ' . $command);
|
||||
return exec($command, $output, $result_code);
|
||||
}
|
||||
|
||||
function f_mkdir(string $directory, int $permissions = 0777, bool $recursive = false): bool
|
||||
{
|
||||
if (file_exists($directory)) {
|
||||
logger()->debug("Dir {$directory} already exists, ignored");
|
||||
return true;
|
||||
}
|
||||
logger()->debug('Making new directory ' . ($recursive ? 'recursive' : '') . ': ' . $directory);
|
||||
return mkdir($directory, $permissions, $recursive);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user