2025-04-24 14:18:39 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace SPC\command;
|
|
|
|
|
|
|
|
|
|
use SPC\exception\ValidationException;
|
|
|
|
|
use SPC\util\ConfigValidator;
|
|
|
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
|
|
|
use Symfony\Component\Process\Process;
|
|
|
|
|
|
|
|
|
|
#[AsCommand('craft', 'Build static-php from craft.yml')]
|
2025-06-29 16:21:22 +08:00
|
|
|
class CraftCommand extends BuildCommand
|
2025-04-24 14:18:39 +08:00
|
|
|
{
|
|
|
|
|
public function configure(): void
|
|
|
|
|
{
|
|
|
|
|
$this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function handle(): int
|
|
|
|
|
{
|
|
|
|
|
$craft_file = $this->getArgument('craft');
|
|
|
|
|
// Check if the craft.yml file exists
|
|
|
|
|
if (!file_exists($craft_file)) {
|
|
|
|
|
$this->output->writeln('<error>craft.yml not found, please create one!</error>');
|
|
|
|
|
return static::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the craft.yml file is valid
|
|
|
|
|
try {
|
|
|
|
|
$craft = ConfigValidator::validateAndParseCraftFile($craft_file, $this);
|
|
|
|
|
if ($craft['debug']) {
|
|
|
|
|
$this->input->setOption('debug', true);
|
|
|
|
|
}
|
|
|
|
|
} catch (ValidationException $e) {
|
|
|
|
|
$this->output->writeln('<error>craft.yml parse error: ' . $e->getMessage() . '</error>');
|
|
|
|
|
return static::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Craft!!!
|
|
|
|
|
$this->output->writeln('<info>Crafting...</info>');
|
|
|
|
|
|
|
|
|
|
// apply env
|
|
|
|
|
if (isset($craft['extra-env'])) {
|
|
|
|
|
$env = $craft['extra-env'];
|
|
|
|
|
foreach ($env as $key => $val) {
|
|
|
|
|
f_putenv("{$key}={$val}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 11:55:55 +07:00
|
|
|
$static_extensions = implode(',', $craft['extensions']);
|
2025-06-14 02:30:00 +08:00
|
|
|
$shared_extensions = implode(',', $craft['shared-extensions'] ?? []);
|
2025-04-24 14:18:39 +08:00
|
|
|
$libs = implode(',', $craft['libs']);
|
|
|
|
|
|
|
|
|
|
// init log
|
|
|
|
|
if (file_exists(WORKING_DIR . '/craft.log')) {
|
|
|
|
|
unlink(WORKING_DIR . '/craft.log');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// craft doctor
|
|
|
|
|
if ($craft['craft-options']['doctor']) {
|
|
|
|
|
$retcode = $this->runCommand('doctor', '--auto-fix');
|
|
|
|
|
if ($retcode !== 0) {
|
|
|
|
|
$this->output->writeln('<error>craft doctor failed</error>');
|
|
|
|
|
$this->log("craft doctor failed with code: {$retcode}", true);
|
|
|
|
|
return static::FAILURE;
|
2025-06-19 10:36:31 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// install go and xcaddy for frankenphp
|
|
|
|
|
if (in_array('frankenphp', $craft['sapi'])) {
|
2025-06-19 12:07:22 +07:00
|
|
|
$retcode = $this->runCommand('install-pkg', 'go-xcaddy');
|
2025-06-19 10:36:31 +07:00
|
|
|
if ($retcode !== 0) {
|
2025-06-19 12:07:22 +07:00
|
|
|
$this->output->writeln('<error>craft go-xcaddy failed</error>');
|
|
|
|
|
$this->log("craft go-xcaddy failed with code: {$retcode}", true);
|
2025-06-19 10:36:31 +07:00
|
|
|
return static::FAILURE;
|
2025-04-24 14:18:39 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// craft download
|
|
|
|
|
if ($craft['craft-options']['download']) {
|
2025-06-02 11:55:55 +07:00
|
|
|
$sharedAppend = $shared_extensions ? ',' . $shared_extensions : '';
|
|
|
|
|
$args = ["--for-extensions={$static_extensions}{$sharedAppend}"];
|
2025-04-24 14:18:39 +08:00
|
|
|
if ($craft['libs'] !== []) {
|
|
|
|
|
$args[] = "--for-libs={$libs}";
|
|
|
|
|
}
|
|
|
|
|
if (isset($craft['php-version'])) {
|
|
|
|
|
$args[] = '--with-php=' . $craft['php-version'];
|
2025-06-06 09:57:20 +07:00
|
|
|
if (!array_key_exists('ignore-cache-sources', $craft['download-options'])) {
|
2025-04-24 14:18:39 +08:00
|
|
|
$craft['download-options']['ignore-cache-sources'] = 'php-src';
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-26 14:14:23 +02:00
|
|
|
$this->optionsToArguments($craft['download-options'], $args);
|
2025-04-24 14:18:39 +08:00
|
|
|
$retcode = $this->runCommand('download', ...$args);
|
|
|
|
|
if ($retcode !== 0) {
|
|
|
|
|
$this->output->writeln('<error>craft download failed</error>');
|
|
|
|
|
$this->log('craft download failed with code: ' . $retcode, true);
|
|
|
|
|
return static::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// craft build
|
|
|
|
|
if ($craft['craft-options']['build']) {
|
2025-06-02 12:27:58 +07:00
|
|
|
$args = [$static_extensions, "--with-libs={$libs}", "--build-shared={$shared_extensions}", ...array_map(fn ($x) => "--build-{$x}", $craft['sapi'])];
|
2025-05-26 14:14:23 +02:00
|
|
|
$this->optionsToArguments($craft['build-options'], $args);
|
2025-04-24 14:18:39 +08:00
|
|
|
$retcode = $this->runCommand('build', ...$args);
|
|
|
|
|
if ($retcode !== 0) {
|
|
|
|
|
$this->output->writeln('<error>craft build failed</error>');
|
|
|
|
|
$this->log('craft build failed with code: ' . $retcode, true);
|
|
|
|
|
return static::FAILURE;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function processLogCallback($type, $buffer): void
|
|
|
|
|
{
|
|
|
|
|
if ($type === Process::ERR) {
|
|
|
|
|
fwrite(STDERR, $buffer);
|
|
|
|
|
} else {
|
|
|
|
|
fwrite(STDOUT, $buffer);
|
|
|
|
|
$this->log($buffer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function log(string $log, bool $master_log = false): void
|
|
|
|
|
{
|
|
|
|
|
if ($master_log) {
|
|
|
|
|
$log = "\n[static-php-cli]> " . $log . "\n\n";
|
|
|
|
|
} else {
|
|
|
|
|
$log = preg_replace('/\x1b\[[0-9;]*m/', '', $log);
|
|
|
|
|
}
|
|
|
|
|
file_put_contents('craft.log', $log, FILE_APPEND);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function runCommand(string $cmd, ...$args): int
|
|
|
|
|
{
|
|
|
|
|
global $argv;
|
|
|
|
|
if ($this->getOption('debug')) {
|
|
|
|
|
array_unshift($args, '--debug');
|
|
|
|
|
}
|
|
|
|
|
$prefix = PHP_SAPI === 'cli' ? [PHP_BINARY, $argv[0]] : [$argv[0]];
|
|
|
|
|
|
2025-06-02 15:12:44 +07:00
|
|
|
$env = getenv();
|
|
|
|
|
$process = new Process([...$prefix, $cmd, '--no-motd', ...$args], env: $env, timeout: null);
|
2025-04-24 14:18:39 +08:00
|
|
|
$this->log("Running: {$process->getCommandLine()}", true);
|
|
|
|
|
|
|
|
|
|
if (PHP_OS_FAMILY === 'Windows') {
|
|
|
|
|
sapi_windows_set_ctrl_handler(function () use ($process) {
|
2025-04-29 14:52:24 +08:00
|
|
|
if ($process->isRunning()) {
|
|
|
|
|
$process->signal(-1073741510);
|
|
|
|
|
}
|
2025-04-24 14:18:39 +08:00
|
|
|
});
|
|
|
|
|
} elseif (extension_loaded('pcntl')) {
|
|
|
|
|
pcntl_signal(SIGINT, function () use ($process) {
|
|
|
|
|
$process->signal(SIGINT);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
logger()->debug('You have not enabled `pcntl` extension, cannot prevent download file corruption when Ctrl+C');
|
|
|
|
|
}
|
|
|
|
|
// $process->setTty(true);
|
|
|
|
|
$process->run([$this, 'processLogCallback']);
|
|
|
|
|
return $process->getExitCode();
|
|
|
|
|
}
|
2025-05-26 14:14:23 +02:00
|
|
|
|
|
|
|
|
private function optionsToArguments(array $options, array &$args): void
|
|
|
|
|
{
|
|
|
|
|
foreach ($options as $option => $val) {
|
|
|
|
|
if ((is_bool($val) && $val) || $val === null) {
|
|
|
|
|
$args[] = "--{$option}";
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (is_string($val)) {
|
|
|
|
|
$args[] = "--{$option}={$val}";
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (is_array($val)) {
|
|
|
|
|
foreach ($val as $v) {
|
|
|
|
|
if (is_string($v)) {
|
|
|
|
|
$args[] = "--{$option}={$v}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-24 14:18:39 +08:00
|
|
|
}
|