Refactor all command class exception handling

This commit is contained in:
crazywhalecc
2025-08-06 20:45:16 +08:00
committed by Jerry Ma
parent f68f060be2
commit 333b776e77
10 changed files with 691 additions and 643 deletions

View File

@@ -4,18 +4,15 @@ declare(strict_types=1);
namespace SPC\builder; namespace SPC\builder;
use PharIo\FileSystem\File; use SPC\exception\BuildFailureException;
use SPC\exception\ExceptionHandler;
use SPC\exception\FileSystemException;
use SPC\exception\InterruptException; use SPC\exception\InterruptException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\LockFile; use SPC\store\LockFile;
use SPC\store\SourceManager; use SPC\store\SourceManager;
use SPC\store\SourcePatcher; use SPC\store\SourcePatcher;
use SPC\util\CustomExt; use SPC\util\AttributeMapper;
abstract class BuilderBase abstract class BuilderBase
{ {
@@ -64,12 +61,11 @@ abstract class BuilderBase
match ($status) { match ($status) {
LIB_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] setup success, took ' . round(microtime(true) - $starttime, 2) . ' s'), 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_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'), LIB_STATUS_INSTALL_FAILED => logger()->error('lib [' . $lib::NAME . '] install failed'),
default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'), default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'),
}; };
if (in_array($status, [LIB_STATUS_BUILD_FAILED, LIB_STATUS_INSTALL_FAILED])) { 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(); $ext->buildShared();
} }
} catch (RuntimeException $e) { } finally {
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', $extension_dir_line); 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::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'); 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]); 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 public function getPHPVersion(bool $exception_on_failure = true): string
@@ -329,7 +324,7 @@ abstract class BuilderBase
if (!$exception_on_failure) { if (!$exception_on_failure) {
return 'unknown'; 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) { foreach ($patches as $patch) {
try { try {
if (!file_exists($patch)) { 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); logger()->debug('Running additional patch script: ' . $patch);
require $patch; require $patch;
@@ -489,11 +484,6 @@ abstract class BuilderBase
exit($e->getCode()); exit($e->getCode());
} catch (\Throwable $e) { } catch (\Throwable $e) {
logger()->critical('Patch script ' . $patch . ' failed to run.'); 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; throw $e;
} }
} }

View File

@@ -9,8 +9,8 @@ use Laravel\Prompts\Prompt;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use SPC\ConsoleApplication; use SPC\ConsoleApplication;
use SPC\exception\ExceptionHandler; use SPC\exception\ExceptionHandler;
use SPC\exception\ValidationException; use SPC\exception\SPCException;
use SPC\exception\WrongUsageException; use SPC\util\AttributeMapper;
use SPC\util\GlobalEnvManager; use SPC\util\GlobalEnvManager;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Helper\QuestionHelper;
@@ -32,6 +32,7 @@ abstract class BaseCommand extends Command
parent::__construct($name); parent::__construct($name);
$this->addOption('debug', null, null, 'Enable debug mode'); $this->addOption('debug', null, null, 'Enable debug mode');
$this->addOption('no-motd', null, null, 'Disable motd'); $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 public function initialize(InputInterface $input, OutputInterface $output): void
@@ -93,30 +94,20 @@ abstract class BaseCommand extends Command
GlobalEnvManager::init(); GlobalEnvManager::init();
f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes'); f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes');
} }
if ($this->shouldExecute()) {
try { try {
// show raw argv list for logger()->debug // show raw argv list for logger()->debug
logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); logger()->debug('argv: ' . implode(' ', $_SERVER['argv']));
return $this->handle(); return $this->handle();
} catch (ValidationException|WrongUsageException $e) { } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) {
$msg = explode("\n", $e->getMessage()); // Handle SPCException and log it
foreach ($msg as $v) { ExceptionHandler::handleSPCException($e);
logger()->error($v); return static::FAILURE;
} } catch (\Throwable $e) {
return static::FAILURE; // Handle any other exceptions
} catch (\Throwable $e) { ExceptionHandler::handleDefaultException($e);
if ($this->getOption('debug')) { return static::FAILURE;
ExceptionHandler::getInstance()->handle($e);
} else {
$msg = explode("\n", $e->getMessage());
foreach ($msg as $v) {
logger()->error($v);
}
}
return static::FAILURE;
}
} }
return static::SUCCESS;
} }
protected function getOption(string $name): mixed protected function getOption(string $name): mixed
@@ -129,11 +120,6 @@ abstract class BaseCommand extends Command
return $this->input->getArgument($name); return $this->input->getArgument($name);
} }
protected function shouldExecute(): bool
{
return true;
}
protected function logWithResult(bool $result, string $success_msg, string $fail_msg): int protected function logWithResult(bool $result, string $success_msg, string $fail_msg): int
{ {
if ($result) { if ($result) {

View File

@@ -50,32 +50,22 @@ class BuildLibsCommand extends BuildCommand
} }
} }
try { // 构建对象
// 构建对象 $builder = BuilderProvider::makeBuilderByInput($this->input);
$builder = BuilderProvider::makeBuilderByInput($this->input); // 只编译 library 的情况下,标记
// 只编译 library 的情况下,标记 $builder->setLibsOnly();
$builder->setLibsOnly(); // 编译和检查库完整
// 编译和检查库完整 $libraries = DependencyUtil::getLibs($libraries);
$libraries = DependencyUtil::getLibs($libraries); $display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
$display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
logger()->info('Building libraries: ' . implode(',', $display_libs)); logger()->info('Building libraries: ' . implode(',', $display_libs));
sleep(2); sleep(2);
$builder->proveLibs($libraries); $builder->proveLibs($libraries);
$builder->validateLibsAndExts(); $builder->validateLibsAndExts();
$builder->setupLibs(); $builder->setupLibs();
$time = round(microtime(true) - START_TIME, 3); $time = round(microtime(true) - START_TIME, 3);
logger()->info('Build libs complete, used ' . $time . ' s !'); logger()->info('Build libs complete, used ' . $time . ' s !');
return static::SUCCESS; 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;
}
} }
} }

View File

@@ -5,8 +5,7 @@ declare(strict_types=1);
namespace SPC\command; namespace SPC\command;
use SPC\builder\BuilderProvider; use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler; use SPC\exception\SPCException;
use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\SourcePatcher; 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!'); logger()->warning('Some cases micro.sfx cannot be packed via UPX due to dynamic size bug, be aware!');
} }
} }
try { // create builder
// create builder $builder = BuilderProvider::makeBuilderByInput($this->input);
$builder = BuilderProvider::makeBuilderByInput($this->input); $include_suggest_ext = $this->getOption('with-suggested-exts');
$include_suggest_ext = $this->getOption('with-suggested-exts'); $include_suggest_lib = $this->getOption('with-suggested-libs');
$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);
[$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']));
$display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
// separate static and shared extensions from $extensions // separate static and shared extensions from $extensions
// filter rule: including shared extensions if they are in $static_extensions or $shared_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)); $static_extensions = array_filter($extensions, fn ($ext) => !in_array($ext, $shared_extensions) || in_array($ext, $static_extensions));
// print info // print info
$indent_texts = [ $indent_texts = [
'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')', 'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')',
'Build Target' => getenv('SPC_TARGET'), 'Build Target' => getenv('SPC_TARGET'),
'Build Toolchain' => getenv('SPC_TOOLCHAIN'), 'Build Toolchain' => getenv('SPC_TOOLCHAIN'),
'Build SAPI' => $builder->getBuildTypeName($rule), 'Build SAPI' => $builder->getBuildTypeName($rule),
'Static Extensions (' . count($static_extensions) . ')' => implode(',', $static_extensions), 'Static Extensions (' . count($static_extensions) . ')' => implode(',', $static_extensions),
'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions),
'Libraries (' . count($libraries) . ')' => implode(',', $display_libs), 'Libraries (' . count($libraries) . ')' => implode(',', $display_libs),
'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes', 'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes',
'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no', 'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no',
]; ];
if (!empty($shared_extensions) || ($rule & BUILD_TARGET_EMBED)) { if (!empty($shared_extensions) || ($rule & BUILD_TARGET_EMBED)) {
$indent_texts['Build Dev'] = 'yes'; $indent_texts['Build Dev'] = 'yes';
} }
if (!empty($this->input->getOption('with-config-file-path'))) { if (!empty($this->input->getOption('with-config-file-path'))) {
$indent_texts['Config File Path'] = $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'))) { if (!empty($this->input->getOption('with-hardcoded-ini'))) {
$indent_texts['Hardcoded INI'] = $this->input->getOption('with-hardcoded-ini'); $indent_texts['Hardcoded INI'] = $this->input->getOption('with-hardcoded-ini');
} }
if ($this->input->getOption('disable-opcache-jit')) { if ($this->input->getOption('disable-opcache-jit')) {
$indent_texts['Opcache JIT'] = 'disabled'; $indent_texts['Opcache JIT'] = 'disabled';
} }
if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) { if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) {
$indent_texts['UPX Pack'] = 'enabled'; $indent_texts['UPX Pack'] = 'enabled';
} }
$ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion(false); $ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion(false);
$indent_texts['PHP Version'] = $ver; $indent_texts['PHP Version'] = $ver;
if (!empty($not_included)) { if (!empty($not_included)) {
$indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included); $indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included);
} }
$this->printFormatInfo($this->getDefinedEnvs(), true); $this->printFormatInfo($this->getDefinedEnvs(), true);
$this->printFormatInfo($indent_texts); $this->printFormatInfo($indent_texts);
// bind extra info to SPCException
SPCException::bindBuildPHPExtraInfo($indent_texts);
logger()->notice('Build will start after 2s ...'); logger()->notice('Build will start after 2s ...');
sleep(2); sleep(2);
// compile libraries // compile libraries
$builder->proveLibs($libraries); $builder->proveLibs($libraries);
// check extensions // check extensions
$builder->proveExts($static_extensions, $shared_extensions); $builder->proveExts($static_extensions, $shared_extensions);
// validate libs and extensions // validate libs and extensions
$builder->validateLibsAndExts(); $builder->validateLibsAndExts();
// check some things before building all the things // check some things before building all the things
$builder->checkBeforeBuildPHP($rule); $builder->checkBeforeBuildPHP($rule);
// clean builds and sources // clean builds and sources
if ($this->input->getOption('with-clean')) { if ($this->input->getOption('with-clean')) {
logger()->info('Cleaning source and previous build dir...'); logger()->info('Cleaning source and previous build dir...');
FileSystem::removeDir(SOURCE_PATH); FileSystem::removeDir(SOURCE_PATH);
FileSystem::removeDir(BUILD_ROOT_PATH); FileSystem::removeDir(BUILD_ROOT_PATH);
} }
// build or install libraries // build or install libraries
$builder->setupLibs(); $builder->setupLibs();
// Process -I option // Process -I option
$custom_ini = []; $custom_ini = [];
foreach ($this->input->getOption('with-hardcoded-ini') as $value) { foreach ($this->input->getOption('with-hardcoded-ini') as $value) {
[$source_name, $ini_value] = explode('=', $value, 2); [$source_name, $ini_value] = explode('=', $value, 2);
$custom_ini[$source_name] = $ini_value; $custom_ini[$source_name] = $ini_value;
logger()->info('Adding hardcoded INI [' . $source_name . ' = ' . $ini_value . ']'); logger()->info('Adding hardcoded INI [' . $source_name . ' = ' . $ini_value . ']');
} }
if (!empty($custom_ini)) { if (!empty($custom_ini)) {
SourcePatcher::patchHardcodedINI($custom_ini); SourcePatcher::patchHardcodedINI($custom_ini);
} }
// add static-php-cli.version to main.c, in order to debug php failure more easily // add static-php-cli.version to main.c, in order to debug php failure more easily
SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion()); SourcePatcher::patchSPCVersionToPHP($this->getApplication()->getVersion());
// clean old modules that may conflict with the new php build // clean old modules that may conflict with the new php build
FileSystem::removeDir(BUILD_MODULES_PATH); FileSystem::removeDir(BUILD_MODULES_PATH);
// start to build // start to build
$builder->buildPHP($rule); $builder->buildPHP($rule);
// build dynamic extensions if needed // build dynamic extensions if needed
if (!empty($shared_extensions)) { if (!empty($shared_extensions)) {
logger()->info('Building shared extensions ...'); logger()->info('Building shared extensions ...');
$builder->buildSharedExts(); $builder->buildSharedExts();
} }
$builder->testPHP($rule); $builder->testPHP($rule);
// compile stopwatch :P // compile stopwatch :P
$time = round(microtime(true) - START_TIME, 3); $time = round(microtime(true) - START_TIME, 3);
logger()->info(''); logger()->info('');
logger()->info(' Build complete, used ' . $time . ' s !'); logger()->info(' Build complete, used ' . $time . ' s !');
logger()->info(''); logger()->info('');
// ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ----------
$build_root_path = BUILD_ROOT_PATH; $build_root_path = BUILD_ROOT_PATH;
$cwd = getcwd(); $cwd = getcwd();
$fixed = ''; $fixed = '';
if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) {
str_replace($cwd, '', $build_root_path); str_replace($cwd, '', $build_root_path);
$build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path); $build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path);
$fixed = ' (host system)'; $fixed = ' (host system)';
} }
if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
$win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : ''; $win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : '';
$path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}"); $path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}");
logger()->info("Static php binary path{$fixed}: {$path}"); logger()->info("Static php binary path{$fixed}: {$path}");
} }
if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
$path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx"); $path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx");
logger()->info("phpmicro binary path{$fixed}: {$path}"); logger()->info("phpmicro binary path{$fixed}: {$path}");
} }
if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') { if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') {
$path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm"); $path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm");
logger()->info("Static php-fpm binary path{$fixed}: {$path}"); logger()->info("Static php-fpm binary path{$fixed}: {$path}");
} }
if (!empty($shared_extensions)) { if (!empty($shared_extensions)) {
foreach ($shared_extensions as $ext) { foreach ($shared_extensions as $ext) {
$path = FileSystem::convertPath("{$build_root_path}/modules/{$ext}.so"); $path = FileSystem::convertPath("{$build_root_path}/modules/{$ext}.so");
if (file_exists(BUILD_MODULES_PATH . "/{$ext}.so")) { if (file_exists(BUILD_MODULES_PATH . "/{$ext}.so")) {
logger()->info("Shared extension [{$ext}] path{$fixed}: {$path}"); logger()->info("Shared extension [{$ext}] path{$fixed}: {$path}");
} else { } else {
logger()->warning("Shared extension [{$ext}] not found, please check!"); 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;
} }
/** /**

View File

@@ -4,9 +4,6 @@ declare(strict_types=1);
namespace SPC\command; namespace SPC\command;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Downloader; use SPC\store\Downloader;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\LockFile; use SPC\store\LockFile;
@@ -36,57 +33,49 @@ class DeleteDownloadCommand extends BaseCommand
public function handle(): int public function handle(): int
{ {
try { // get source list that will be downloaded
// get source list that will be downloaded $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources'))));
$sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); if (empty($sources)) {
if (empty($sources)) { logger()->notice('Removing downloads/ directory ...');
logger()->notice('Removing downloads/ directory ...'); FileSystem::removeDir(DOWNLOAD_PATH);
FileSystem::removeDir(DOWNLOAD_PATH); logger()->info('Removed downloads/ dir!');
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!');
return static::SUCCESS; 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;
} }
} }

View File

@@ -6,9 +6,7 @@ namespace SPC\command;
use SPC\builder\traits\UnixSystemUtilTrait; use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\DownloaderException; use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException; use SPC\exception\SPCException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\Downloader; use SPC\store\Downloader;
use SPC\store\LockFile; use SPC\store\LockFile;
@@ -87,177 +85,169 @@ class DownloadCommand extends BaseCommand
public function handle(): int public function handle(): int
{ {
try { if ($this->getOption('clean')) {
if ($this->getOption('clean')) { return $this->_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="(?<file>openssl-(?<version>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;
} }
// --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="(?<file>openssl-(?<version>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 private function downloadFromZip(string $path): int
@@ -276,28 +266,24 @@ class DownloadCommand extends BaseCommand
} }
// unzip command check // unzip command check
if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) { if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) {
logger()->critical('Missing unzip command, you need to install it first !'); $this->output->writeln('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('You can use "bin/spc doctor" command to check and install required tools');
return static::FAILURE; return static::FAILURE;
} }
// create downloads // create downloads
try { if (PHP_OS_FAMILY === 'Windows') {
if (PHP_OS_FAMILY !== 'Windows') { // Windows TODO
$abs_path = realpath($path); $this->output->writeln('<error>Windows currently does not support --from-zip !</error>');
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());
return static::FAILURE; 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('<error>.lock.json not exist in "downloads/", please run "bin/spc download" first !</error>');
return static::FAILURE;
}
$this->output->writeln('<info>Extract success</info>');
return static::SUCCESS; return static::SUCCESS;
} }

View File

@@ -5,9 +5,6 @@ declare(strict_types=1);
namespace SPC\command; namespace SPC\command;
use SPC\builder\traits\UnixSystemUtilTrait; use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\PackageManager; use SPC\store\PackageManager;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
@@ -30,66 +27,58 @@ class InstallPkgCommand extends BaseCommand
public function handle(): int public function handle(): int
{ {
try { // Use shallow-clone can reduce git resource download
// Use shallow-clone can reduce git resource download if ($this->getOption('shallow-clone')) {
if ($this->getOption('shallow-clone')) { define('GIT_SHALLOW_CLONE', true);
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;
} }
// 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;
} }
} }

View File

@@ -7,10 +7,7 @@ namespace SPC\command\dev;
use SPC\builder\BuilderProvider; use SPC\builder\BuilderProvider;
use SPC\builder\LibraryBase; use SPC\builder\LibraryBase;
use SPC\command\BuildCommand; use SPC\command\BuildCommand;
use SPC\exception\ExceptionHandler; use SPC\exception\ValidationException;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\LockFile; use SPC\store\LockFile;
@@ -30,120 +27,110 @@ class PackLibCommand extends BuildCommand
public function handle(): int public function handle(): int
{ {
try { $lib_name = $this->getArgument('library');
$lib_name = $this->getArgument('library'); $builder = BuilderProvider::makeBuilderByInput($this->input);
$builder = BuilderProvider::makeBuilderByInput($this->input); $builder->setLibsOnly();
$builder->setLibsOnly(); $libraries = DependencyUtil::getLibs([$lib_name]);
$libraries = DependencyUtil::getLibs([$lib_name]); logger()->info('Building libraries: ' . implode(',', $libraries));
logger()->info('Building libraries: ' . implode(',', $libraries)); sleep(2);
sleep(2);
FileSystem::createDir(WORKING_DIR . '/dist'); FileSystem::createDir(WORKING_DIR . '/dist');
$builder->proveLibs($libraries); $builder->proveLibs($libraries);
$builder->validateLibsAndExts(); $builder->validateLibsAndExts();
// before pack, check if the dependency tree contains lib-suggests // before pack, check if the dependency tree contains lib-suggests
foreach ($libraries as $lib) { foreach ($libraries as $lib) {
if (Config::getLib($lib, 'lib-suggests', []) !== []) { if (Config::getLib($lib, 'lib-suggests', []) !== []) {
logger()->critical("The library {$lib} has lib-suggests, packing [{$lib_name}] is not safe, abort !"); 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; 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 = []; // patch pkg-config and la files with absolute path
// get pack placehoder defines foreach ($increase_files as $file) {
$placehoder = get_pack_replace(); if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) {
$content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file);
foreach ($builder->getLibs() as $lib) { $origin_files[$file] = $content;
if ($lib->getName() !== $lib_name) { // replace relative paths with absolute paths
// other dependencies: install or build, both ok $content = str_replace(
$lib->setup(); array_keys($placehoder),
} else { array_values($placehoder),
// Get lock info $content
$source = Config::getLib($lib->getName(), 'source'); );
if (($lock = LockFile::get($source)) === null || ($lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT)) { FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content);
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);
// 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) { // add .spc-extract-placeholder.json in BUILD_ROOT_PATH
// restore original files $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json';
if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT));
FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); $increase_files[] = '.spc-extract-placeholder.json';
}
}
$time = round(microtime(true) - START_TIME, 3); // every file mapped with BUILD_ROOT_PATH
logger()->info('Build libs complete, used ' . $time . ' s !'); // get BUILD_ROOT_PATH last dir part
return static::SUCCESS; $buildroot_part = basename(BUILD_ROOT_PATH);
} catch (\Throwable $e) { $increase_files = array_map(fn ($file) => $buildroot_part . '/' . $file, $increase_files);
if ($this->getOption('debug')) { // write list to packlib_files.txt
ExceptionHandler::getInstance()->handle($e); FileSystem::writeFile(WORKING_DIR . '/packlib_files.txt', implode("\n", $increase_files));
} else { // pack
logger()->critical('Build failed with ' . get_class($e) . ': ' . $e->getMessage()); $filename = Config::getPreBuilt('match-pattern');
logger()->critical('Please check with --debug option to see more details.'); $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 private function sanityCheckLib(LibraryBase $lib): void
@@ -152,7 +139,10 @@ class PackLibCommand extends BuildCommand
// config // config
foreach ($lib->getStaticLibs() as $static_lib) { foreach ($lib->getStaticLibs() as $static_lib) {
if (!file_exists(FileSystem::convertPath(BUILD_LIB_PATH . '/' . $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"
);
} }
} }
} }

View File

@@ -4,24 +4,167 @@ declare(strict_types=1);
namespace SPC\exception; namespace SPC\exception;
use ZM\Logger\ConsoleColor;
class ExceptionHandler 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) { // XXX error: yyy
self::$obj = new self(); $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() . ')'); $class = get_class($e);
logger()->error($e->getTraceAsString()); self::logError("Unhandled exception {$class}: {$e->getMessage()}\n\t{$e->getMessage()}\n");
logger()->critical('You can report this exception to static-php-cli GitHub repo.'); 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;
}
} }
} }

View File

@@ -9,7 +9,6 @@ use SPC\builder\BuilderBase;
use SPC\builder\BuilderProvider; use SPC\builder\BuilderProvider;
use SPC\builder\Extension; use SPC\builder\Extension;
use SPC\builder\LibraryBase; use SPC\builder\LibraryBase;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\LockFile; use SPC\store\LockFile;
@@ -88,7 +87,7 @@ class BuilderTest extends TestCase
if ($cnt !== 0) { if ($cnt !== 0) {
$this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID()); $this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID());
} else { } else {
$this->expectException(RuntimeException::class); $this->expectException(WrongUsageException::class);
$this->builder->getPHPVersionID(); $this->builder->getPHPVersionID();
} }
} else { } else {
@@ -105,7 +104,7 @@ class BuilderTest extends TestCase
if ($cnt !== 0) { if ($cnt !== 0) {
$this->assertEquals($match[1], $this->builder->getPHPVersion()); $this->assertEquals($match[1], $this->builder->getPHPVersion());
} else { } else {
$this->expectException(RuntimeException::class); $this->expectException(WrongUsageException::class);
$this->builder->getPHPVersion(); $this->builder->getPHPVersion();
} }
} else { } else {
@@ -246,7 +245,7 @@ class BuilderTest extends TestCase
public function testEmitPatchPointNotExists() public function testEmitPatchPointNotExists()
{ {
$this->expectOutputRegex('/failed to run/'); $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->setOption('with-added-patch', ['/tmp/patch-point.not_exsssists.php']);
$this->builder->emitPatchPoint('not-exists'); $this->builder->emitPatchPoint('not-exists');
} }