mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-03 23:05:41 +08:00
Merge branch 'main' into fix/icurel
This commit is contained in:
@@ -6,6 +6,7 @@ namespace SPC;
|
||||
|
||||
use SPC\command\BuildLibsCommand;
|
||||
use SPC\command\BuildPHPCommand;
|
||||
use SPC\command\CraftCommand;
|
||||
use SPC\command\DeleteDownloadCommand;
|
||||
use SPC\command\dev\AllExtCommand;
|
||||
use SPC\command\dev\ExtVerCommand;
|
||||
@@ -43,6 +44,8 @@ final class ConsoleApplication extends Application
|
||||
|
||||
$this->addCommands(
|
||||
[
|
||||
// Craft command
|
||||
new CraftCommand(),
|
||||
// Common commands
|
||||
new BuildPHPCommand(),
|
||||
new BuildLibsCommand(),
|
||||
|
||||
@@ -9,6 +9,7 @@ use Laravel\Prompts\Prompt;
|
||||
use Psr\Log\LogLevel;
|
||||
use SPC\ConsoleApplication;
|
||||
use SPC\exception\ExceptionHandler;
|
||||
use SPC\exception\ValidationException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\util\GlobalEnvManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -58,12 +59,12 @@ abstract class BaseCommand extends Command
|
||||
});
|
||||
$version = ConsoleApplication::VERSION;
|
||||
if (!$this->no_motd) {
|
||||
echo " _ _ _ _
|
||||
___| |_ __ _| |_(_) ___ _ __ | |__ _ __
|
||||
/ __| __/ _` | __| |/ __|____| '_ \\| '_ \\| '_ \\
|
||||
echo " _ _ _ _
|
||||
___| |_ __ _| |_(_) ___ _ __ | |__ _ __
|
||||
/ __| __/ _` | __| |/ __|____| '_ \\| '_ \\| '_ \\
|
||||
\\__ \\ || (_| | |_| | (_|_____| |_) | | | | |_) |
|
||||
|___/\\__\\__,_|\\__|_|\\___| | .__/|_| |_| .__/ v{$version}
|
||||
|_| |_|
|
||||
|_| |_|
|
||||
";
|
||||
}
|
||||
}
|
||||
@@ -104,7 +105,7 @@ abstract class BaseCommand extends Command
|
||||
// show raw argv list for logger()->debug
|
||||
logger()->debug('argv: ' . implode(' ', $_SERVER['argv']));
|
||||
return $this->handle();
|
||||
} catch (WrongUsageException $e) {
|
||||
} catch (ValidationException|WrongUsageException $e) {
|
||||
$msg = explode("\n", $e->getMessage());
|
||||
foreach ($msg as $v) {
|
||||
logger()->error($v);
|
||||
|
||||
178
src/SPC/command/CraftCommand.php
Normal file
178
src/SPC/command/CraftCommand.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?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')]
|
||||
class CraftCommand extends BaseCommand
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
$extensions = implode(',', $craft['extensions']);
|
||||
$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;
|
||||
}
|
||||
}
|
||||
// craft download
|
||||
if ($craft['craft-options']['download']) {
|
||||
$args = ["--for-extensions={$extensions}"];
|
||||
if ($craft['libs'] !== []) {
|
||||
$args[] = "--for-libs={$libs}";
|
||||
}
|
||||
if (isset($craft['php-version'])) {
|
||||
$args[] = '--with-php=' . $craft['php-version'];
|
||||
if (!array_key_exists('ignore-cache-sources', $craft['download-options']) || $craft['download-options']['ignore-cache-sources'] === false) {
|
||||
$craft['download-options']['ignore-cache-sources'] = 'php-src';
|
||||
} elseif ($craft['download-options']['ignore-cache-sources'] !== null) {
|
||||
$craft['download-options']['ignore-cache-sources'] .= ',php-src';
|
||||
}
|
||||
}
|
||||
$this->optionsToArguments($craft['download-options'], $args);
|
||||
$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']) {
|
||||
$args = [$extensions, "--with-libs={$libs}", ...array_map(fn ($x) => "--build-{$x}", $craft['sapi'])];
|
||||
$this->optionsToArguments($craft['build-options'], $args);
|
||||
$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]];
|
||||
|
||||
$process = new Process([...$prefix, $cmd, '--no-motd', ...$args], timeout: null);
|
||||
$this->log("Running: {$process->getCommandLine()}", true);
|
||||
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
sapi_windows_set_ctrl_handler(function () use ($process) {
|
||||
if ($process->isRunning()) {
|
||||
$process->signal(-1073741510);
|
||||
}
|
||||
});
|
||||
} elseif (extension_loaded('pcntl')) {
|
||||
pcntl_signal(SIGINT, function () use ($process) {
|
||||
/* @noinspection PhpComposerExtensionStubsInspection */
|
||||
$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();
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,20 @@ class CurlHook
|
||||
*/
|
||||
public static function setupGithubToken(string $method, string $url, array &$headers): void
|
||||
{
|
||||
if (!getenv('GITHUB_TOKEN')) {
|
||||
$token = getenv('GITHUB_TOKEN');
|
||||
if (!$token) {
|
||||
logger()->debug('no github token found, skip');
|
||||
return;
|
||||
}
|
||||
if (getenv('GITHUB_USER')) {
|
||||
$auth = base64_encode(getenv('GITHUB_USER') . ':' . getenv('GITHUB_TOKEN'));
|
||||
$auth = base64_encode(getenv('GITHUB_USER') . ':' . $token);
|
||||
$he = "Authorization: Basic {$auth}";
|
||||
if (!in_array($he, $headers)) {
|
||||
$headers[] = $he;
|
||||
}
|
||||
logger()->info("using basic github token for {$method} {$url}");
|
||||
} else {
|
||||
$auth = getenv('GITHUB_TOKEN');
|
||||
$auth = $token;
|
||||
$he = "Authorization: Bearer {$auth}";
|
||||
if (!in_array($he, $headers)) {
|
||||
$headers[] = $he;
|
||||
|
||||
@@ -5,6 +5,9 @@ declare(strict_types=1);
|
||||
namespace SPC\util;
|
||||
|
||||
use SPC\exception\ValidationException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ConfigValidator
|
||||
{
|
||||
@@ -110,4 +113,123 @@ class ConfigValidator
|
||||
{
|
||||
is_array($data) || throw new ValidationException('pkg.json is broken');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $craft_file craft.yml path
|
||||
* @param Command $command craft command instance
|
||||
* @return array{
|
||||
* php-version?: string,
|
||||
* extensions: array<string>,
|
||||
* libs?: array<string>,
|
||||
* sapi: array<string>,
|
||||
* debug?: bool,
|
||||
* clean-build?: bool,
|
||||
* build-options?: array<string, mixed>,
|
||||
* download-options?: array<string, mixed>,
|
||||
* extra-env?: array<string, string>,
|
||||
* craft-options?: array{
|
||||
* doctor?: bool,
|
||||
* download?: bool,
|
||||
* build?: bool
|
||||
* }
|
||||
* }
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function validateAndParseCraftFile(mixed $craft_file, Command $command): array
|
||||
{
|
||||
$build_options = $command->getApplication()->find('build')->getDefinition()->getOptions();
|
||||
$download_options = $command->getApplication()->find('download')->getDefinition()->getOptions();
|
||||
|
||||
try {
|
||||
$craft = Yaml::parse(file_get_contents($craft_file));
|
||||
} catch (ParseException $e) {
|
||||
throw new ValidationException('Craft file is broken: ' . $e->getMessage());
|
||||
}
|
||||
if (!is_assoc_array($craft)) {
|
||||
throw new ValidationException('Craft file is broken');
|
||||
}
|
||||
// check php-version
|
||||
if (isset($craft['php-version'])) {
|
||||
// validdate version, accept 8.x, 7.x, 8.x.x, 7.x.x, 8, 7
|
||||
$version = strval($craft['php-version']);
|
||||
if (!preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', $version, $matches)) {
|
||||
throw new ValidationException('Craft file php-version is invalid');
|
||||
}
|
||||
}
|
||||
// check extensions
|
||||
if (!isset($craft['extensions'])) {
|
||||
throw new ValidationException('Craft file must have extensions');
|
||||
}
|
||||
if (is_string($craft['extensions'])) {
|
||||
$craft['extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['extensions'])));
|
||||
}
|
||||
// check libs
|
||||
if (isset($craft['libs']) && is_string($craft['libs'])) {
|
||||
$craft['libs'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['libs'])));
|
||||
} elseif (!isset($craft['libs'])) {
|
||||
$craft['libs'] = [];
|
||||
}
|
||||
// check sapi
|
||||
if (!isset($craft['sapi'])) {
|
||||
throw new ValidationException('Craft file must have sapi');
|
||||
}
|
||||
if (is_string($craft['sapi'])) {
|
||||
$craft['sapi'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['sapi'])));
|
||||
}
|
||||
// debug as boolean
|
||||
if (isset($craft['debug'])) {
|
||||
$craft['debug'] = filter_var($craft['debug'], FILTER_VALIDATE_BOOLEAN);
|
||||
} else {
|
||||
$craft['debug'] = false;
|
||||
}
|
||||
// check clean-build
|
||||
$craft['clean-build'] ??= false;
|
||||
// check build-options
|
||||
if (isset($craft['build-options'])) {
|
||||
if (!is_assoc_array($craft['build-options'])) {
|
||||
throw new ValidationException('Craft file build-options must be an object');
|
||||
}
|
||||
foreach ($craft['build-options'] as $key => $value) {
|
||||
if (!isset($build_options[$key])) {
|
||||
throw new ValidationException("Craft file build-options {$key} is invalid");
|
||||
}
|
||||
// check an array
|
||||
if ($build_options[$key]->isArray() && !is_array($value)) {
|
||||
throw new ValidationException("Craft file build-options {$key} must be an array");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$craft['build-options'] = [];
|
||||
}
|
||||
// check download options
|
||||
if (isset($craft['download-options'])) {
|
||||
if (!is_assoc_array($craft['download-options'])) {
|
||||
throw new ValidationException('Craft file download-options must be an object');
|
||||
}
|
||||
foreach ($craft['download-options'] as $key => $value) {
|
||||
if (!isset($download_options[$key])) {
|
||||
throw new ValidationException("Craft file download-options {$key} is invalid");
|
||||
}
|
||||
// check an array
|
||||
if ($download_options[$key]->isArray() && !is_array($value)) {
|
||||
throw new ValidationException("Craft file download-options {$key} must be an array");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$craft['download-options'] = [];
|
||||
}
|
||||
// check extra-env
|
||||
if (isset($craft['extra-env'])) {
|
||||
if (!is_assoc_array($craft['extra-env'])) {
|
||||
throw new ValidationException('Craft file extra-env must be an object');
|
||||
}
|
||||
} else {
|
||||
$craft['extra-env'] = [];
|
||||
}
|
||||
// check craft-options
|
||||
$craft['craft-options']['doctor'] ??= true;
|
||||
$craft['craft-options']['download'] ??= true;
|
||||
$craft['craft-options']['build'] ??= true;
|
||||
return $craft;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,4 +85,4 @@ const AUTOCONF_LDFLAGS = 8;
|
||||
const AUTOCONF_ALL = 15;
|
||||
|
||||
ConsoleLogger::$date_format = 'H:i:s';
|
||||
ConsoleLogger::$format = '[%date%] [I] %body%';
|
||||
ConsoleLogger::$format = '[%date%] [%level_short%] %body%';
|
||||
|
||||
@@ -24,7 +24,7 @@ $test_os = [
|
||||
'macos-13',
|
||||
// 'macos-14',
|
||||
'macos-15',
|
||||
'ubuntu-latest',
|
||||
// 'ubuntu-latest',
|
||||
'ubuntu-22.04',
|
||||
'ubuntu-24.04',
|
||||
'ubuntu-22.04-arm',
|
||||
@@ -131,7 +131,7 @@ if ($argv[1] === 'doctor_cmd') {
|
||||
$doctor_cmd = 'doctor --auto-fix --debug';
|
||||
}
|
||||
if ($argv[1] === 'install_upx_cmd') {
|
||||
$install_upx_cmd = 'install-pkg upx';
|
||||
$install_upx_cmd = 'install-pkg upx --debug';
|
||||
}
|
||||
|
||||
$prefix = match ($argv[2] ?? null) {
|
||||
|
||||
Reference in New Issue
Block a user