From 333b776e7796a45832a7350bf04492d255e5f5f8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 6 Aug 2025 20:45:16 +0800 Subject: [PATCH] Refactor all command class exception handling --- src/SPC/builder/BuilderBase.php | 24 +- src/SPC/command/BaseCommand.php | 46 +-- src/SPC/command/BuildLibsCommand.php | 40 +-- src/SPC/command/BuildPHPCommand.php | 274 ++++++++-------- src/SPC/command/DeleteDownloadCommand.php | 95 +++--- src/SPC/command/DownloadCommand.php | 366 +++++++++++----------- src/SPC/command/InstallPkgCommand.php | 115 +++---- src/SPC/command/dev/PackLibCommand.php | 204 ++++++------ src/SPC/exception/ExceptionHandler.php | 163 +++++++++- tests/SPC/builder/BuilderTest.php | 7 +- 10 files changed, 691 insertions(+), 643 deletions(-) diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index 2dd3362b..1aa30f28 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -4,18 +4,15 @@ declare(strict_types=1); namespace SPC\builder; -use PharIo\FileSystem\File; -use SPC\exception\ExceptionHandler; -use SPC\exception\FileSystemException; +use SPC\exception\BuildFailureException; use SPC\exception\InterruptException; -use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\FileSystem; use SPC\store\LockFile; use SPC\store\SourceManager; use SPC\store\SourcePatcher; -use SPC\util\CustomExt; +use SPC\util\AttributeMapper; abstract class BuilderBase { @@ -64,12 +61,11 @@ abstract class BuilderBase match ($status) { LIB_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] setup success, took ' . round(microtime(true) - $starttime, 2) . ' s'), LIB_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'), - LIB_STATUS_BUILD_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'), LIB_STATUS_INSTALL_FAILED => logger()->error('lib [' . $lib::NAME . '] install failed'), default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'), }; if (in_array($status, [LIB_STATUS_BUILD_FAILED, LIB_STATUS_INSTALL_FAILED])) { - throw new RuntimeException('Library [' . $lib::NAME . '] setup failed.'); + throw new BuildFailureException('Library [' . $lib::NAME . '] setup failed.'); } } } @@ -254,9 +250,8 @@ abstract class BuilderBase } $ext->buildShared(); } - } catch (RuntimeException $e) { + } finally { FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line); - throw $e; } FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line); FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', '# test "[$]$1" = "no" && $1=yes', 'test "[$]$1" = "no" && $1=yes'); @@ -311,7 +306,7 @@ abstract class BuilderBase return intval($match[1]); } - throw new RuntimeException('PHP version file format is malformed, please remove it and download again'); + throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); } public function getPHPVersion(bool $exception_on_failure = true): string @@ -329,7 +324,7 @@ abstract class BuilderBase if (!$exception_on_failure) { return 'unknown'; } - throw new RuntimeException('PHP version file format is malformed, please remove it and download again'); + throw new WrongUsageException('PHP version file format is malformed, please remove it and download again'); } /** @@ -476,7 +471,7 @@ abstract class BuilderBase foreach ($patches as $patch) { try { if (!file_exists($patch)) { - throw new RuntimeException("Additional patch script file {$patch} not found!"); + throw new WrongUsageException("Additional patch script file {$patch} not found!"); } logger()->debug('Running additional patch script: ' . $patch); require $patch; @@ -489,11 +484,6 @@ abstract class BuilderBase exit($e->getCode()); } catch (\Throwable $e) { logger()->critical('Patch script ' . $patch . ' failed to run.'); - if ($this->getOption('debug')) { - ExceptionHandler::getInstance()->handle($e); - } else { - logger()->critical('Please check with --debug option to see more details.'); - } throw $e; } } diff --git a/src/SPC/command/BaseCommand.php b/src/SPC/command/BaseCommand.php index 09927ad9..745dbe28 100644 --- a/src/SPC/command/BaseCommand.php +++ b/src/SPC/command/BaseCommand.php @@ -9,8 +9,8 @@ use Laravel\Prompts\Prompt; use Psr\Log\LogLevel; use SPC\ConsoleApplication; use SPC\exception\ExceptionHandler; -use SPC\exception\ValidationException; -use SPC\exception\WrongUsageException; +use SPC\exception\SPCException; +use SPC\util\AttributeMapper; use SPC\util\GlobalEnvManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; @@ -32,6 +32,7 @@ abstract class BaseCommand extends Command parent::__construct($name); $this->addOption('debug', null, null, 'Enable debug mode'); $this->addOption('no-motd', null, null, 'Disable motd'); + $this->addOption('preserve-log', null, null, 'Preserve log files, do not delete them on initialized'); } public function initialize(InputInterface $input, OutputInterface $output): void @@ -93,30 +94,20 @@ abstract class BaseCommand extends Command GlobalEnvManager::init(); f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes'); } - if ($this->shouldExecute()) { - try { - // show raw argv list for logger()->debug - logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); - return $this->handle(); - } catch (ValidationException|WrongUsageException $e) { - $msg = explode("\n", $e->getMessage()); - foreach ($msg as $v) { - logger()->error($v); - } - return static::FAILURE; - } catch (\Throwable $e) { - if ($this->getOption('debug')) { - ExceptionHandler::getInstance()->handle($e); - } else { - $msg = explode("\n", $e->getMessage()); - foreach ($msg as $v) { - logger()->error($v); - } - } - return static::FAILURE; - } + + try { + // show raw argv list for logger()->debug + logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); + return $this->handle(); + } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { + // Handle SPCException and log it + ExceptionHandler::handleSPCException($e); + return static::FAILURE; + } catch (\Throwable $e) { + // Handle any other exceptions + ExceptionHandler::handleDefaultException($e); + return static::FAILURE; } - return static::SUCCESS; } protected function getOption(string $name): mixed @@ -129,11 +120,6 @@ abstract class BaseCommand extends Command return $this->input->getArgument($name); } - protected function shouldExecute(): bool - { - return true; - } - protected function logWithResult(bool $result, string $success_msg, string $fail_msg): int { if ($result) { diff --git a/src/SPC/command/BuildLibsCommand.php b/src/SPC/command/BuildLibsCommand.php index d9071267..de1f1bba 100644 --- a/src/SPC/command/BuildLibsCommand.php +++ b/src/SPC/command/BuildLibsCommand.php @@ -50,32 +50,22 @@ class BuildLibsCommand extends BuildCommand } } - try { - // 构建对象 - $builder = BuilderProvider::makeBuilderByInput($this->input); - // 只编译 library 的情况下,标记 - $builder->setLibsOnly(); - // 编译和检查库完整 - $libraries = DependencyUtil::getLibs($libraries); - $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package'])); + // 构建对象 + $builder = BuilderProvider::makeBuilderByInput($this->input); + // 只编译 library 的情况下,标记 + $builder->setLibsOnly(); + // 编译和检查库完整 + $libraries = DependencyUtil::getLibs($libraries); + $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package'])); - logger()->info('Building libraries: ' . implode(',', $display_libs)); - sleep(2); - $builder->proveLibs($libraries); - $builder->validateLibsAndExts(); - $builder->setupLibs(); + logger()->info('Building libraries: ' . implode(',', $display_libs)); + sleep(2); + $builder->proveLibs($libraries); + $builder->validateLibsAndExts(); + $builder->setupLibs(); - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Build libs complete, used ' . $time . ' s !'); - return static::SUCCESS; - } catch (\Throwable $e) { - if ($this->getOption('debug')) { - ExceptionHandler::getInstance()->handle($e); - } else { - logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage()); - logger()->critical('Please check with --debug option to see more details.'); - } - return static::FAILURE; - } + $time = round(microtime(true) - START_TIME, 3); + logger()->info('Build libs complete, used ' . $time . ' s !'); + return static::SUCCESS; } } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 229b4e89..c987f174 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -5,8 +5,7 @@ declare(strict_types=1); namespace SPC\command; use SPC\builder\BuilderProvider; -use SPC\exception\ExceptionHandler; -use SPC\exception\WrongUsageException; +use SPC\exception\SPCException; use SPC\store\Config; use SPC\store\FileSystem; use SPC\store\SourcePatcher; @@ -119,167 +118,154 @@ class BuildPHPCommand extends BuildCommand logger()->warning('Some cases micro.sfx cannot be packed via UPX due to dynamic size bug, be aware!'); } } - try { - // create builder - $builder = BuilderProvider::makeBuilderByInput($this->input); - $include_suggest_ext = $this->getOption('with-suggested-exts'); - $include_suggest_lib = $this->getOption('with-suggested-libs'); - [$extensions, $libraries, $not_included] = DependencyUtil::getExtsAndLibs(array_merge($static_extensions, $shared_extensions), $libraries, $include_suggest_ext, $include_suggest_lib); - $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package'])); + // create builder + $builder = BuilderProvider::makeBuilderByInput($this->input); + $include_suggest_ext = $this->getOption('with-suggested-exts'); + $include_suggest_lib = $this->getOption('with-suggested-libs'); + [$extensions, $libraries, $not_included] = DependencyUtil::getExtsAndLibs(array_merge($static_extensions, $shared_extensions), $libraries, $include_suggest_ext, $include_suggest_lib); + $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package'])); - // separate static and shared extensions from $extensions - // filter rule: including shared extensions if they are in $static_extensions or $shared_extensions - $static_extensions = array_filter($extensions, fn ($ext) => !in_array($ext, $shared_extensions) || in_array($ext, $static_extensions)); + // separate static and shared extensions from $extensions + // filter rule: including shared extensions if they are in $static_extensions or $shared_extensions + $static_extensions = array_filter($extensions, fn ($ext) => !in_array($ext, $shared_extensions) || in_array($ext, $static_extensions)); - // print info - $indent_texts = [ - 'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')', - 'Build Target' => getenv('SPC_TARGET'), - 'Build Toolchain' => getenv('SPC_TOOLCHAIN'), - 'Build SAPI' => $builder->getBuildTypeName($rule), - 'Static Extensions (' . count($static_extensions) . ')' => implode(',', $static_extensions), - 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), - 'Libraries (' . count($libraries) . ')' => implode(',', $display_libs), - 'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes', - 'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no', - ]; - if (!empty($shared_extensions) || ($rule & BUILD_TARGET_EMBED)) { - $indent_texts['Build Dev'] = 'yes'; - } - if (!empty($this->input->getOption('with-config-file-path'))) { - $indent_texts['Config File Path'] = $this->input->getOption('with-config-file-path'); - } - if (!empty($this->input->getOption('with-hardcoded-ini'))) { - $indent_texts['Hardcoded INI'] = $this->input->getOption('with-hardcoded-ini'); - } - if ($this->input->getOption('disable-opcache-jit')) { - $indent_texts['Opcache JIT'] = 'disabled'; - } - if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) { - $indent_texts['UPX Pack'] = 'enabled'; - } + // print info + $indent_texts = [ + 'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')', + 'Build Target' => getenv('SPC_TARGET'), + 'Build Toolchain' => getenv('SPC_TOOLCHAIN'), + 'Build SAPI' => $builder->getBuildTypeName($rule), + 'Static Extensions (' . count($static_extensions) . ')' => implode(',', $static_extensions), + 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), + 'Libraries (' . count($libraries) . ')' => implode(',', $display_libs), + 'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes', + 'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no', + ]; + if (!empty($shared_extensions) || ($rule & BUILD_TARGET_EMBED)) { + $indent_texts['Build Dev'] = 'yes'; + } + if (!empty($this->input->getOption('with-config-file-path'))) { + $indent_texts['Config File Path'] = $this->input->getOption('with-config-file-path'); + } + if (!empty($this->input->getOption('with-hardcoded-ini'))) { + $indent_texts['Hardcoded INI'] = $this->input->getOption('with-hardcoded-ini'); + } + if ($this->input->getOption('disable-opcache-jit')) { + $indent_texts['Opcache JIT'] = 'disabled'; + } + if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) { + $indent_texts['UPX Pack'] = 'enabled'; + } - $ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion(false); - $indent_texts['PHP Version'] = $ver; + $ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion(false); + $indent_texts['PHP Version'] = $ver; - if (!empty($not_included)) { - $indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included); - } - $this->printFormatInfo($this->getDefinedEnvs(), true); - $this->printFormatInfo($indent_texts); + if (!empty($not_included)) { + $indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included); + } + $this->printFormatInfo($this->getDefinedEnvs(), true); + $this->printFormatInfo($indent_texts); + // bind extra info to SPCException + SPCException::bindBuildPHPExtraInfo($indent_texts); - logger()->notice('Build will start after 2s ...'); - sleep(2); + logger()->notice('Build will start after 2s ...'); + sleep(2); - // compile libraries - $builder->proveLibs($libraries); - // check extensions - $builder->proveExts($static_extensions, $shared_extensions); - // validate libs and extensions - $builder->validateLibsAndExts(); + // compile libraries + $builder->proveLibs($libraries); + // check extensions + $builder->proveExts($static_extensions, $shared_extensions); + // validate libs and extensions + $builder->validateLibsAndExts(); - // check some things before building all the things - $builder->checkBeforeBuildPHP($rule); + // check some things before building all the things + $builder->checkBeforeBuildPHP($rule); - // clean builds and sources - if ($this->input->getOption('with-clean')) { - logger()->info('Cleaning source and previous build dir...'); - FileSystem::removeDir(SOURCE_PATH); - FileSystem::removeDir(BUILD_ROOT_PATH); - } + // clean builds and sources + if ($this->input->getOption('with-clean')) { + logger()->info('Cleaning source and previous build dir...'); + FileSystem::removeDir(SOURCE_PATH); + FileSystem::removeDir(BUILD_ROOT_PATH); + } - // build or install libraries - $builder->setupLibs(); + // build or install libraries + $builder->setupLibs(); - // Process -I option - $custom_ini = []; - foreach ($this->input->getOption('with-hardcoded-ini') as $value) { - [$source_name, $ini_value] = explode('=', $value, 2); - $custom_ini[$source_name] = $ini_value; - logger()->info('Adding hardcoded INI [' . $source_name . ' = ' . $ini_value . ']'); - } - if (!empty($custom_ini)) { - SourcePatcher::patchHardcodedINI($custom_ini); - } + // Process -I option + $custom_ini = []; + foreach ($this->input->getOption('with-hardcoded-ini') as $value) { + [$source_name, $ini_value] = explode('=', $value, 2); + $custom_ini[$source_name] = $ini_value; + logger()->info('Adding hardcoded INI [' . $source_name . ' = ' . $ini_value . ']'); + } + if (!empty($custom_ini)) { + SourcePatcher::patchHardcodedINI($custom_ini); + } - // add static-php-cli.version to main.c, in order to debug php failure more easily - SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion()); + // add static-php-cli.version to main.c, in order to debug php failure more easily + SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion()); - // clean old modules that may conflict with the new php build - FileSystem::removeDir(BUILD_MODULES_PATH); - // start to build - $builder->buildPHP($rule); + // clean old modules that may conflict with the new php build + FileSystem::removeDir(BUILD_MODULES_PATH); + // start to build + $builder->buildPHP($rule); - // build dynamic extensions if needed - if (!empty($shared_extensions)) { - logger()->info('Building shared extensions ...'); - $builder->buildSharedExts(); - } + // build dynamic extensions if needed + if (!empty($shared_extensions)) { + logger()->info('Building shared extensions ...'); + $builder->buildSharedExts(); + } - $builder->testPHP($rule); + $builder->testPHP($rule); - // compile stopwatch :P - $time = round(microtime(true) - START_TIME, 3); - logger()->info(''); - logger()->info(' Build complete, used ' . $time . ' s !'); - logger()->info(''); + // compile stopwatch :P + $time = round(microtime(true) - START_TIME, 3); + logger()->info(''); + logger()->info(' Build complete, used ' . $time . ' s !'); + logger()->info(''); - // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- - $build_root_path = BUILD_ROOT_PATH; - $cwd = getcwd(); - $fixed = ''; - if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { - str_replace($cwd, '', $build_root_path); - $build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path); - $fixed = ' (host system)'; - } - if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { - $win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : ''; - $path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}"); - logger()->info("Static php binary path{$fixed}: {$path}"); - } - if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - $path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx"); - logger()->info("phpmicro binary path{$fixed}: {$path}"); - } - if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') { - $path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm"); - logger()->info("Static php-fpm binary path{$fixed}: {$path}"); - } - if (!empty($shared_extensions)) { - foreach ($shared_extensions as $ext) { - $path = FileSystem::convertPath("{$build_root_path}/modules/{$ext}.so"); - if (file_exists(BUILD_MODULES_PATH . "/{$ext}.so")) { - logger()->info("Shared extension [{$ext}] path{$fixed}: {$path}"); - } else { - logger()->warning("Shared extension [{$ext}] not found, please check!"); - } + // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- + $build_root_path = BUILD_ROOT_PATH; + $cwd = getcwd(); + $fixed = ''; + if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { + str_replace($cwd, '', $build_root_path); + $build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path); + $fixed = ' (host system)'; + } + if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { + $win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : ''; + $path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}"); + logger()->info("Static php binary path{$fixed}: {$path}"); + } + if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { + $path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx"); + logger()->info("phpmicro binary path{$fixed}: {$path}"); + } + if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') { + $path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm"); + logger()->info("Static php-fpm binary path{$fixed}: {$path}"); + } + if (!empty($shared_extensions)) { + foreach ($shared_extensions as $ext) { + $path = FileSystem::convertPath("{$build_root_path}/modules/{$ext}.so"); + if (file_exists(BUILD_MODULES_PATH . "/{$ext}.so")) { + logger()->info("Shared extension [{$ext}] path{$fixed}: {$path}"); + } else { + logger()->warning("Shared extension [{$ext}] not found, please check!"); } } - - // export metadata - file_put_contents(BUILD_ROOT_PATH . '/build-extensions.json', json_encode($extensions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - file_put_contents(BUILD_ROOT_PATH . '/build-libraries.json', json_encode($libraries, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); - // export licenses - $dumper = new LicenseDumper(); - $dumper->addExts($extensions)->addLibs($libraries)->addSources(['php-src'])->dump(BUILD_ROOT_PATH . '/license'); - $path = FileSystem::convertPath("{$build_root_path}/license/"); - logger()->info("License path{$fixed}: {$path}"); - return static::SUCCESS; - } catch (WrongUsageException $e) { - // WrongUsageException is not an exception, it's a user error, so we just print the error message - logger()->critical($e->getMessage()); - logger()->error($e->getTraceAsString()); - return static::FAILURE; - } catch (\Throwable $e) { - if ($this->getOption('debug')) { - ExceptionHandler::getInstance()->handle($e); - } else { - logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage()); - logger()->critical('Please check with --debug option to see more details.'); - } - return static::FAILURE; } + + // export metadata + file_put_contents(BUILD_ROOT_PATH . '/build-extensions.json', json_encode($extensions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + file_put_contents(BUILD_ROOT_PATH . '/build-libraries.json', json_encode($libraries, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + // export licenses + $dumper = new LicenseDumper(); + $dumper->addExts($extensions)->addLibs($libraries)->addSources(['php-src'])->dump(BUILD_ROOT_PATH . '/license'); + $path = FileSystem::convertPath("{$build_root_path}/license/"); + logger()->info("License path{$fixed}: {$path}"); + return static::SUCCESS; } /** diff --git a/src/SPC/command/DeleteDownloadCommand.php b/src/SPC/command/DeleteDownloadCommand.php index 3cda4a1d..82dd62c4 100644 --- a/src/SPC/command/DeleteDownloadCommand.php +++ b/src/SPC/command/DeleteDownloadCommand.php @@ -4,9 +4,6 @@ declare(strict_types=1); namespace SPC\command; -use SPC\exception\DownloaderException; -use SPC\exception\FileSystemException; -use SPC\exception\WrongUsageException; use SPC\store\Downloader; use SPC\store\FileSystem; use SPC\store\LockFile; @@ -36,57 +33,49 @@ class DeleteDownloadCommand extends BaseCommand public function handle(): int { - try { - // get source list that will be downloaded - $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); - if (empty($sources)) { - logger()->notice('Removing downloads/ directory ...'); - FileSystem::removeDir(DOWNLOAD_PATH); - logger()->info('Removed downloads/ dir!'); - return static::SUCCESS; - } - $chosen_sources = $sources; - - $deleted_sources = []; - foreach ($chosen_sources as $source) { - $source = trim($source); - if (LockFile::get($source) && !$this->getOption('pre-built-only')) { - $deleted_sources[] = $source; - } - if (LockFile::get(Downloader::getPreBuiltLockName($source)) && !$this->getOption('source-only')) { - $deleted_sources[] = Downloader::getPreBuiltLockName($source); - } - } - - foreach ($deleted_sources as $lock_name) { - $lock = LockFile::get($lock_name); - // remove download file/dir if exists - if ($lock['source_type'] === SPC_SOURCE_ARCHIVE) { - if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['filename']))) { - logger()->info('Deleting file ' . $path); - unlink($path); - } else { - logger()->warning("Source/Package [{$lock_name}] file not found, skip deleting file."); - } - } else { - if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['dirname']))) { - logger()->info('Deleting dir ' . $path); - FileSystem::removeDir($path); - } else { - logger()->warning("Source/Package [{$lock_name}] directory not found, skip deleting dir."); - } - } - // remove locked sources - LockFile::put($lock_name, null); - } - logger()->info('Delete success!'); + // get source list that will be downloaded + $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); + if (empty($sources)) { + logger()->notice('Removing downloads/ directory ...'); + FileSystem::removeDir(DOWNLOAD_PATH); + logger()->info('Removed downloads/ dir!'); return static::SUCCESS; - } catch (DownloaderException $e) { - logger()->error($e->getMessage()); - return static::FAILURE; - } catch (WrongUsageException $e) { - logger()->critical($e->getMessage()); - return static::FAILURE; } + $chosen_sources = $sources; + + $deleted_sources = []; + foreach ($chosen_sources as $source) { + $source = trim($source); + if (LockFile::get($source) && !$this->getOption('pre-built-only')) { + $deleted_sources[] = $source; + } + if (LockFile::get(Downloader::getPreBuiltLockName($source)) && !$this->getOption('source-only')) { + $deleted_sources[] = Downloader::getPreBuiltLockName($source); + } + } + + foreach ($deleted_sources as $lock_name) { + $lock = LockFile::get($lock_name); + // remove download file/dir if exists + if ($lock['source_type'] === SPC_SOURCE_ARCHIVE) { + if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['filename']))) { + logger()->info('Deleting file ' . $path); + unlink($path); + } else { + logger()->warning("Source/Package [{$lock_name}] file not found, skip deleting file."); + } + } else { + if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['dirname']))) { + logger()->info('Deleting dir ' . $path); + FileSystem::removeDir($path); + } else { + logger()->warning("Source/Package [{$lock_name}] directory not found, skip deleting dir."); + } + } + // remove locked sources + LockFile::put($lock_name, null); + } + logger()->info('Delete success!'); + return static::SUCCESS; } } diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php index 417409d6..b9bae6a5 100644 --- a/src/SPC/command/DownloadCommand.php +++ b/src/SPC/command/DownloadCommand.php @@ -6,9 +6,7 @@ namespace SPC\command; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\exception\DownloaderException; -use SPC\exception\FileSystemException; -use SPC\exception\RuntimeException; -use SPC\exception\WrongUsageException; +use SPC\exception\SPCException; use SPC\store\Config; use SPC\store\Downloader; use SPC\store\LockFile; @@ -87,177 +85,169 @@ class DownloadCommand extends BaseCommand public function handle(): int { - try { - if ($this->getOption('clean')) { - return $this->_clean(); - } - - // --from-zip - if ($path = $this->getOption('from-zip')) { - return $this->downloadFromZip($path); - } - - // Define PHP major version - $ver = $this->php_major_ver = $this->getOption('with-php'); - define('SPC_BUILD_PHP_VERSION', $ver); - if ($ver !== 'git' && !preg_match('/^\d+\.\d+$/', $ver)) { - // If not git, we need to check the version format - if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) { - logger()->error("bad version arg: {$ver}, x.y or x.y.z required!"); - return static::FAILURE; - } - } - - // retry - $retry = intval($this->getOption('retry')); - f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry); - - // Use shallow-clone can reduce git resource download - if ($this->getOption('shallow-clone')) { - define('GIT_SHALLOW_CLONE', true); - } - - // To read config - Config::getSource('openssl'); - - // use openssl 1.1 - if ($this->getOption('with-openssl11')) { - logger()->debug('Using openssl 1.1'); - Config::$source['openssl']['regex'] = '/href="(?openssl-(?1.[^"]+)\.tar\.gz)\"/'; - } - - $chosen_sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); - - $sss = $this->getOption('ignore-cache-sources'); - if ($sss === false) { - // false is no-any-ignores, that is, default. - $force_all = false; - $force_list = []; - } elseif ($sss === null) { - // null means all sources will be ignored, equals to --force-all (but we don't want to add too many options) - $force_all = true; - $force_list = []; - } else { - // ignore some sources - $force_all = false; - $force_list = array_map('trim', array_filter(explode(',', $this->getOption('ignore-cache-sources')))); - } - - if ($this->getOption('all')) { - logger()->notice('Downloading with --all option will take more times to download, we recommend you to download with --for-extensions option !'); - } - - // Process -U options - $custom_urls = []; - foreach ($this->input->getOption('custom-url') as $value) { - [$source_name, $url] = explode(':', $value, 2); - $custom_urls[$source_name] = $url; - } - // Process -G options - $custom_gits = []; - foreach ($this->input->getOption('custom-git') as $value) { - [$source_name, $branch, $url] = explode(':', $value, 3); - $custom_gits[$source_name] = [$branch, $url]; - } - - // If passing --prefer-pre-built option, we need to load pre-built library list from pre-built.json targeted releases - if ($this->getOption('prefer-pre-built')) { - $repo = Config::getPreBuilt('repo'); - $pre_built_libs = Downloader::getLatestGithubRelease($repo, [ - 'repo' => $repo, - 'prefer-stable' => Config::getPreBuilt('prefer-stable'), - ], false); - } else { - $pre_built_libs = []; - } - - // Download them - f_mkdir(DOWNLOAD_PATH); - $cnt = count($chosen_sources); - $ni = 0; - foreach ($chosen_sources as $source) { - ++$ni; - if (isset($custom_urls[$source])) { - $config = Config::getSource($source); - $new_config = [ - 'type' => 'url', - 'url' => $custom_urls[$source], - ]; - if (isset($config['path'])) { - $new_config['path'] = $config['path']; - } - if (isset($config['filename'])) { - $new_config['filename'] = $config['filename']; - } - logger()->info("[{$ni}/{$cnt}] Downloading source {$source} from custom url: {$new_config['url']}"); - Downloader::downloadSource($source, $new_config, true); - } elseif (isset($custom_gits[$source])) { - $config = Config::getSource($source); - $new_config = [ - 'type' => 'git', - 'rev' => $custom_gits[$source][0], - 'url' => $custom_gits[$source][1], - ]; - if (isset($config['path'])) { - $new_config['path'] = $config['path']; - } - logger()->info("[{$ni}/{$cnt}] Downloading source {$source} from custom git: {$new_config['url']}"); - Downloader::downloadSource($source, $new_config, true); - } else { - $config = Config::getSource($source); - // Prefer pre-built, we need to search pre-built library - if ($this->getOption('prefer-pre-built') && ($config['provide-pre-built'] ?? false) === true) { - // We need to replace pattern - $replace = [ - '{name}' => $source, - '{arch}' => arch2gnu(php_uname('m')), - '{os}' => strtolower(PHP_OS_FAMILY), - '{libc}' => SPCTarget::getLibc() ?? 'default', - '{libcver}' => SPCTarget::getLibcVersion() ?? 'default', - ]; - $find = str_replace(array_keys($replace), array_values($replace), Config::getPreBuilt('match-pattern')); - // find filename in asset list - if (($url = $this->findPreBuilt($pre_built_libs, $find)) !== null) { - logger()->info("[{$ni}/{$cnt}] Downloading pre-built content {$source}"); - Downloader::downloadSource($source, ['type' => 'url', 'url' => $url], $force_all || in_array($source, $force_list), SPC_DOWNLOAD_PRE_BUILT); - continue; - } - logger()->warning("Pre-built content not found for {$source}, fallback to source download"); - } - logger()->info("[{$ni}/{$cnt}] Downloading source {$source}"); - try { - Downloader::downloadSource($source, $config, $force_all || in_array($source, $force_list)); - } /* @noinspection PhpRedundantCatchClauseInspection */ catch (DownloaderException|RuntimeException $e) { - // if `--no-alt` option is set, we will not download alternative sources - if ($this->getOption('no-alt')) { - throw $e; - } - // if download failed, we will try to download alternative sources - logger()->warning("Download failed: {$e->getMessage()}"); - $alt_sources = Config::getSource($source)['alt'] ?? null; - if ($alt_sources === null) { - logger()->warning("No alternative sources found for {$source}, using default alternative source"); - $alt_config = array_merge($config, Downloader::getDefaultAlternativeSource($source)); - } elseif ($alt_sources === false) { - throw new DownloaderException("No alternative sources found for {$source}, skipping alternative download"); - } else { - logger()->notice("Trying to download alternative sources for {$source}"); - $alt_config = array_merge($config, $alt_sources); - } - Downloader::downloadSource($source, $alt_config, $force_all || in_array($source, $force_list)); - } - } - } - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Download complete, used ' . $time . ' s !'); - return static::SUCCESS; - } catch (DownloaderException $e) { - logger()->error($e->getMessage()); - return static::FAILURE; - } catch (WrongUsageException $e) { - logger()->critical($e->getMessage()); - return static::FAILURE; + if ($this->getOption('clean')) { + return $this->_clean(); } + + // --from-zip + if ($path = $this->getOption('from-zip')) { + return $this->downloadFromZip($path); + } + + // Define PHP major version + $ver = $this->php_major_ver = $this->getOption('with-php'); + define('SPC_BUILD_PHP_VERSION', $ver); + if ($ver !== 'git' && !preg_match('/^\d+\.\d+$/', $ver)) { + // If not git, we need to check the version format + if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $ver)) { + logger()->error("bad version arg: {$ver}, x.y or x.y.z required!"); + return static::FAILURE; + } + } + + // retry + $retry = intval($this->getOption('retry')); + f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry); + + // Use shallow-clone can reduce git resource download + if ($this->getOption('shallow-clone')) { + define('GIT_SHALLOW_CLONE', true); + } + + // To read config + Config::getSource('openssl'); + + // use openssl 1.1 + if ($this->getOption('with-openssl11')) { + logger()->debug('Using openssl 1.1'); + Config::$source['openssl']['regex'] = '/href="(?openssl-(?1.[^"]+)\.tar\.gz)\"/'; + } + + $chosen_sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); + + $sss = $this->getOption('ignore-cache-sources'); + if ($sss === false) { + // false is no-any-ignores, that is, default. + $force_all = false; + $force_list = []; + } elseif ($sss === null) { + // null means all sources will be ignored, equals to --force-all (but we don't want to add too many options) + $force_all = true; + $force_list = []; + } else { + // ignore some sources + $force_all = false; + $force_list = array_map('trim', array_filter(explode(',', $this->getOption('ignore-cache-sources')))); + } + + if ($this->getOption('all')) { + logger()->notice('Downloading with --all option will take more times to download, we recommend you to download with --for-extensions option !'); + } + + // Process -U options + $custom_urls = []; + foreach ($this->input->getOption('custom-url') as $value) { + [$source_name, $url] = explode(':', $value, 2); + $custom_urls[$source_name] = $url; + } + // Process -G options + $custom_gits = []; + foreach ($this->input->getOption('custom-git') as $value) { + [$source_name, $branch, $url] = explode(':', $value, 3); + $custom_gits[$source_name] = [$branch, $url]; + } + + // If passing --prefer-pre-built option, we need to load pre-built library list from pre-built.json targeted releases + if ($this->getOption('prefer-pre-built')) { + $repo = Config::getPreBuilt('repo'); + $pre_built_libs = Downloader::getLatestGithubRelease($repo, [ + 'repo' => $repo, + 'prefer-stable' => Config::getPreBuilt('prefer-stable'), + ], false); + } else { + $pre_built_libs = []; + } + + // Download them + f_mkdir(DOWNLOAD_PATH); + $cnt = count($chosen_sources); + $ni = 0; + foreach ($chosen_sources as $source) { + ++$ni; + if (isset($custom_urls[$source])) { + $config = Config::getSource($source); + $new_config = [ + 'type' => 'url', + 'url' => $custom_urls[$source], + ]; + if (isset($config['path'])) { + $new_config['path'] = $config['path']; + } + if (isset($config['filename'])) { + $new_config['filename'] = $config['filename']; + } + logger()->info("[{$ni}/{$cnt}] Downloading source {$source} from custom url: {$new_config['url']}"); + Downloader::downloadSource($source, $new_config, true); + } elseif (isset($custom_gits[$source])) { + $config = Config::getSource($source); + $new_config = [ + 'type' => 'git', + 'rev' => $custom_gits[$source][0], + 'url' => $custom_gits[$source][1], + ]; + if (isset($config['path'])) { + $new_config['path'] = $config['path']; + } + logger()->info("[{$ni}/{$cnt}] Downloading source {$source} from custom git: {$new_config['url']}"); + Downloader::downloadSource($source, $new_config, true); + } else { + $config = Config::getSource($source); + // Prefer pre-built, we need to search pre-built library + if ($this->getOption('prefer-pre-built') && ($config['provide-pre-built'] ?? false) === true) { + // We need to replace pattern + $replace = [ + '{name}' => $source, + '{arch}' => arch2gnu(php_uname('m')), + '{os}' => strtolower(PHP_OS_FAMILY), + '{libc}' => SPCTarget::getLibc() ?? 'default', + '{libcver}' => SPCTarget::getLibcVersion() ?? 'default', + ]; + $find = str_replace(array_keys($replace), array_values($replace), Config::getPreBuilt('match-pattern')); + // find filename in asset list + if (($url = $this->findPreBuilt($pre_built_libs, $find)) !== null) { + logger()->info("[{$ni}/{$cnt}] Downloading pre-built content {$source}"); + Downloader::downloadSource($source, ['type' => 'url', 'url' => $url], $force_all || in_array($source, $force_list), SPC_DOWNLOAD_PRE_BUILT); + continue; + } + logger()->warning("Pre-built content not found for {$source}, fallback to source download"); + } + logger()->info("[{$ni}/{$cnt}] Downloading source {$source}"); + try { + Downloader::downloadSource($source, $config, $force_all || in_array($source, $force_list)); + } catch (SPCException $e) { + // if `--no-alt` option is set, we will not download alternative sources + if ($this->getOption('no-alt')) { + throw $e; + } + // if download failed, we will try to download alternative sources + logger()->warning("Download failed: {$e->getMessage()}"); + $alt_sources = Config::getSource($source)['alt'] ?? null; + if ($alt_sources === null) { + logger()->warning("No alternative sources found for {$source}, using default alternative source"); + $alt_config = array_merge($config, Downloader::getDefaultAlternativeSource($source)); + } elseif ($alt_sources === false) { + throw new DownloaderException("No alternative sources found for {$source}, skipping alternative download"); + } else { + logger()->notice("Trying to download alternative sources for {$source}"); + $alt_config = array_merge($config, $alt_sources); + } + Downloader::downloadSource($source, $alt_config, $force_all || in_array($source, $force_list)); + } + } + } + $time = round(microtime(true) - START_TIME, 3); + logger()->info('Download complete, used ' . $time . ' s !'); + return static::SUCCESS; } private function downloadFromZip(string $path): int @@ -276,28 +266,24 @@ class DownloadCommand extends BaseCommand } // unzip command check if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) { - logger()->critical('Missing unzip command, you need to install it first !'); - logger()->critical('You can use "bin/spc doctor" command to check and install required tools'); + $this->output->writeln('Missing unzip command, you need to install it first !'); + $this->output->writeln('You can use "bin/spc doctor" command to check and install required tools'); return static::FAILURE; } // create downloads - try { - if (PHP_OS_FAMILY !== 'Windows') { - $abs_path = realpath($path); - f_passthru('mkdir ' . DOWNLOAD_PATH . ' && cd ' . DOWNLOAD_PATH . ' && unzip ' . escapeshellarg($abs_path)); - } else { - // Windows TODO - throw new WrongUsageException('Windows currently does not support --from-zip !'); - } - - if (!file_exists(LockFile::LOCK_FILE)) { - throw new RuntimeException('.lock.json not exist in "downloads/"'); - } - } catch (RuntimeException $e) { - logger()->critical('Extract failed: ' . $e->getMessage()); + if (PHP_OS_FAMILY === 'Windows') { + // Windows TODO + $this->output->writeln('Windows currently does not support --from-zip !'); return static::FAILURE; } - logger()->info('Extract success'); + $abs_path = realpath($path); + f_passthru('mkdir ' . DOWNLOAD_PATH . ' && cd ' . DOWNLOAD_PATH . ' && unzip ' . escapeshellarg($abs_path)); + + if (!file_exists(LockFile::LOCK_FILE)) { + $this->output->writeln('.lock.json not exist in "downloads/", please run "bin/spc download" first !'); + return static::FAILURE; + } + $this->output->writeln('Extract success'); return static::SUCCESS; } diff --git a/src/SPC/command/InstallPkgCommand.php b/src/SPC/command/InstallPkgCommand.php index c4f60981..13a07888 100644 --- a/src/SPC/command/InstallPkgCommand.php +++ b/src/SPC/command/InstallPkgCommand.php @@ -5,9 +5,6 @@ declare(strict_types=1); namespace SPC\command; use SPC\builder\traits\UnixSystemUtilTrait; -use SPC\exception\DownloaderException; -use SPC\exception\FileSystemException; -use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\PackageManager; use Symfony\Component\Console\Attribute\AsCommand; @@ -30,66 +27,58 @@ class InstallPkgCommand extends BaseCommand public function handle(): int { - try { - // Use shallow-clone can reduce git resource download - if ($this->getOption('shallow-clone')) { - define('GIT_SHALLOW_CLONE', true); - } - - // Process -U options - $custom_urls = []; - foreach ($this->input->getOption('custom-url') as $value) { - [$pkg_name, $url] = explode(':', $value, 2); - $custom_urls[$pkg_name] = $url; - } - - $chosen_pkgs = array_map('trim', array_filter(explode(',', $this->getArgument('packages')))); - - // Download them - f_mkdir(DOWNLOAD_PATH); - $ni = 0; - $cnt = count($chosen_pkgs); - - foreach ($chosen_pkgs as $pkg) { - ++$ni; - if (isset($custom_urls[$pkg])) { - $config = Config::getPkg($pkg); - $new_config = [ - 'type' => 'url', - 'url' => $custom_urls[$pkg], - ]; - if (isset($config['extract'])) { - $new_config['extract'] = $config['extract']; - } - if (isset($config['filename'])) { - $new_config['filename'] = $config['filename']; - } - logger()->info("Installing source {$pkg} from custom url [{$ni}/{$cnt}]"); - PackageManager::installPackage( - $pkg, - $new_config, - allow_alt: false, - extract: !$this->getOption('skip-extract') - ); - } else { - logger()->info("Fetching package {$pkg} [{$ni}/{$cnt}]"); - PackageManager::installPackage( - $pkg, - Config::getPkg($pkg), - allow_alt: !$this->getOption('no-alt'), - extract: !$this->getOption('skip-extract') - ); - } - } - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Install packages complete, used ' . $time . ' s !'); - return static::SUCCESS; - } catch (DownloaderException $e) { - logger()->error($e->getMessage()); - return static::FAILURE; - } catch (WrongUsageException $e) { - logger()->critical($e->getMessage()); - return static::FAILURE; + // Use shallow-clone can reduce git resource download + if ($this->getOption('shallow-clone')) { + define('GIT_SHALLOW_CLONE', true); } + + // Process -U options + $custom_urls = []; + foreach ($this->input->getOption('custom-url') as $value) { + [$pkg_name, $url] = explode(':', $value, 2); + $custom_urls[$pkg_name] = $url; + } + + $chosen_pkgs = array_map('trim', array_filter(explode(',', $this->getArgument('packages')))); + + // Download them + f_mkdir(DOWNLOAD_PATH); + $ni = 0; + $cnt = count($chosen_pkgs); + + foreach ($chosen_pkgs as $pkg) { + ++$ni; + if (isset($custom_urls[$pkg])) { + $config = Config::getPkg($pkg); + $new_config = [ + 'type' => 'url', + 'url' => $custom_urls[$pkg], + ]; + if (isset($config['extract'])) { + $new_config['extract'] = $config['extract']; + } + if (isset($config['filename'])) { + $new_config['filename'] = $config['filename']; + } + logger()->info("Installing source {$pkg} from custom url [{$ni}/{$cnt}]"); + PackageManager::installPackage( + $pkg, + $new_config, + allow_alt: false, + extract: !$this->getOption('skip-extract') + ); + } else { + logger()->info("Fetching package {$pkg} [{$ni}/{$cnt}]"); + PackageManager::installPackage( + $pkg, + Config::getPkg($pkg), + allow_alt: !$this->getOption('no-alt'), + extract: !$this->getOption('skip-extract') + ); + } + } + $time = round(microtime(true) - START_TIME, 3); + logger()->info('Install packages complete, used ' . $time . ' s !'); + return static::SUCCESS; } } diff --git a/src/SPC/command/dev/PackLibCommand.php b/src/SPC/command/dev/PackLibCommand.php index 4bac73d7..9b02fd3f 100644 --- a/src/SPC/command/dev/PackLibCommand.php +++ b/src/SPC/command/dev/PackLibCommand.php @@ -7,10 +7,7 @@ namespace SPC\command\dev; use SPC\builder\BuilderProvider; use SPC\builder\LibraryBase; use SPC\command\BuildCommand; -use SPC\exception\ExceptionHandler; -use SPC\exception\FileSystemException; -use SPC\exception\RuntimeException; -use SPC\exception\WrongUsageException; +use SPC\exception\ValidationException; use SPC\store\Config; use SPC\store\FileSystem; use SPC\store\LockFile; @@ -30,120 +27,110 @@ class PackLibCommand extends BuildCommand public function handle(): int { - try { - $lib_name = $this->getArgument('library'); - $builder = BuilderProvider::makeBuilderByInput($this->input); - $builder->setLibsOnly(); - $libraries = DependencyUtil::getLibs([$lib_name]); - logger()->info('Building libraries: ' . implode(',', $libraries)); - sleep(2); + $lib_name = $this->getArgument('library'); + $builder = BuilderProvider::makeBuilderByInput($this->input); + $builder->setLibsOnly(); + $libraries = DependencyUtil::getLibs([$lib_name]); + logger()->info('Building libraries: ' . implode(',', $libraries)); + sleep(2); - FileSystem::createDir(WORKING_DIR . '/dist'); + FileSystem::createDir(WORKING_DIR . '/dist'); - $builder->proveLibs($libraries); - $builder->validateLibsAndExts(); + $builder->proveLibs($libraries); + $builder->validateLibsAndExts(); - // before pack, check if the dependency tree contains lib-suggests - foreach ($libraries as $lib) { - if (Config::getLib($lib, 'lib-suggests', []) !== []) { - logger()->critical("The library {$lib} has lib-suggests, packing [{$lib_name}] is not safe, abort !"); + // before pack, check if the dependency tree contains lib-suggests + foreach ($libraries as $lib) { + if (Config::getLib($lib, 'lib-suggests', []) !== []) { + logger()->critical("The library {$lib} has lib-suggests, packing [{$lib_name}] is not safe, abort !"); + return static::FAILURE; + } + } + + $origin_files = []; + // get pack placehoder defines + $placehoder = get_pack_replace(); + + foreach ($builder->getLibs() as $lib) { + if ($lib->getName() !== $lib_name) { + // other dependencies: install or build, both ok + $lib->setup(); + } else { + // Get lock info + $source = Config::getLib($lib->getName(), 'source'); + if (($lock = LockFile::get($source)) === null || ($lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT)) { + logger()->critical("The library {$lib->getName()} is downloaded as pre-built, we need to build it instead of installing pre-built."); return static::FAILURE; } - } + // Before build: load buildroot/ directory + $before_buildroot = FileSystem::scanDirFiles(BUILD_ROOT_PATH, relative: true); + // build + $lib->tryBuild(true); + // do something like patching pkg-conf files. + $lib->beforePack(); + // sanity check for libs (check if the libraries are built correctly) + $this->sanityCheckLib($lib); + // After build: load buildroot/ directory, and calculate increase files + $after_buildroot = FileSystem::scanDirFiles(BUILD_ROOT_PATH, relative: true); + $increase_files = array_diff($after_buildroot, $before_buildroot); - $origin_files = []; - // get pack placehoder defines - $placehoder = get_pack_replace(); - - foreach ($builder->getLibs() as $lib) { - if ($lib->getName() !== $lib_name) { - // other dependencies: install or build, both ok - $lib->setup(); - } else { - // Get lock info - $source = Config::getLib($lib->getName(), 'source'); - if (($lock = LockFile::get($source)) === null || ($lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT)) { - logger()->critical("The library {$lib->getName()} is downloaded as pre-built, we need to build it instead of installing pre-built."); - return static::FAILURE; + // patch pkg-config and la files with absolute path + foreach ($increase_files as $file) { + if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) { + $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); + $origin_files[$file] = $content; + // replace relative paths with absolute paths + $content = str_replace( + array_keys($placehoder), + array_values($placehoder), + $content + ); + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); } - // Before build: load buildroot/ directory - $before_buildroot = FileSystem::scanDirFiles(BUILD_ROOT_PATH, relative: true); - // build - $lib->tryBuild(true); - // do something like patching pkg-conf files. - $lib->beforePack(); - // sanity check for libs (check if the libraries are built correctly) - $this->sanityCheckLib($lib); - // After build: load buildroot/ directory, and calculate increase files - $after_buildroot = FileSystem::scanDirFiles(BUILD_ROOT_PATH, relative: true); - $increase_files = array_diff($after_buildroot, $before_buildroot); - - // patch pkg-config and la files with absolute path - foreach ($increase_files as $file) { - if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) { - $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); - $origin_files[$file] = $content; - // replace relative paths with absolute paths - $content = str_replace( - array_keys($placehoder), - array_values($placehoder), - $content - ); - FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); - } - } - - // add .spc-extract-placeholder.json in BUILD_ROOT_PATH - $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; - file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT)); - $increase_files[] = '.spc-extract-placeholder.json'; - - // every file mapped with BUILD_ROOT_PATH - // get BUILD_ROOT_PATH last dir part - $buildroot_part = basename(BUILD_ROOT_PATH); - $increase_files = array_map(fn ($file) => $buildroot_part . '/' . $file, $increase_files); - // write list to packlib_files.txt - FileSystem::writeFile(WORKING_DIR . '/packlib_files.txt', implode("\n", $increase_files)); - // pack - $filename = Config::getPreBuilt('match-pattern'); - $replace = [ - '{name}' => $lib->getName(), - '{arch}' => arch2gnu(php_uname('m')), - '{os}' => strtolower(PHP_OS_FAMILY), - '{libc}' => SPCTarget::getLibc() ?? 'default', - '{libcver}' => SPCTarget::getLibcVersion() ?? 'default', - ]; - // detect suffix, for proper tar option - $tar_option = $this->getTarOptionFromSuffix(Config::getPreBuilt('match-pattern')); - $filename = str_replace(array_keys($replace), array_values($replace), $filename); - $filename = WORKING_DIR . '/dist/' . $filename; - f_passthru("tar {$tar_option} {$filename} -T " . WORKING_DIR . '/packlib_files.txt'); - logger()->info('Pack library ' . $lib->getName() . ' to ' . $filename . ' complete.'); - - // remove temp files - unlink($placeholder_file); } - } - foreach ($origin_files as $file => $content) { - // restore original files - if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { - FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); - } - } + // add .spc-extract-placeholder.json in BUILD_ROOT_PATH + $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; + file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT)); + $increase_files[] = '.spc-extract-placeholder.json'; - $time = round(microtime(true) - START_TIME, 3); - logger()->info('Build libs complete, used ' . $time . ' s !'); - return static::SUCCESS; - } catch (\Throwable $e) { - if ($this->getOption('debug')) { - ExceptionHandler::getInstance()->handle($e); - } else { - logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage()); - logger()->critical('Please check with --debug option to see more details.'); + // every file mapped with BUILD_ROOT_PATH + // get BUILD_ROOT_PATH last dir part + $buildroot_part = basename(BUILD_ROOT_PATH); + $increase_files = array_map(fn ($file) => $buildroot_part . '/' . $file, $increase_files); + // write list to packlib_files.txt + FileSystem::writeFile(WORKING_DIR . '/packlib_files.txt', implode("\n", $increase_files)); + // pack + $filename = Config::getPreBuilt('match-pattern'); + $replace = [ + '{name}' => $lib->getName(), + '{arch}' => arch2gnu(php_uname('m')), + '{os}' => strtolower(PHP_OS_FAMILY), + '{libc}' => SPCTarget::getLibc() ?? 'default', + '{libcver}' => SPCTarget::getLibcVersion() ?? 'default', + ]; + // detect suffix, for proper tar option + $tar_option = $this->getTarOptionFromSuffix(Config::getPreBuilt('match-pattern')); + $filename = str_replace(array_keys($replace), array_values($replace), $filename); + $filename = WORKING_DIR . '/dist/' . $filename; + f_passthru("tar {$tar_option} {$filename} -T " . WORKING_DIR . '/packlib_files.txt'); + logger()->info('Pack library ' . $lib->getName() . ' to ' . $filename . ' complete.'); + + // remove temp files + unlink($placeholder_file); } - return static::FAILURE; } + + foreach ($origin_files as $file => $content) { + // restore original files + if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + } + } + + $time = round(microtime(true) - START_TIME, 3); + logger()->info('Build libs complete, used ' . $time . ' s !'); + return static::SUCCESS; } private function sanityCheckLib(LibraryBase $lib): void @@ -152,7 +139,10 @@ class PackLibCommand extends BuildCommand // config foreach ($lib->getStaticLibs() as $static_lib) { if (!file_exists(FileSystem::convertPath(BUILD_LIB_PATH . '/' . $static_lib))) { - throw new RuntimeException('Static library ' . $static_lib . ' not found in ' . BUILD_LIB_PATH); + throw new ValidationException( + 'Static library ' . $static_lib . ' not found in ' . BUILD_LIB_PATH, + validation_module: "Static library {$static_lib} existence check" + ); } } } diff --git a/src/SPC/exception/ExceptionHandler.php b/src/SPC/exception/ExceptionHandler.php index d103b957..45815142 100644 --- a/src/SPC/exception/ExceptionHandler.php +++ b/src/SPC/exception/ExceptionHandler.php @@ -4,24 +4,167 @@ declare(strict_types=1); namespace SPC\exception; +use ZM\Logger\ConsoleColor; + class ExceptionHandler { - protected mixed $whoops = null; + public const array KNOWN_EXCEPTIONS = [ + BuildFailureException::class, + DownloaderException::class, + EnvironmentException::class, + ExecutionException::class, + FileSystemException::class, + InterruptException::class, + PatchException::class, + SPCInternalException::class, + ValidationException::class, + WrongUsageException::class, + ]; - private static ?ExceptionHandler $obj = null; + public const array MINOR_LOG_EXCEPTIONS = [ + InterruptException::class, + WrongUsageException::class, + ]; - public static function getInstance(): ExceptionHandler + public static function handleSPCException(SPCException $e): void { - if (self::$obj === null) { - self::$obj = new self(); + // XXX error: yyy + $head_msg = match ($class = get_class($e)) { + BuildFailureException::class => "Build failed: {$e->getMessage()}", + DownloaderException::class => "Download failed: {$e->getMessage()}", + EnvironmentException::class => "Environment check failed: {$e->getMessage()}", + ExecutionException::class => "Command execution failed: {$e->getMessage()}", + FileSystemException::class => "File system error: {$e->getMessage()}", + InterruptException::class => "⚠ Build interrupted by user: {$e->getMessage()}", + PatchException::class => "Patch apply failed: {$e->getMessage()}", + SPCInternalException::class => "SPC internal error: {$e->getMessage()}", + ValidationException::class => "Validation failed: {$e->getMessage()}", + WrongUsageException::class => $e->getMessage(), + default => "Unknown SPC exception {$class}: {$e->getMessage()}", + }; + self::logError($head_msg); + + // ---------------------------------------- + $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); + + if ($minor_logs) { + return; + } + + self::logError("----------------------------------------\n"); + + // get the SPCException module + if ($php_info = $e->getBuildPHPInfo()) { + self::logError('✗ Failed module: ' . ConsoleColor::yellow("PHP builder {$php_info['builder_class']} for {$php_info['os']}")); + } elseif ($lib_info = $e->getLibraryInfo()) { + self::logError('✗ Failed module: ' . ConsoleColor::yellow("library {$lib_info['library_name']} builder for {$lib_info['os']}")); + } elseif ($ext_info = $e->getExtensionInfo()) { + self::logError('✗ Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder")); + } elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) { + self::logError('✗ Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); + } + self::logError(''); + + // get command execution info + if ($e instanceof ExecutionException) { + self::logError('✗ Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand())); + if ($cd = $e->getCd()) { + self::logError('✗ Command executed in: ' . ConsoleColor::yellow($cd)); + } + if ($env = $e->getEnv()) { + self::logError('✗ Command inline env variables:'); + foreach ($env as $k => $v) { + self::logError(ConsoleColor::yellow("{$k}={$v}"), 4); + } + } + } + + // validation error + if ($e instanceof ValidationException) { + self::logError('✗ Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString())); + } + + // environment error + if ($e instanceof EnvironmentException) { + self::logError('✗ Failed environment check: ' . ConsoleColor::yellow($e->getMessage())); + if (($solution = $e->getSolution()) !== null) { + self::logError('✗ Solution: ' . ConsoleColor::yellow($solution)); + } + } + + // get patch info + if ($e instanceof PatchException) { + self::logError("✗ Failed patch module: {$e->getPatchModule()}"); + } + + // get internal trace + if ($e instanceof SPCInternalException) { + self::logError('✗ Internal trace:'); + self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4); + } + + // get the full build info if possible + if (($info = $e->getBuildPHPExtraInfo()) && defined('DEBUG_MODE')) { + self::logError('✗ Build PHP extra info:'); + $maxlen = 0; + foreach ($info as $k => $v) { + $maxlen = max(strlen($k), $maxlen); + } + foreach ($info as $k => $v) { + if (is_string($v)) { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v), 4); + } elseif (is_array($v) && !is_assoc_array($v)) { + $first = array_shift($v); + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first), 4); + foreach ($v as $vs) { + self::logError(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs), 4); + } + } + } + } + + self::logError("\n----------------------------------------\n"); + + // put getenv info to log + $env_log = fopen(SPC_ENV_LOG, 'a'); + $env_info = getenv(); + if ($env_info) { + foreach ($env_info as $k => $v) { + fwrite($env_log, $k . ' = ' . $v . PHP_EOL); + } + } + + self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_OUTPUT_LOG)); + if (file_exists(SPC_SHELL_LOG)) { + self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_SHELL_LOG)); + } + if ($e->getExtraLogFiles() !== []) { + foreach ($e->getExtraLogFiles() as $key => $file) { + self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none(SPC_LOGS_DIR . "/{$file}")); + } + } + if (!defined('DEBUG_MODE')) { + self::logError('⚠ If you want to see more details in console, use `--debug` option.'); } - return self::$obj; } - public function handle(\Throwable $e): void + public static function handleDefaultException(\Throwable $e): void { - logger()->error('Uncaught ' . get_class($e) . ': ' . $e->getMessage() . ' at ' . $e->getFile() . '(' . $e->getLine() . ')'); - logger()->error($e->getTraceAsString()); - logger()->critical('You can report this exception to static-php-cli GitHub repo.'); + $class = get_class($e); + self::logError("Unhandled exception {$class}: {$e->getMessage()}\n\t{$e->getMessage()}\n"); + self::logError('Stack trace:'); + self::logError(ConsoleColor::gray($e->getTraceAsString()), 4); + self::logError('Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues'); + } + + private static function logError($message, int $indent_space = 0): void + { + $spc_log = fopen(SPC_OUTPUT_LOG, 'a'); + $msg = explode("\n", (string) $message); + foreach ($msg as $v) { + $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); + fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + echo ConsoleColor::red($line) . PHP_EOL; + } } } diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php index 483d8bbb..4181b693 100644 --- a/tests/SPC/builder/BuilderTest.php +++ b/tests/SPC/builder/BuilderTest.php @@ -9,7 +9,6 @@ use SPC\builder\BuilderBase; use SPC\builder\BuilderProvider; use SPC\builder\Extension; use SPC\builder\LibraryBase; -use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\FileSystem; use SPC\store\LockFile; @@ -88,7 +87,7 @@ class BuilderTest extends TestCase if ($cnt !== 0) { $this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID()); } else { - $this->expectException(RuntimeException::class); + $this->expectException(WrongUsageException::class); $this->builder->getPHPVersionID(); } } else { @@ -105,7 +104,7 @@ class BuilderTest extends TestCase if ($cnt !== 0) { $this->assertEquals($match[1], $this->builder->getPHPVersion()); } else { - $this->expectException(RuntimeException::class); + $this->expectException(WrongUsageException::class); $this->builder->getPHPVersion(); } } else { @@ -246,7 +245,7 @@ class BuilderTest extends TestCase public function testEmitPatchPointNotExists() { $this->expectOutputRegex('/failed to run/'); - $this->expectException(RuntimeException::class); + $this->expectException(WrongUsageException::class); $this->builder->setOption('with-added-patch', ['/tmp/patch-point.not_exsssists.php']); $this->builder->emitPatchPoint('not-exists'); }