From 528ad1199aab9528836ffad643e98b3777cec251 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 22 Apr 2023 21:23:12 +0800 Subject: [PATCH] add doctor command --- src/SPC/builder/macos/MacOSBuilder.php | 13 --- src/SPC/command/DoctorCommand.php | 12 +- src/SPC/doctor/AsCheckItem.php | 18 +++ src/SPC/doctor/AsFixItem.php | 13 +++ src/SPC/doctor/CheckListHandler.php | 128 +++++++++++++++++++++ src/SPC/doctor/CheckResult.php | 42 +++++++ src/SPC/doctor/item/MacOSToolCheckList.php | 84 ++++++++++++++ src/SPC/doctor/item/OSCheckList.php | 23 ++++ src/SPC/util/UnixShell.php | 4 +- src/globals/defines.php | 20 ++-- src/globals/functions.php | 4 +- 11 files changed, 334 insertions(+), 27 deletions(-) create mode 100644 src/SPC/doctor/AsCheckItem.php create mode 100644 src/SPC/doctor/AsFixItem.php create mode 100644 src/SPC/doctor/CheckListHandler.php create mode 100644 src/SPC/doctor/CheckResult.php create mode 100644 src/SPC/doctor/item/MacOSToolCheckList.php create mode 100644 src/SPC/doctor/item/OSCheckList.php diff --git a/src/SPC/builder/macos/MacOSBuilder.php b/src/SPC/builder/macos/MacOSBuilder.php index c6bc482f..9a6c1cd2 100644 --- a/src/SPC/builder/macos/MacOSBuilder.php +++ b/src/SPC/builder/macos/MacOSBuilder.php @@ -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); diff --git a/src/SPC/command/DoctorCommand.php b/src/SPC/command/DoctorCommand.php index 6a4ea752..f8a5ee64 100644 --- a/src/SPC/command/DoctorCommand.php +++ b/src/SPC/command/DoctorCommand.php @@ -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('Doctor check complete !'); + } catch (\Throwable $e) { + $this->output->writeln('' . $e->getMessage() . ''); + return 1; + } + return 0; } } diff --git a/src/SPC/doctor/AsCheckItem.php b/src/SPC/doctor/AsCheckItem.php new file mode 100644 index 00000000..6c20e38c --- /dev/null +++ b/src/SPC/doctor/AsCheckItem.php @@ -0,0 +1,18 @@ +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 ' . $item->item_name . ' ... '); + $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('' . $result->getMessage() . ''); + 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('Fix done'); + } else { + $this->output->writeln('Fix failed'); + throw new RuntimeException('Some check item are not fixed'); + } + } +} diff --git a/src/SPC/doctor/CheckResult.php b/src/SPC/doctor/CheckResult.php new file mode 100644 index 00000000..d7628ee8 --- /dev/null +++ b/src/SPC/doctor/CheckResult.php @@ -0,0 +1,42 @@ +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); + } +} diff --git a/src/SPC/doctor/item/MacOSToolCheckList.php b/src/SPC/doctor/item/MacOSToolCheckList.php new file mode 100644 index 00000000..cf564863 --- /dev/null +++ b/src/SPC/doctor/item/MacOSToolCheckList.php @@ -0,0 +1,84 @@ +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; + } +} diff --git a/src/SPC/doctor/item/OSCheckList.php b/src/SPC/doctor/item/OSCheckList.php new file mode 100644 index 00000000..1bab92cd --- /dev/null +++ b/src/SPC/doctor/item/OSCheckList.php @@ -0,0 +1,23 @@ +debug = defined('DEBUG_MODE'); + $this->debug = $debug ?? defined('DEBUG_MODE'); } public function cd(string $dir): UnixShell diff --git a/src/globals/defines.php b/src/globals/defines.php index f241d608..c692e3b2 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -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'; diff --git a/src/globals/functions.php b/src/globals/functions.php index ddf22369..9ce84214 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -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); }