initial commit for macOS support

This commit is contained in:
crazywhalecc
2023-03-18 17:32:21 +08:00
parent 64054f16c5
commit 4eee09c390
50 changed files with 4385 additions and 6 deletions

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler;
use SPC\util\DependencyUtil;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/** @noinspection PhpUnused */
class BuildCliCommand extends BuildCommand
{
protected static $defaultName = 'build';
public function configure()
{
$this->setDescription('Build CLI binary');
$this->addArgument('extensions', InputArgument::REQUIRED, 'The extensions will be compiled, comma separated');
$this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
$this->addOption('build-micro', null, null, 'build micro only');
$this->addOption('build-all', null, null, 'build both cli and micro');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
// 从参数中获取要编译的 libraries并转换为数组
$libraries = array_map('trim', array_filter(explode(',', $input->getOption('with-libs'))));
// 从参数中获取要编译的 extensions并转换为数组
$extensions = array_map('trim', array_filter(explode(',', $input->getArgument('extensions'))));
define('BUILD_ALL_STATIC', true);
if ($input->getOption('build-all')) {
$rule = BUILD_MICRO_BOTH;
logger()->info('Builder will build php-cli and phpmicro SAPI');
} elseif ($input->getOption('build-micro')) {
$rule = BUILD_MICRO_ONLY;
logger()->info('Builder will build phpmicro SAPI');
} else {
$rule = BUILD_MICRO_NONE;
logger()->info('Builder will build php-cli SAPI');
}
try {
// 构建对象
$builder = BuilderProvider::makeBuilderByInput($input);
// 根据提供的扩展列表获取依赖库列表并编译
[$extensions, $libraries, $not_included] = DependencyUtil::getExtLibsByDeps($extensions, $libraries);
logger()->info('Enabled extensions: ' . implode(', ', $extensions));
logger()->info('Required libraries: ' . implode(', ', $libraries));
if (!empty($not_included)) {
logger()->warning('some extensions will be enabled due to dependencies: ' . implode(',', $not_included));
}
sleep(2);
// 编译和检查库是否完整
$builder->buildLibs($libraries);
// 执行扩展检测
$builder->proveExts($extensions);
// 构建
$builder->buildPHP($rule, $input->getOption('with-clean'), $input->getOption('bloat'));
// 统计时间
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Build complete, used ' . $time . ' s !');
if ($rule !== BUILD_MICRO_ONLY) {
logger()->info('Static php binary path: ' . SOURCE_PATH . '/php-src/sapi/cli/php');
}
if ($rule !== BUILD_MICRO_NONE) {
logger()->info('phpmicro binary path: ' . SOURCE_PATH . '/php-src/sapi/micro/micro.sfx');
}
return 0;
} catch (\Throwable $e) {
if ($input->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
logger()->emergency('Build failed, please check terminal output, or build with --debug option to see more details.');
logger()->emergency($e->getMessage());
}
return 1;
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use Symfony\Component\Console\Input\InputOption;
abstract class BuildCommand extends BaseCommand
{
public function __construct(string $name = null)
{
parent::__construct($name);
// 根据运行的操作系统分配允许不同的命令行参数Windows 需要额外的 VS 和 SDK等*nix 需要提供架构
switch (PHP_OS_FAMILY) {
case 'Windows':
$this->addOption('with-sdk-binary-dir', null, InputOption::VALUE_REQUIRED, 'path to binary sdk');
$this->addOption('vs-ver', null, InputOption::VALUE_REQUIRED, 'vs version, e.g. "17" for Visual Studio 2022');
$this->addOption('arch', null, InputOption::VALUE_REQUIRED, 'architecture, "x64" or "arm64"', 'x64');
break;
case 'Linux':
$this->addOption('no-system-static', null, null, 'do not use system static libraries');
// no break
case 'Darwin':
$this->addOption('cc', null, InputOption::VALUE_REQUIRED, 'C compiler');
$this->addOption('cxx', null, InputOption::VALUE_REQUIRED, 'C++ compiler');
$this->addOption('arch', null, InputOption::VALUE_REQUIRED, 'architecture', php_uname('m'));
break;
}
// 是否在编译 make 前清除旧的文件
$this->addOption('with-clean', null, null, 'fresh build, `make clean` before `make`');
// 是否采用强制链接,让链接器强制加载静态库文件
$this->addOption('bloat', null, null, 'add all libraries into binary');
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/** @noinspection PhpUnused */
class BuildLibsCommand extends BuildCommand
{
protected static $defaultName = 'build:libs';
public function configure()
{
$this->setDescription('Build dependencies');
$this->addArgument('libraries', InputArgument::REQUIRED, 'The libraries will be compiled, comma separated');
$this->addOption('clean', null, null, 'Clean old download cache and source before fetch');
$this->addOption('all', 'A', null, 'Build all libs that static-php-cli needed');
}
public function initialize(InputInterface $input, OutputInterface $output)
{
// --all 等于 ""
if ($input->getOption('all')) {
$input->setArgument('libraries', '');
}
}
/**
* @throws RuntimeException
* @throws FileSystemException
*/
public function execute(InputInterface $input, OutputInterface $output): int
{
// 从参数中获取要编译的 libraries并转换为数组
$libraries = array_map('trim', array_filter(explode(',', $input->getArgument('libraries'))));
// 删除旧资源
if ($input->getOption('clean')) {
logger()->warning('You are doing some operations that not recoverable: removing directories below');
logger()->warning(BUILD_ROOT_PATH);
logger()->warning('I will remove these dir after you press [Enter] !');
echo 'Confirm operation? [Yes] ';
fgets(STDIN);
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('rmdir /s /q ' . BUILD_ROOT_PATH);
} else {
f_passthru('rm -rf ' . BUILD_ROOT_PATH);
}
}
// 构建对象
$builder = BuilderProvider::makeBuilderByInput($input);
// 只编译 library 的情况下,标记
$builder->setLibsOnly();
// 编译和检查库完整
$builder->buildLibs($libraries);
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Build libs complete, used ' . $time . ' s !');
return 0;
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use CliHelper\Tools\ArgFixer;
use CliHelper\Tools\DataProvider;
use CliHelper\Tools\SeekableArrayIterator;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/** @noinspection PhpUnused */
class DeployCommand extends BaseCommand
{
protected static $defaultName = 'deploy-self';
public function configure()
{
$this->setDescription('Deploy static-php-cli self to an .phar application');
$this->addArgument('target', InputArgument::OPTIONAL, 'The file or directory to pack.');
$this->addOption('auto-phar-fix', null, InputOption::VALUE_NONE, 'Automatically fix ini option.');
$this->addOption('overwrite', 'W', InputOption::VALUE_NONE, 'Overwrite existing files.');
$this->addOption('disable-gzip', 'z', InputOption::VALUE_NONE, 'disable gzip archive mode');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
// 第一阶段流程如果没有写path将会提示输入要打包的path
$prompt = new ArgFixer($input, $output);
// 首先得确认是不是关闭了readonly模式
if (ini_get('phar.readonly') == 1) {
if ($input->getOption('auto-phar-fix')) {
$ask = true;
} else {
$ask = $prompt->requireBool('<comment>pack command needs "phar.readonly" = "Off" !</comment>' . PHP_EOL . 'If you want to automatically set it and continue, just Enter', true);
}
$output->writeln('<info>Now running command in child process.</info>');
if ($ask) {
global $argv;
passthru(PHP_BINARY . ' -d phar.readonly=0 ' . implode(' ', $argv), $retcode);
exit($retcode);
}
}
// 获取路径
$path = WORKING_DIR;
// 如果是目录,则将目录下的所有文件打包
$phar_path = $prompt->requireArgument('target', 'Please input the phar target filename', 'static-php-cli.phar');
if (DataProvider::isRelativePath($phar_path)) {
$phar_path = '/tmp/' . $phar_path;
}
if (file_exists($phar_path)) {
$ask = $input->getOption('overwrite') ? true : $prompt->requireBool('<comment>The file "' . $phar_path . '" already exists, do you want to overwrite it?</comment>' . PHP_EOL . 'If you want to, just Enter');
if (!$ask) {
$output->writeln('<comment>User canceled.</comment>');
return 1;
}
@unlink($phar_path);
}
$phar = new \Phar($phar_path);
$phar->startBuffering();
$all = DataProvider::scanDirFiles($path, true, true);
$all = array_filter($all, function ($x) {
$dirs = preg_match('/(^(bin|config|src|vendor)\\/|^(composer\\.json|README\\.md|source\\.json|LICENSE|README-en\\.md)$)/', $x);
return !($dirs !== 1);
});
sort($all);
$map = [];
foreach ($all as $v) {
$map[$v] = $path . '/' . $v;
}
$output->writeln('<info>Start packing files...</info>');
try {
$phar->buildFromIterator(new SeekableArrayIterator($map, new ProgressBar($output)));
$phar->addFromString(
'.phar-entry.php',
str_replace(
'/../vendor/autoload.php',
'/vendor/autoload.php',
file_get_contents(ROOT_DIR . '/bin/static-php-cli')
)
);
$stub = '.phar-entry.php';
$phar->setStub($phar->createDefaultStub($stub));
} catch (\Throwable $e) {
$output->writeln($e);
return 1;
}
$phar->addFromString('.prod', 'true');
if (!$input->getOption('disable-gzip')) {
$phar->compressFiles(\Phar::GZ);
}
$phar->stopBuffering();
$output->writeln(PHP_EOL . 'Done! Phar file is generated at "' . $phar_path . '".');
if (file_exists(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx')) {
$output->writeln('Detected you have already compiled micro binary, I will make executable now for you!');
file_put_contents(
$phar_path . '.exe',
file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') .
file_get_contents($phar_path)
);
chmod($phar_path . '.exe', 0755);
$output->writeln('<info>Static: ' . $phar_path . '.exe</info>');
}
chmod($phar_path, 0755);
$output->writeln('<info>Phar: ' . $phar_path . '</info>');
return 0;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* 修改 config 后对其 kv 进行排序的操作
*/
class DumpLicenseCommand extends BaseCommand
{
protected static $defaultName = 'dump-license';
public function configure()
{
$this->setDescription('Dump licenses for required libraries');
$this->addArgument('config-name', InputArgument::REQUIRED, 'Your config to be sorted, you can sort "lib", "source" and "ext".');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>not implemented</info>');
return 1;
}
}

View File

@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\exception\DownloaderException;
use SPC\exception\ExceptionHandler;
use SPC\exception\FileSystemException;
use SPC\exception\InvalidArgumentException;
use SPC\exception\RuntimeException;
use SPC\store\Config;
use SPC\store\Downloader;
use SPC\util\Patcher;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/** @noinspection PhpUnused */
class FetchSourceCommand extends BaseCommand
{
protected static $defaultName = 'fetch';
protected string $php_major_ver;
protected InputInterface $input;
public function configure()
{
$this->setDescription('Fetch required sources');
$this->addArgument('extensions', InputArgument::REQUIRED, 'The extensions will be compiled, comma separated');
$this->addArgument('libraries', InputArgument::REQUIRED, 'The libraries will be compiled, comma separated');
$this->addOption('hash', null, null, 'Hash only');
$this->addOption('shallow-clone', null, null, 'Clone shallow');
$this->addOption('with-openssl11', null, null, 'Use openssl 1.1');
$this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'version in major.minor format like 8.1', '8.1');
$this->addOption('clean', null, null, 'Clean old download cache and source before fetch');
$this->addOption('all', 'A', null, 'Fetch all sources that static-php-cli needed');
}
public function initialize(InputInterface $input, OutputInterface $output)
{
// --all 等于 "" "",也就是所有东西都要下载
if ($input->getOption('all')) {
$input->setArgument('extensions', '');
$input->setArgument('libraries', '');
}
parent::initialize($input, $output);
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
try {
// 匹配版本
$ver = $this->php_major_ver = $input->getOption('with-php') ?? '8.1';
preg_match('/^\d+\.\d+$/', $ver, $matches);
if (!$matches) {
logger()->error("bad version arg: {$ver}, x.y required!");
return 1;
}
// 删除旧资源
if ($input->getOption('clean')) {
logger()->warning('You are doing some operations that not recoverable: removing directories below');
logger()->warning(SOURCE_PATH);
logger()->warning(DOWNLOAD_PATH);
logger()->warning('I will remove these dir after you press [Enter] !');
echo 'Confirm operation? [Yes] ';
$r = strtolower(trim(fgets(STDIN)));
if ($r !== 'yes' && $r !== '') {
logger()->notice('Operation canceled.');
return 1;
}
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('rmdir /s /q ' . SOURCE_PATH);
f_passthru('rmdir /s /q ' . DOWNLOAD_PATH);
} else {
f_passthru('rm -rf ' . SOURCE_PATH);
f_passthru('rm -rf ' . DOWNLOAD_PATH);
}
}
// 使用浅克隆可以减少调用 git 命令下载资源时的存储空间占用
if ($input->getOption('shallow-clone')) {
define('GIT_SHALLOW_CLONE', true);
}
// 读取源配置随便读一个source用于缓存 source 配置
Config::getSource('openssl');
// 是否启用openssl11
if ($input->getOption('with-openssl11')) {
logger()->debug('Using openssl 1.1');
// 手动修改配置
Config::$source['openssl']['regex'] = '/href="(?<file>openssl-(?<version>1.[^"]+)\.tar\.gz)\"/';
}
// 默认预选 phpmicro
$chosen_sources = ['micro'];
// 从参数中获取要编译的 libraries并转换为数组
$libraries = array_map('trim', array_filter(explode(',', $input->getArgument('libraries'))));
if ($libraries) {
foreach ($libraries as $lib) {
// 从 lib 的 config 中找到对应 source 资源名称,组成一个 lib 的 source 列表
$src_name = Config::getLib($lib, 'source');
$chosen_sources[] = $src_name;
}
} else { // 如果传入了空串,那么代表 fetch 所有包
$chosen_sources = [...$chosen_sources, ...array_map(fn ($x) => $x['source'], array_values(Config::getLibs()))];
}
// 从参数中获取要编译的 extensions并转换为数组
$extensions = array_map('trim', array_filter(explode(',', $input->getArgument('extensions'))));
if ($extensions) {
foreach ($extensions as $lib) {
if (Config::getExt($lib, 'type') !== 'builtin') {
$src_name = Config::getExt($lib, 'source');
$chosen_sources[] = $src_name;
}
}
} else {
foreach (Config::getExts() as $ext) {
if ($ext['type'] !== 'builtin') {
$chosen_sources[] = $ext['source'];
}
}
}
$chosen_sources = array_unique($chosen_sources);
// 是否只hash不下载资源
if ($input->getOption('hash')) {
$hash = $this->doHash($chosen_sources);
$output->writeln($hash);
return 0;
}
// 创建目录
f_mkdir(SOURCE_PATH);
f_mkdir(DOWNLOAD_PATH);
// 下载 PHP
Downloader::fetchSource('php-src', Downloader::getLatestPHPInfo($ver));
// 下载别的依赖资源
$cnt = count($chosen_sources);
$ni = 0;
foreach ($chosen_sources as $name) {
++$ni;
logger()->info("Fetching source {$name} [{$ni}/{$cnt}]");
Downloader::fetchSource($name, Config::getSource($name));
}
// patch 每份资源只需一次如果已经下载好的资源已经patch了就标记一下不patch了
if (!file_exists(SOURCE_PATH . '/.patched')) {
$this->doPatch();
} else {
logger()->notice('sources already patched');
}
// 打印拉取资源用时
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Fetch complete, used ' . $time . ' s !');
return 0;
} catch (\Throwable $e) {
// 不开 debug 模式就不要再显示复杂的调试栈信息了
if ($input->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
logger()->emergency($e->getMessage() . ', previous message: ' . $e->getPrevious()->getMessage());
}
return 1;
}
}
/**
* 计算资源名称列表的 Hash
*
* @param array $chosen_sources 要计算 hash 的资源名称列表
* @throws InvalidArgumentException
* @throws DownloaderException
* @throws FileSystemException
*/
private function doHash(array $chosen_sources): string
{
$files = [];
foreach ($chosen_sources as $name) {
$source = Config::getSource($name);
$filename = match ($source['type']) {
'ghtar' => Downloader::getLatestGithubTarball($name, $source)[1],
'ghtagtar' => Downloader::getLatestGithubTarball($name, $source, 'tags')[1],
'ghrel' => Downloader::getLatestGithubRelease($name, $source)[1],
'filelist' => Downloader::getFromFileList($name, $source)[1],
'url' => $source['filename'] ?? basename($source['url']),
'git' => null,
default => throw new InvalidArgumentException('unknown source type: ' . $source['type']),
};
if ($filename !== null) {
logger()->info("found {$name} source: {$filename}");
$files[] = $filename;
}
}
return hash('sha256', implode('|', $files));
}
/**
* 在拉回资源后,需要对一些文件做一些补丁 patch
*
* @throws FileSystemException
* @throws RuntimeException
*/
private function doPatch(): void
{
// patch 一些 PHP 的资源,以便编译
Patcher::patchPHPDepFiles();
// openssl 3 需要 patch 额外的东西
if (!$this->input->getOption('with-openssl11') && $this->php_major_ver === '8.0') {
Patcher::patchOpenssl3();
}
// openssl1.1.1q 在 MacOS 上直接编译会报错patch 一下
// @see: https://github.com/openssl/openssl/issues/18720
if ($this->input->getOption('with-openssl11') && file_exists(SOURCE_PATH . '/openssl/test/v3ext.c') && PHP_OS_FAMILY === 'Darwin') {
Patcher::patchDarwinOpenssl11();
}
// swow 需要软链接内部的文件夹才能正常编译
if (!file_exists(SOURCE_PATH . '/php-src/ext/swow')) {
Patcher::patchSwow();
}
// 标记 patch 完成,避免重复 patch
file_put_contents(SOURCE_PATH . '/.patched', '');
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\builder\traits\NoMotdTrait;
use SPC\store\Config;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ListExtCommand extends BaseCommand
{
use NoMotdTrait;
protected static $defaultName = 'list-ext';
public function configure()
{
$this->setDescription('List supported extensions');
}
public function execute(InputInterface $input, OutputInterface $output)
{
foreach (Config::getExts() as $ext => $meta) {
echo $ext . PHP_EOL;
}
return 0;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\exception\FileSystemException;
use SPC\exception\ValidationException;
use SPC\store\FileSystem;
use SPC\util\ConfigValidator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* 修改 config 后对其 kv 进行排序的操作
*/
class SortConfigCommand extends BaseCommand
{
protected static $defaultName = 'sort-config';
public function configure()
{
$this->setDescription('After config edited, sort it by alphabet');
$this->addArgument('config-name', InputArgument::REQUIRED, 'Your config to be sorted, you can sort "lib", "source" and "ext".');
}
/**
* @throws ValidationException
* @throws FileSystemException
*/
public function execute(InputInterface $input, OutputInterface $output): int
{
switch ($name = $input->getArgument('config-name')) {
case 'lib':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true);
ConfigValidator::validateLibs($file);
ksort($file);
file_put_contents(ROOT_DIR . '/config/lib.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
break;
case 'source':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/source.json'), true);
ConfigValidator::validateSource($file);
ksort($file);
file_put_contents(ROOT_DIR . '/config/source.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
break;
case 'ext':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/ext.json'), true);
ConfigValidator::validateExts($file);
ksort($file);
file_put_contents(ROOT_DIR . '/config/ext.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
break;
default:
$output->writeln("<error>invalid config name: {$name}</error>");
return 1;
}
$output->writeln('<info>sort success</info>');
return 0;
}
}