Skip to content

Enhance exception handling by binding builder and extra info to exception handler #853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ext-matrix-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
- zlib
- zstd
php-version:
- "git"
- "8.5"
operating-system:
- "ubuntu-latest"
#- "macos-13"
Expand Down
3 changes: 3 additions & 0 deletions src/SPC/builder/BuilderBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ abstract public function buildPHP(int $build_target = BUILD_TARGET_NONE);
*/
abstract public function testPHP(int $build_target = BUILD_TARGET_NONE);

/**
* Build shared extensions.
*/
public function buildSharedExts(): void
{
$lines = file(BUILD_BIN_PATH . '/php-config');
Expand Down
5 changes: 5 additions & 0 deletions src/SPC/builder/BuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use SPC\builder\linux\LinuxBuilder;
use SPC\builder\macos\MacOSBuilder;
use SPC\builder\windows\WindowsBuilder;
use SPC\exception\ExceptionHandler;
use SPC\exception\WrongUsageException;
use Symfony\Component\Console\Input\InputInterface;

Expand All @@ -29,6 +30,10 @@ public static function makeBuilderByInput(InputInterface $input): BuilderBase
'BSD' => new BSDBuilder($input->getOptions()),
default => throw new WrongUsageException('Current OS "' . PHP_OS_FAMILY . '" is not supported yet'),
};

// bind the builder to ExceptionHandler
ExceptionHandler::bindBuilder(self::$builder);

return self::$builder;
}

Expand Down
7 changes: 4 additions & 3 deletions src/SPC/command/BuildPHPCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace SPC\command;

use SPC\builder\BuilderProvider;
use SPC\exception\SPCException;
use SPC\exception\ExceptionHandler;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
Expand Down Expand Up @@ -165,8 +165,9 @@ public function handle(): int
}
$this->printFormatInfo($this->getDefinedEnvs(), true);
$this->printFormatInfo($indent_texts);
// bind extra info to SPCException
SPCException::bindBuildPHPExtraInfo($indent_texts);

// bind extra info to exception handler
ExceptionHandler::bindBuildPhpExtraInfo($indent_texts);

logger()->notice('Build will start after 2s ...');
sleep(2);
Expand Down
151 changes: 105 additions & 46 deletions src/SPC/exception/ExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

namespace SPC\exception;

use SPC\builder\BuilderBase;
use SPC\builder\freebsd\BSDBuilder;
use SPC\builder\linux\LinuxBuilder;
use SPC\builder\macos\MacOSBuilder;
use SPC\builder\windows\WindowsBuilder;
use ZM\Logger\ConsoleColor;

class ExceptionHandler
Expand All @@ -26,21 +31,27 @@ class ExceptionHandler
WrongUsageException::class,
];

/** @var null|BuilderBase Builder binding */
private static ?BuilderBase $builder = null;

/** @var array<string, mixed> Build PHP extra info binding */
private static array $build_php_extra_info = [];

public static function handleSPCException(SPCException $e): void
{
// 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()}",
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()}",
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()}",
default => "Unknown SPC exception {$class}: {$e->getMessage()}",
};
self::logError($head_msg);

Expand All @@ -54,25 +65,32 @@ public static function handleSPCException(SPCException $e): void
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']}"));
if ($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"));
self::logError('Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder"));
} elseif (self::$builder) {
$os = match (get_class(self::$builder)) {
WindowsBuilder::class => 'Windows',
MacOSBuilder::class => 'macOS',
LinuxBuilder::class => 'Linux',
BSDBuilder::class => 'FreeBSD',
default => 'Unknown OS',
};
self::logError('Failed module: ' . ConsoleColor::yellow("Builder for {$os}"));
} elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) {
self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class));
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()));
self::logError('');
self::logError('Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand()));
if ($cd = $e->getCd()) {
self::logError('Command executed in: ' . ConsoleColor::yellow($cd));
self::logError('Command executed in: ' . ConsoleColor::yellow($cd));
}
if ($env = $e->getEnv()) {
self::logError('Command inline env variables:');
self::logError('Command inline env variables:');
foreach ($env as $k => $v) {
self::logError(ConsoleColor::yellow("{$k}={$v}"), 4);
}
Expand All @@ -81,46 +99,40 @@ public static function handleSPCException(SPCException $e): void

// validation error
if ($e instanceof ValidationException) {
self::logError('Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString()));
self::logError('Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString()));
}

// environment error
if ($e instanceof EnvironmentException) {
self::logError('Failed environment check: ' . ConsoleColor::yellow($e->getMessage()));
self::logError('Failed environment check: ' . ConsoleColor::yellow($e->getMessage()));
if (($solution = $e->getSolution()) !== null) {
self::logError('Solution: ' . ConsoleColor::yellow($solution));
self::logError('Solution: ' . ConsoleColor::yellow($solution));
}
}

// get patch info
if ($e instanceof PatchException) {
self::logError("Failed patch module: {$e->getPatchModule()}");
self::logError("Failed patch module: {$e->getPatchModule()}");
}

// get internal trace
if ($e instanceof SPCInternalException) {
self::logError('Internal trace:');
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);
}
}
}
if ($info = ExceptionHandler::$build_php_extra_info) {
self::logError('', output_log: defined('DEBUG_MODE'));
self::logError('Build PHP extra info:', output_log: defined('DEBUG_MODE'));
self::printArrayInfo($info);
}

// get the full builder options if possible
if ($e->getBuildPHPInfo()) {
$info = $e->getBuildPHPInfo();
self::logError('', output_log: defined('DEBUG_MODE'));
self::logError('Builder function: ' . ConsoleColor::yellow($info['builder_function']), output_log: defined('DEBUG_MODE'));
}

self::logError("\n----------------------------------------\n");
Expand All @@ -142,20 +154,67 @@ public static function handleSPCException(SPCException $e): void
public static function handleDefaultException(\Throwable $e): void
{
$class = get_class($e);
self::logError("Unhandled exception {$class}: {$e->getMessage()}\n\t{$e->getMessage()}\n");
self::logError("Unhandled exception {$class}:\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');
self::logError(ConsoleColor::gray($e->getTraceAsString()) . PHP_EOL, 4);
self::logError('⚠ Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues');
}

public static function bindBuilder(?BuilderBase $bind_builder): void
{
self::$builder = $bind_builder;
}

private static function logError($message, int $indent_space = 0): void
public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void
{
self::$build_php_extra_info = $build_php_extra_info;
}

private static function logError($message, int $indent_space = 0, bool $output_log = true): 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;
if ($output_log) {
echo ConsoleColor::red($line) . PHP_EOL;
}
}
}

/**
* Print array info to console and log.
*/
private static function printArrayInfo(array $info): void
{
$log_output = defined('DEBUG_MODE');
$maxlen = 0;
foreach ($info as $k => $v) {
$maxlen = max(strlen($k), $maxlen);
}
foreach ($info as $k => $v) {
if (is_string($v)) {
if ($v === '') {
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('""'), 4, $log_output);
} else {
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v), 4, $log_output);
}
} elseif (is_array($v) && !is_assoc_array($v)) {
if ($v === []) {
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('[]'), 4, $log_output);
continue;
}
$first = array_shift($v);
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first), 4, $log_output);
foreach ($v as $vs) {
self::logError(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs), 4, $log_output);
}
} elseif (is_bool($v) || is_null($v)) {
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::cyan($v === true ? 'true' : ($v === false ? 'false' : 'null')), 4, $log_output);
} else {
self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow(json_encode($v, JSON_PRETTY_PRINT)), 4, $log_output);
}
}
}
}
37 changes: 4 additions & 33 deletions src/SPC/exception/SPCException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@
namespace SPC\exception;

use SPC\builder\BuilderBase;
use SPC\builder\freebsd\BSDBuilder;
use SPC\builder\freebsd\library\BSDLibraryBase;
use SPC\builder\LibraryBase;
use SPC\builder\linux\library\LinuxLibraryBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\builder\macos\library\MacOSLibraryBase;
use SPC\builder\macos\MacOSBuilder;
use SPC\builder\windows\library\WindowsLibraryBase;
use SPC\builder\windows\WindowsBuilder;

/**
* Base class for SPC exceptions.
Expand All @@ -24,8 +20,6 @@
*/
abstract class SPCException extends \Exception
{
private static ?array $build_php_extra_info = null;

private ?array $library_info = null;

private ?array $extension_info = null;
Expand All @@ -40,11 +34,6 @@ public function __construct(string $message = '', int $code = 0, ?\Throwable $pr
$this->loadStackTraceInfo();
}

public static function bindBuildPHPExtraInfo(array $indent_texts): void
{
self::$build_php_extra_info = $indent_texts;
}

public function bindExtensionInfo(array $extension_info): void
{
$this->extension_info = $extension_info;
Expand All @@ -55,11 +44,6 @@ public function addExtraLogFile(string $key, string $filename): void
$this->extra_log_files[$key] = $filename;
}

public function getBuildPHPExtraInfo(): ?array
{
return self::$build_php_extra_info;
}

/**
* Returns an array containing information about the SPC module.
*
Expand All @@ -82,8 +66,7 @@ public function getLibraryInfo(): ?array
* Returns an array containing information about the PHP build process.
*
* @return null|array{
* builder_class: string,
* os: string,
* builder_function: string,
* file: null|string,
* line: null|int,
* } an array containing PHP build information
Expand Down Expand Up @@ -124,7 +107,7 @@ private function loadStackTraceInfo(): void
}

// Check if the class is a subclass of LibraryBase
if (!$this->library_info && is_subclass_of($frame['class'], LibraryBase::class)) {
if (!$this->library_info && is_a($frame['class'], LibraryBase::class, true)) {
try {
$reflection = new \ReflectionClass($frame['class']);
if ($reflection->hasConstant('NAME')) {
Expand Down Expand Up @@ -152,21 +135,9 @@ private function loadStackTraceInfo(): void
}

// Check if the class is a subclass of BuilderBase and the method is buildPHP
if (!$this->build_php_info && is_subclass_of($frame['class'], BuilderBase::class) && $frame['function'] === 'buildPHP') {
$reflection = new \ReflectionClass($frame['class']);
if ($reflection->hasProperty('options')) {
$options = $reflection->getProperty('options')->getValue();
}
if (!$this->build_php_info && is_a($frame['class'], BuilderBase::class, true)) {
$this->build_php_info = [
'builder_class' => $frame['class'],
'builder_options' => $options ?? [],
'os' => match (true) {
is_a($frame['class'], BSDBuilder::class, true) => 'BSD',
is_a($frame['class'], LinuxBuilder::class, true) => 'Linux',
is_a($frame['class'], MacOSBuilder::class, true) => 'macOS',
is_a($frame['class'], WindowsBuilder::class, true) => 'Windows',
default => 'Unknown',
},
'builder_function' => $frame['function'],
'file' => $frame['file'] ?? null,
'line' => $frame['line'] ?? null,
];
Expand Down
Loading
Loading