add doctor command

This commit is contained in:
crazywhalecc 2023-04-22 21:23:12 +08:00
parent 4c0d35c723
commit 528ad1199a
No known key found for this signature in database
GPG Key ID: 1F4BDD59391F2680
11 changed files with 334 additions and 27 deletions

View File

@ -21,9 +21,6 @@ class MacOSBuilder extends BuilderBase
/** 编译的 Unix 工具集 */
use UnixBuilderTrait;
/** @var string[] MacOS 环境下编译依赖的命令 */
public const REQUIRED_COMMANDS = ['make', 'bison', 'flex', 'pkg-config', 'git', 'autoconf', 'automake', 'tar', 'unzip', 'xz', 'gzip', 'bzip2', 'cmake'];
/** @var bool 标记是否 patch 了 phar */
private bool $phar_patched = false;
@ -56,16 +53,6 @@ class MacOSBuilder extends BuilderBase
"CC='{$this->cc}' " .
"CXX='{$this->cxx}' " .
"CFLAGS='{$this->arch_c_flags} -Wimplicit-function-declaration'";
// 保存丢失的命令
$missing = [];
foreach (self::REQUIRED_COMMANDS as $cmd) {
if (SystemUtil::findCommand($cmd) === null) {
$missing[] = $cmd;
}
}
if (!empty($missing)) {
throw new RuntimeException('missing system commands: ' . implode(', ', $missing));
}
// 创立 pkg-config 和放头文件的目录
f_mkdir(BUILD_LIB_PATH . '/pkgconfig', recursive: true);

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace SPC\command;
use SPC\doctor\CheckListHandler;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand('doctor', 'Diagnose whether the current environment can compile normally')]
@ -11,7 +12,14 @@ class DoctorCommand extends BaseCommand
{
public function handle(): int
{
logger()->error('Not implemented');
return 1;
try {
$checker = new CheckListHandler($this->input, $this->output);
$checker->runCheck(FIX_POLICY_PROMPT);
$this->output->writeln('<info>Doctor check complete !</info>');
} catch (\Throwable $e) {
$this->output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
return 0;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
#[\Attribute(\Attribute::TARGET_METHOD)]
class AsCheckItem
{
public mixed $callback = null;
public function __construct(
public string $item_name,
public ?string $limit_os = null,
public int $level = 100
) {
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
#[\Attribute(\Attribute::TARGET_METHOD)]
class AsFixItem
{
public function __construct(public string $name)
{
}
}

View File

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class CheckListHandler
{
/** @var AsCheckItem[] */
private array $check_list = [];
private array $fix_map = [];
/**
* @throws \ReflectionException
* @throws FileSystemException
* @throws RuntimeException
*/
public function __construct(private InputInterface $input, private OutputInterface $output)
{
$this->loadCheckList();
}
/**
* @throws RuntimeException
*/
public function runCheck(int $fix_policy = FIX_POLICY_DIE): void
{
foreach ($this->check_list as $item) {
if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) {
continue;
}
$this->output->write('Checking <comment>' . $item->item_name . '</comment> ... ');
$result = call_user_func($item->callback);
if ($result === null) {
$this->output->writeln('skipped');
} elseif ($result instanceof CheckResult) {
if ($result->isOK()) {
$this->output->writeln('ok');
continue;
}
// Failed
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
switch ($fix_policy) {
case FIX_POLICY_DIE:
throw new RuntimeException('Some check items can not be fixed !');
case FIX_POLICY_PROMPT:
if ($result->getFixItem() !== '') {
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Do you want to fix it? [Y/n] ', true);
if ($helper->ask($this->input, $this->output, $question)) {
$this->emitFix($result);
} else {
throw new RuntimeException('You cancelled fix');
}
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
case FIX_POLICY_AUTOFIX:
if ($result->getFixItem() !== '') {
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
$this->emitFix($result);
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
}
}
}
}
/**
* Load Doctor check item list
*
* @throws \ReflectionException
* @throws RuntimeException
* @throws FileSystemException
*/
private function loadCheckList(): void
{
foreach (FileSystem::getClassesPsr4(__DIR__ . '/item', 'SPC\\doctor\\item') as $class) {
$ref = new \ReflectionClass($class);
foreach ($ref->getMethods() as $method) {
$attr = $method->getAttributes(AsCheckItem::class);
if (isset($attr[0])) {
/** @var AsCheckItem $instance */
$instance = $attr[0]->newInstance();
$instance->callback = [new $class(), $method->getName()];
$this->check_list[] = $instance;
continue;
}
$attr = $method->getAttributes(AsFixItem::class);
if (isset($attr[0])) {
/** @var AsFixItem $instance */
$instance = $attr[0]->newInstance();
// Redundant fix item
if (isset($this->fix_map[$instance->name])) {
throw new RuntimeException('Redundant doctor fix item: ' . $instance->name);
}
$this->fix_map[$instance->name] = [new $class(), $method->getName()];
}
}
}
// sort check list by level
usort($this->check_list, fn ($a, $b) => $a->level > $b->level ? -1 : ($a->level == $b->level ? 0 : 1));
}
private function emitFix(CheckResult $result)
{
$fix = $this->fix_map[$result->getFixItem()];
$fix_result = call_user_func($fix, ...$result->getFixParams());
if ($fix_result) {
$this->output->writeln('<info>Fix done</info>');
} else {
$this->output->writeln('<error>Fix failed</error>');
throw new RuntimeException('Some check item are not fixed');
}
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace SPC\doctor;
class CheckResult
{
public function __construct(private string $message = '', private string $fix_item = '', private array $fix_params = [])
{
}
public static function fail(string $message, string $fix_item = '', array $fix_params = []): CheckResult
{
return new static($message, $fix_item, $fix_params);
}
public static function ok(): CheckResult
{
return new static();
}
public function getMessage(): string
{
return $this->message;
}
public function getFixItem(): string
{
return $this->fix_item;
}
public function getFixParams(): array
{
return $this->fix_params;
}
public function isOK(): bool
{
return empty($this->message);
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace SPC\doctor\item;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\doctor\AsCheckItem;
use SPC\doctor\AsFixItem;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
class MacOSToolCheckList
{
use UnixSystemUtilTrait;
/** @var string[] MacOS 环境下编译依赖的命令 */
public const REQUIRED_COMMANDS = [
'curl',
'make',
'bison',
'flex',
'pkg-config',
'git',
'autoconf',
'automake',
'tar',
'unzip',
'xz',
'gzip',
'bzip2',
'gotop',
'cmake',
];
#[AsCheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)]
public function checkBrew(): ?CheckResult
{
// 检查 homebrew 是否已经安装
if ($this->findCommand('brew') === null) {
return CheckResult::fail('Homebrew is not installed', 'brew');
}
return CheckResult::ok();
}
#[AsCheckItem('if necessary tools are installed', limit_os: 'Darwin')]
public function checkCliTools(): ?CheckResult
{
$missing = [];
foreach (self::REQUIRED_COMMANDS as $cmd) {
if ($this->findCommand($cmd) === null) {
$missing[] = $cmd;
}
}
if (!empty($missing)) {
return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]);
}
return CheckResult::ok();
}
#[AsFixItem('brew')]
public function fixBrew(): bool
{
try {
shell(true)->exec('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
} catch (RuntimeException) {
return false;
}
return true;
}
#[AsFixItem('build-tools')]
public function fixBuildTools(array $missing): bool
{
foreach ($missing as $cmd) {
try {
shell(true)->exec('brew install ' . escapeshellarg($cmd));
} catch (RuntimeException) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace SPC\doctor\item;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\doctor\AsCheckItem;
use SPC\doctor\CheckResult;
class OSCheckList
{
use UnixSystemUtilTrait;
#[AsCheckItem('if current os are supported', level: 999)]
public function checkOS(): ?CheckResult
{
if (!in_array(PHP_OS_FAMILY, ['Darwin', 'Linux'])) {
return CheckResult::fail('Current OS is not supported');
}
return CheckResult::ok();
}
}

View File

@ -13,9 +13,9 @@ class UnixShell
private bool $debug;
public function __construct()
public function __construct(?bool $debug = null)
{
$this->debug = defined('DEBUG_MODE');
$this->debug = $debug ?? defined('DEBUG_MODE');
}
public function cd(string $dir): UnixShell

View File

@ -2,16 +2,14 @@
declare(strict_types=1);
// 工作目录
use ZM\Logger\ConsoleLogger;
define('WORKING_DIR', getcwd());
const ROOT_DIR = __DIR__ . '/../..';
// 程序启动时间
// CLI start time
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'));
@ -24,29 +22,35 @@ define('SEPARATED_PATH', [
BUILD_ROOT_PATH,
]);
// 危险的命令额外用 notice 级别提醒
// dangerous command
const DANGER_CMD = [
'rm',
'rmdir',
];
// 替换方案
// file replace strategy
const REPLACE_FILE_STR = 1;
const REPLACE_FILE_PREG = 2;
const REPLACE_FILE_USER = 3;
// 编译输出类型
// build sapi type
const BUILD_MICRO_NONE = 0;
const BUILD_MICRO_ONLY = 1;
const BUILD_MICRO_BOTH = 2;
// 编译状态
// library build status
const BUILD_STATUS_OK = 0;
const BUILD_STATUS_ALREADY = 1;
const BUILD_STATUS_FAILED = 2;
// 编译类型
// build target type
const BUILD_TYPE_CLI = 1;
const BUILD_TYPE_MICRO = 2;
const BUILD_TYPE_FPM = 3;
// doctor error fix policy
const FIX_POLICY_DIE = 1; // die directly
const FIX_POLICY_PROMPT = 2; // if it can be fixed, ask fix or not
const FIX_POLICY_AUTOFIX = 3; // if it can be fixed, just fix automatically
ConsoleLogger::$date_format = 'H:i:s';

View File

@ -105,7 +105,7 @@ function f_mkdir(string $directory, int $permissions = 0777, bool $recursive = f
return mkdir($directory, $permissions, $recursive);
}
function shell(): UnixShell
function shell(?bool $debug = null): UnixShell
{
return new UnixShell();
return new UnixShell($debug);
}