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