From bbab685247c9592fb5786650fcb218e8641a16d7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 15 Feb 2026 21:58:42 +0800 Subject: [PATCH] Refactor exception handler to v3, use standard shell exitcode --- bin/spc | 6 +- src/StaticPHP/Command/BaseCommand.php | 11 +- src/StaticPHP/Command/Dev/EnvCommand.php | 2 +- .../Command/Dev/IsInstalledCommand.php | 2 +- .../Command/Dev/LintConfigCommand.php | 4 +- src/StaticPHP/Command/Dev/ShellCommand.php | 2 +- src/StaticPHP/Command/DoctorCommand.php | 2 +- src/StaticPHP/Command/DumpLicenseCommand.php | 6 +- src/StaticPHP/Command/ExtractCommand.php | 2 +- src/StaticPHP/Command/ReturnCode.php | 42 +++ src/StaticPHP/Command/SPCConfigCommand.php | 2 +- src/StaticPHP/Exception/ExceptionHandler.php | 262 +++++++++++------- .../Exception/ExecutionException.php | 4 +- src/StaticPHP/Exception/SPCException.php | 188 +++++++++---- .../Exception/ValidationException.php | 8 - src/StaticPHP/Package/Package.php | 29 +- src/StaticPHP/Package/PackageBuilder.php | 39 ++- 17 files changed, 400 insertions(+), 211 deletions(-) create mode 100644 src/StaticPHP/Command/ReturnCode.php diff --git a/bin/spc b/bin/spc index 9bac188a..622dda86 100755 --- a/bin/spc +++ b/bin/spc @@ -21,10 +21,8 @@ if (PHP_VERSION_ID < 80400) { try { (new ConsoleApplication())->run(); } catch (SPCException $e) { - ExceptionHandler::handleSPCException($e); - exit(1); + exit(ExceptionHandler::handleSPCException($e)); } catch (\Throwable $e) { - ExceptionHandler::handleDefaultException($e); - exit(1); + exit(ExceptionHandler::handleDefaultException($e)); } diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index 02f84ffb..ddcb3671 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -14,6 +14,8 @@ use ZM\Logger\ConsoleColor; abstract class BaseCommand extends Command { + use ReturnCode; + /** * The message of the day (MOTD) displayed when the command is run. * You can customize this to show your application's name and version if you are using SPC in vendor mode. @@ -101,12 +103,10 @@ abstract class BaseCommand extends Command return $this->handle(); } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { // Handle SPCException and log it - ExceptionHandler::handleSPCException($e); - return static::FAILURE; + return ExceptionHandler::handleSPCException($e); } catch (\Throwable $e) { // Handle any other exceptions - ExceptionHandler::handleDefaultException($e); - return static::FAILURE; + return ExceptionHandler::handleDefaultException($e); } } @@ -129,7 +129,8 @@ abstract class BaseCommand extends Command // Don't show commit ID when running in phar if (\Phar::running()) { - return $version; + $stable = file_exists(ROOT_DIR . '/src/.release') ? 'stable' : 'unstable'; + return "{$version} ({$stable})"; } $commitId = $this->getGitCommitShortId(); diff --git a/src/StaticPHP/Command/Dev/EnvCommand.php b/src/StaticPHP/Command/Dev/EnvCommand.php index 160504ed..96d9409b 100644 --- a/src/StaticPHP/Command/Dev/EnvCommand.php +++ b/src/StaticPHP/Command/Dev/EnvCommand.php @@ -29,7 +29,7 @@ class EnvCommand extends BaseCommand $env = $this->getArgument('env'); if (($val = getenv($env)) === false) { $this->output->writeln("Environment variable '{$env}' is not set."); - return static::FAILURE; + return static::USER_ERROR; } if (is_array($val)) { foreach ($val as $k => $v) { diff --git a/src/StaticPHP/Command/Dev/IsInstalledCommand.php b/src/StaticPHP/Command/Dev/IsInstalledCommand.php index a3f69321..c5dfc498 100644 --- a/src/StaticPHP/Command/Dev/IsInstalledCommand.php +++ b/src/StaticPHP/Command/Dev/IsInstalledCommand.php @@ -29,6 +29,6 @@ class IsInstalledCommand extends BaseCommand return static::SUCCESS; } $this->output->writeln("Package [{$package}] is not installed."); - return static::FAILURE; + return static::USER_ERROR; } } diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index d0e4cfa1..1efba4d5 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -34,7 +34,7 @@ class LintConfigCommand extends BaseCommand if ($checkOnly && $hasChanges) { $this->output->writeln('Some config files need sorting. Run "bin/spc dev:lint-config" to fix them.'); - return static::FAILURE; + return static::VALIDATION_ERROR; } return static::SUCCESS; @@ -125,7 +125,7 @@ class LintConfigCommand extends BaseCommand return false; } ksort($data); - foreach ($data as $artifact_name => &$config) { + foreach ($data as &$config) { uksort($config, $config_type === 'artifact' ? [$this, 'artifactSortKey'] : [$this, 'packageSortKey']); } unset($config); diff --git a/src/StaticPHP/Command/Dev/ShellCommand.php b/src/StaticPHP/Command/Dev/ShellCommand.php index 560cc7fe..103300b5 100644 --- a/src/StaticPHP/Command/Dev/ShellCommand.php +++ b/src/StaticPHP/Command/Dev/ShellCommand.php @@ -28,6 +28,6 @@ class ShellCommand extends BaseCommand return $code; } $this->output->writeln('Unsupported OS for shell command.'); - return static::FAILURE; + return static::ENVIRONMENT_ERROR; } } diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index cd90cd94..6ae6d68a 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -30,6 +30,6 @@ class DoctorCommand extends BaseCommand return static::SUCCESS; } - return static::FAILURE; + return static::ENVIRONMENT_ERROR; } } diff --git a/src/StaticPHP/Command/DumpLicenseCommand.php b/src/StaticPHP/Command/DumpLicenseCommand.php index d90ecbf9..2ba4d002 100644 --- a/src/StaticPHP/Command/DumpLicenseCommand.php +++ b/src/StaticPHP/Command/DumpLicenseCommand.php @@ -71,7 +71,7 @@ class DumpLicenseCommand extends BaseCommand $this->output->writeln(' - --for-extensions: dump-license --for-extensions=openssl,mbstring'); $this->output->writeln(' - --for-libs: dump-license --for-libs=openssl,zlib'); $this->output->writeln(' - --for-packages: dump-license --for-packages=php,libssl'); - return self::FAILURE; + return static::USER_ERROR; } // Deduplicate artifacts @@ -90,11 +90,11 @@ class DumpLicenseCommand extends BaseCommand InteractiveTerm::success('Licenses dumped successfully: ' . $dump_dir); // $this->output->writeln("✓ Successfully dumped licenses to: {$dump_dir}"); // $this->output->writeln(" Total artifacts: " . count($artifacts_to_dump) . ''); - return self::SUCCESS; + return static::SUCCESS; } $this->output->writeln('Failed to dump licenses'); - return self::FAILURE; + return static::INTERNAL_ERROR; } /** diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index 14951a34..e2e79bdf 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -47,7 +47,7 @@ class ExtractCommand extends BaseCommand $artifact = ArtifactLoader::getArtifactInstance($name); if ($artifact === null) { $this->output->writeln("Artifact '{$name}' not found."); - return static::FAILURE; + return static::USER_ERROR; } $artifacts[$name] = $artifact; } diff --git a/src/StaticPHP/Command/ReturnCode.php b/src/StaticPHP/Command/ReturnCode.php new file mode 100644 index 00000000..d152101e --- /dev/null +++ b/src/StaticPHP/Command/ReturnCode.php @@ -0,0 +1,42 @@ + "{$config['cflags']} {$config['ldflags']} {$config['libs']}", }); - return 0; + return static::SUCCESS; } } diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 53dc15a8..20cf9395 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -4,15 +4,15 @@ declare(strict_types=1); namespace StaticPHP\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 StaticPHP\Command\BaseCommand; use StaticPHP\DI\ApplicationContext; use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; +/** + * Exception handler for StaticPHP. + * Provides centralized exception handling for the Package-based architecture. + */ class ExceptionHandler { public const array KNOWN_EXCEPTIONS = [ @@ -35,28 +35,25 @@ class ExceptionHandler RegistryException::class, ]; - /** @var null|BuilderBase Builder binding */ - private static ?BuilderBase $builder = null; - /** @var array Build PHP extra info binding */ private static array $build_php_extra_info = []; - public static function handleSPCException(SPCException $e): void + public static function handleSPCException(SPCException $e): int { // XXX error: yyy $head_msg = match ($class = get_class($e)) { - BuildFailureException::class => "✗ Build failed: {$e->getMessage()}", - DownloaderException::class => "✗ Download failed: {$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()}", + 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()}", + PatchException::class => "✘ Patch apply failed: {$e->getMessage()}", + SPCInternalException::class => "✘ SPC internal error: {$e->getMessage()}", ValidationException::class => "⚠ Validation failed: {$e->getMessage()}", WrongUsageException::class => $e->getMessage(), - RegistryException::class => "✗ Registry parsing error: {$e->getMessage()}", - default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}", + RegistryException::class => "✘ Registry error: {$e->getMessage()}", + default => "✘ Unknown SPC exception {$class}: {$e->getMessage()}", }; self::logError($head_msg); @@ -64,83 +61,10 @@ class ExceptionHandler $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); if ($minor_logs) { - return; + return self::getReturnCode($e); } - self::logError("----------------------------------------\n"); - - // get the SPCException module - 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")); - } 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)); - } - - // get command execution info - if ($e instanceof ExecutionException) { - self::logError(''); - 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 = ExceptionHandler::$build_php_extra_info) { - self::logError('', output_log: ApplicationContext::isDebug()); - self::logError('Build PHP extra info:', output_log: ApplicationContext::isDebug()); - self::printArrayInfo($info); - } - - // get the full builder options if possible - if ($e->getBuildPHPInfo()) { - $info = $e->getBuildPHPInfo(); - self::logError('', output_log: ApplicationContext::isDebug()); - self::logError('Builder function: ' . ConsoleColor::yellow($info['builder_function']), output_log: ApplicationContext::isDebug()); - } - - self::logError("\n----------------------------------------\n"); + self::printModuleErrorInfo($e); // convert log file path if in docker $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); @@ -153,28 +77,25 @@ class ExceptionHandler } if ($e->getExtraLogFiles() !== []) { foreach ($e->getExtraLogFiles() as $key => $file) { - self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::cyan("{$spc_logs_dir_convert}/{$file}")); + self::logError('⚠ Log file ' . ConsoleColor::cyan($key) . ' is saved in: ' . ConsoleColor::cyan("{$spc_logs_dir_convert}/{$file}")); } } if (!ApplicationContext::isDebug()) { - self::logError('⚠ If you want to see more details in console, use `--debug` option.'); + self::logError('⚠ If you want to see more details in console, use `-vvv` option.'); } + return self::getReturnCode($e); } - public static function handleDefaultException(\Throwable $e): void + public static function handleDefaultException(\Throwable $e): int { $class = get_class($e); $file = $e->getFile(); $line = $e->getLine(); - self::logError("✗ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); + self::logError("✘ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); self::logError('Stack trace:'); 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; + return self::getReturnCode($e); } public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void @@ -182,7 +103,22 @@ class ExceptionHandler self::$build_php_extra_info = $build_php_extra_info; } - private static function logError($message, int $indent_space = 0, bool $output_log = true): void + private static function getReturnCode(\Throwable $e): int + { + return match (get_class($e)) { + BuildFailureException::class, ExecutionException::class => BaseCommand::BUILD_ERROR, + DownloaderException::class => BaseCommand::DOWNLOAD_ERROR, + EnvironmentException::class => BaseCommand::ENVIRONMENT_ERROR, + FileSystemException::class => BaseCommand::FILE_SYSTEM_ERROR, + InterruptException::class => BaseCommand::INTERRUPT_SIGNAL, + PatchException::class => BaseCommand::PATCH_ERROR, + ValidationException::class => BaseCommand::VALIDATION_ERROR, + WrongUsageException::class => BaseCommand::USER_ERROR, + default => BaseCommand::INTERNAL_ERROR, + }; + } + + private static function logError($message, int $indent_space = 0, bool $output_log = true, string $color = 'red'): void { $spc_log = fopen(SPC_OUTPUT_LOG, 'a'); $msg = explode("\n", (string) $message); @@ -190,7 +126,7 @@ class ExceptionHandler $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { - InteractiveTerm::plain(ConsoleColor::red($line) . '', 'error'); + InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error'); } } } @@ -229,4 +165,124 @@ class ExceptionHandler } } } + + private static function printModuleErrorInfo(SPCException $e): void + { + $class = get_class($e); + self::logError("\n-------------------- " . ConsoleColor::red('Module error info') . ' --------------------', color: 'default'); + + $has_info = false; + + // Get Package information + if ($package_info = $e->getPackageInfo()) { + $type_label = match ($package_info['package_type']) { + 'library' => 'Library Package', + 'php-extension' => 'PHP Extension Package', + 'target' => 'Target Package', + default => 'Package', + }; + self::logError('Failed module: ' . ConsoleColor::gray("{$type_label} '{$package_info['package_name']}'")); + if ($package_info['file'] && $package_info['line']) { + self::logError('Package location: ' . ConsoleColor::gray("{$package_info['file']}:{$package_info['line']}")); + } + $has_info = true; + } + + // Get Stage information (can be displayed together with Package info) + $stage_stack = $e->getStageStack(); + if (!empty($stage_stack)) { + // Build stage call chain: innermost -> ... -> outermost + $stage_names = array_reverse(array_column($stage_stack, 'stage_name')); + $stage_chain = implode(' -> ', $stage_names); + + if (count($stage_names) > 1) { + self::logError('Failed stage: ' . ConsoleColor::gray($stage_chain)); + } else { + self::logError('Failed stage: ' . ConsoleColor::gray($stage_names[0])); + } + + // Show context keys of the innermost (actual failing) stage + $innermost_stage = $stage_stack[0]; + if (!empty($innermost_stage['context_keys'])) { + self::logError('Stage context keys: ' . ConsoleColor::gray(implode(', ', $innermost_stage['context_keys']))); + } + $has_info = true; + } + + // Get PackageBuilder information + if (!$has_info && ($builder_info = $e->getPackageBuilderInfo())) { + self::logError('Failed module: ' . ConsoleColor::gray('PackageBuilder')); + if ($builder_info['method']) { + self::logError('Builder method: ' . ConsoleColor::gray($builder_info['method'])); + } + if ($builder_info['file'] && $builder_info['line']) { + self::logError('Builder location: ' . ConsoleColor::gray("{$builder_info['file']}:{$builder_info['line']}")); + } + $has_info = true; + } + + // Get PackageInstaller information + if (!$has_info && ($installer_info = $e->getPackageInstallerInfo())) { + self::logError('Failed module: ' . ConsoleColor::gray('PackageInstaller')); + if ($installer_info['method']) { + self::logError('Installer method: ' . ConsoleColor::gray($installer_info['method'])); + } + if ($installer_info['file'] && $installer_info['line']) { + self::logError('Installer location: ' . ConsoleColor::gray("{$installer_info['file']}:{$installer_info['line']}")); + } + $has_info = true; + } + + if (!$has_info && !in_array($class, self::KNOWN_EXCEPTIONS)) { + self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); + } + + // get command execution info + if ($e instanceof ExecutionException) { + self::logError(''); + self::logError('Failed command: ' . ConsoleColor::gray($e->getExecutionCommand())); + if ($cd = $e->getCd()) { + self::logError(' - Command executed in: ' . ConsoleColor::gray($cd)); + } + if ($env = $e->getEnv()) { + self::logError(' - Command inline env variables:'); + foreach ($env as $k => $v) { + self::logError(ConsoleColor::gray("{$k}={$v}"), 6); + } + } + } + + // validation error + if ($e instanceof ValidationException) { + self::logError('Failed validation module: ' . ConsoleColor::gray($e->getValidationModuleString())); + } + + // environment error + if ($e instanceof EnvironmentException) { + self::logError('Failed environment check: ' . ConsoleColor::gray($e->getMessage())); + if (($solution = $e->getSolution()) !== null) { + self::logError('Solution: ' . ConsoleColor::gray($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 = ExceptionHandler::$build_php_extra_info) { + self::logError('', output_log: ApplicationContext::isDebug()); + self::logError('Build PHP extra info:', output_log: ApplicationContext::isDebug()); + self::printArrayInfo($info); + } + + self::logError("---------------------------------------------------------\n", color: 'none'); + } } diff --git a/src/StaticPHP/Exception/ExecutionException.php b/src/StaticPHP/Exception/ExecutionException.php index 3fc4c67f..2cb62a2e 100644 --- a/src/StaticPHP/Exception/ExecutionException.php +++ b/src/StaticPHP/Exception/ExecutionException.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace StaticPHP\Exception; -use SPC\util\shell\UnixShell; -use SPC\util\shell\WindowsCmd; +use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\Shell\WindowsCmd; /** * Exception thrown when an error occurs during execution of shell command. diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index 1859dd1f..b989f7d9 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace StaticPHP\Exception; -use SPC\builder\BuilderBase; -use SPC\builder\freebsd\library\BSDLibraryBase; -use SPC\builder\LibraryBase; -use SPC\builder\linux\library\LinuxLibraryBase; -use SPC\builder\macos\library\MacOSLibraryBase; -use SPC\builder\windows\library\WindowsLibraryBase; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\Package; +use StaticPHP\Package\PackageBuilder; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Package\TargetPackage; /** * Base class for SPC exceptions. @@ -20,11 +20,17 @@ use SPC\builder\windows\library\WindowsLibraryBase; */ abstract class SPCException extends \Exception { - private ?array $library_info = null; + /** @var null|array Package information */ + private ?array $package_info = null; - private ?array $extension_info = null; + /** @var null|array Package builder information */ + private ?array $package_builder_info = null; - private ?array $build_php_info = null; + /** @var null|array Package installer information */ + private ?array $package_installer_info = null; + + /** @var array Stage execution call stack */ + private array $stage_stack = []; private array $extra_log_files = []; @@ -34,9 +40,38 @@ abstract class SPCException extends \Exception $this->loadStackTraceInfo(); } - public function bindExtensionInfo(array $extension_info): void + /** + * Bind package information manually. + * + * @param array $package_info Package information array + */ + public function bindPackageInfo(array $package_info): void { - $this->extension_info = $extension_info; + $this->package_info = $package_info; + } + + /** + * Add stage to the call stack. + * This builds a call chain like: build -> configure -> compile + * + * @param string $stage_name Stage name being executed + * @param array $context Stage context (optional) + */ + public function addStageToStack(string $stage_name, array $context = []): void + { + $this->stage_stack[] = [ + 'stage_name' => $stage_name, + 'context_keys' => array_keys($context), + ]; + } + + /** + * Legacy method for backward compatibility. + * @deprecated Use addStageToStack() instead + */ + public function bindStageInfo(string $stage_name, array $context = []): void + { + $this->addStageToStack($stage_name, $context); } public function addExtraLogFile(string $key, string $filename): void @@ -45,52 +80,74 @@ abstract class SPCException extends \Exception } /** - * Returns an array containing information about the SPC module. - * - * This method can be overridden by subclasses to provide specific module information. + * Returns package information. * * @return null|array{ - * library_name: string, - * library_class: string, - * os: string, + * package_name: string, + * package_type: string, + * package_class: string, * file: null|string, * line: null|int, - * } an array containing module information + * } Package information or null */ - public function getLibraryInfo(): ?array + public function getPackageInfo(): ?array { - return $this->library_info; + return $this->package_info; } /** - * Returns an array containing information about the PHP build process. + * Returns package builder information. * * @return null|array{ - * builder_function: string, * file: null|string, * line: null|int, - * } an array containing PHP build information + * method: null|string, + * } Package builder information or null */ - public function getBuildPHPInfo(): ?array + public function getPackageBuilderInfo(): ?array { - return $this->build_php_info; + return $this->package_builder_info; } /** - * Returns an array containing information about the SPC extension. - * - * This method can be overridden by subclasses to provide specific extension information. + * Returns package installer information. * * @return null|array{ - * extension_name: string, - * extension_class: string, * file: null|string, * line: null|int, - * } an array containing extension information + * method: null|string, + * } Package installer information or null */ - public function getExtensionInfo(): ?array + public function getPackageInstallerInfo(): ?array { - return $this->extension_info; + return $this->package_installer_info; + } + + /** + * Returns the stage call stack. + * + * @return array, + * }> Stage call stack (empty array if no stages) + */ + public function getStageStack(): array + { + return $this->stage_stack; + } + + /** + * Returns the innermost (actual failing) stage information. + * Legacy method for backward compatibility. + * + * @return null|array{ + * stage_name: string, + * context_keys: array, + * } Stage information or null + */ + public function getStageInfo(): ?array + { + return empty($this->stage_stack) ? null : end($this->stage_stack); } public function getExtraLogFiles(): array @@ -98,6 +155,9 @@ abstract class SPCException extends \Exception return $this->extra_log_files; } + /** + * Load stack trace information to detect Package, Builder, and Installer context. + */ private function loadStackTraceInfo(): void { $trace = $this->getTrace(); @@ -106,40 +166,48 @@ abstract class SPCException extends \Exception continue; } - // Check if the class is a subclass of LibraryBase - if (!$this->library_info && is_a($frame['class'], LibraryBase::class, true)) { + // Check if the class is a Package subclass + if (!$this->package_info && is_a($frame['class'], Package::class, true)) { try { - $reflection = new \ReflectionClass($frame['class']); - if ($reflection->hasConstant('NAME')) { - $name = $reflection->getConstant('NAME'); - if ($name !== 'unknown') { - $this->library_info = [ - 'library_name' => $name, - 'library_class' => $frame['class'], - 'os' => match (true) { - is_a($frame['class'], BSDLibraryBase::class, true) => 'BSD', - is_a($frame['class'], LinuxLibraryBase::class, true) => 'Linux', - is_a($frame['class'], MacOSLibraryBase::class, true) => 'macOS', - is_a($frame['class'], WindowsLibraryBase::class, true) => 'Windows', - default => 'Unknown', - }, - 'file' => $frame['file'] ?? null, - 'line' => $frame['line'] ?? null, - ]; - continue; - } + // Try to get package information from object if available + if (isset($frame['object']) && $frame['object'] instanceof Package) { + $package = $frame['object']; + $package_type = match (true) { + $package instanceof LibraryPackage => 'library', + $package instanceof PhpExtensionPackage => 'php-extension', + $package instanceof TargetPackage => 'target', + default => 'package', + }; + $this->package_info = [ + 'package_name' => $package->name, + 'package_type' => $package_type, + 'package_class' => $frame['class'], + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + ]; + continue; } - } catch (\ReflectionException) { - continue; + } catch (\Throwable) { + // Ignore reflection errors } } - // Check if the class is a subclass of BuilderBase and the method is buildPHP - if (!$this->build_php_info && is_a($frame['class'], BuilderBase::class, true)) { - $this->build_php_info = [ - 'builder_function' => $frame['function'], + // Check if the class is PackageBuilder + if (!$this->package_builder_info && is_a($frame['class'], PackageBuilder::class, true)) { + $this->package_builder_info = [ 'file' => $frame['file'] ?? null, 'line' => $frame['line'] ?? null, + 'method' => $frame['function'] ?? null, + ]; + continue; + } + + // Check if the class is PackageInstaller + if (!$this->package_installer_info && is_a($frame['class'], PackageInstaller::class, true)) { + $this->package_installer_info = [ + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + 'method' => $frame['function'] ?? null, ]; } } diff --git a/src/StaticPHP/Exception/ValidationException.php b/src/StaticPHP/Exception/ValidationException.php index fe0cb428..7eae9d40 100644 --- a/src/StaticPHP/Exception/ValidationException.php +++ b/src/StaticPHP/Exception/ValidationException.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace StaticPHP\Exception; -use SPC\builder\Extension; - /** * Exception thrown for validation errors in SPC. * @@ -23,12 +21,6 @@ class ValidationException extends SPCException // init validation module if ($validation_module === null) { foreach ($this->getTrace() as $trace) { - // Extension validate() => "Extension validator" - if (is_a($trace['class'] ?? null, Extension::class, true) && $trace['function'] === 'validate') { - $this->validation_module = 'Extension validator'; - break; - } - // Other => "ClassName::functionName" $this->validation_module = [ 'class' => $trace['class'] ?? null, diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index cd4f3840..64b9f2e4 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -7,6 +7,7 @@ namespace StaticPHP\Package; use StaticPHP\Artifact\Artifact; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\SPCException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Registry\PackageLoader; @@ -63,13 +64,29 @@ abstract class Package static::class => $this, ], $context); - // emit BeforeStage - $this->emitBeforeStage($name, $stageContext); + try { + // emit BeforeStage + $this->emitBeforeStage($name, $stageContext); - $ret = ApplicationContext::invoke($this->stages[$name], $stageContext); - // emit AfterStage - $this->emitAfterStage($name, $stageContext, $ret); - return $ret; + $ret = ApplicationContext::invoke($this->stages[$name], $stageContext); + // emit AfterStage + $this->emitAfterStage($name, $stageContext, $ret); + return $ret; + } catch (SPCException $e) { + // Bind package information only if not already bound + if ($e->getPackageInfo() === null) { + $e->bindPackageInfo([ + 'package_name' => $this->name, + 'package_type' => $this->type, + 'package_class' => static::class, + 'file' => null, + 'line' => null, + ]); + } + // Always add current stage to the stack to build call chain + $e->addStageToStack($name, $stageContext); + throw $e; + } } public function setOutput(string $key, string $value): static diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index d782b3e2..e50798ad 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -6,6 +6,7 @@ namespace StaticPHP\Package; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\SPCException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\Shell\Shell; @@ -59,19 +60,33 @@ class PackageBuilder InteractiveTerm::advance(); }); - if ($package->getType() !== 'virtual-target') { - // patch before build - $package->emitPatchBeforeBuild(); - } - - // build - $package->runStage('build'); - - if ($package->getType() !== 'virtual-target') { - // install license - if (($license = PackageConfig::get($package->getName(), 'license')) !== null) { - $this->installLicense($package, $license); + try { + if ($package->getType() !== 'virtual-target') { + // patch before build + $package->emitPatchBeforeBuild(); } + + // build + $package->runStage('build'); + + if ($package->getType() !== 'virtual-target') { + // install license + if (($license = PackageConfig::get($package->getName(), 'license')) !== null) { + $this->installLicense($package, $license); + } + } + } catch (SPCException $e) { + // Ensure package information is bound if not already + if ($e->getPackageInfo() === null) { + $e->bindPackageInfo([ + 'package_name' => $package->name, + 'package_type' => $package->type, + 'package_class' => get_class($package), + 'file' => null, + 'line' => null, + ]); + } + throw $e; } return SPC_STATUS_BUILT; }