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);
}