refactor download

This commit is contained in:
crazywhalecc 2023-04-30 12:42:19 +08:00
parent 117cd93e8f
commit 0bed76da11
No known key found for this signature in database
GPG Key ID: 1F4BDD59391F2680
30 changed files with 901 additions and 673 deletions

106
.github/workflows/build-arm.yml vendored Normal file
View File

@ -0,0 +1,106 @@
name: CI on arm linux
on:
workflow_dispatch:
inputs:
operating-system:
required: true
description: Compile target arch (Linux only)
type: choice
options:
- aarch64
- armv7l
version:
required: true
description: php version to compile
default: '8.2'
type: choice
options:
- '8.2'
- '8.1'
- '8.0'
- '7.4'
build-cli:
description: build cli binary
default: true
type: boolean
build-micro:
description: build phpmicro binary
type: boolean
build-fpm:
description: build fpm binary
type: boolean
extensions:
description: extensions to compile (comma separated)
required: true
type: string
debug:
type: boolean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build:
name: build ${{ inputs.version }} on ${{ inputs.operating-system }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Cache downloaded source
- id: cache-download
uses: actions/cache@v3
with:
path: downloads
key: php-${{ inputs.version }}-dependencies
# With or without debug
- if: inputs.debug == true
run: echo "SPC_BUILD_DEBUG=--debug" >> $GITHUB_ENV
# With target select: cli, micro or both
- if: ${{ inputs.build-cli == true }}
run: echo "SPC_BUILD_CLI=--build-cli" >> $GITHUB_ENV
- if: ${{ inputs.build-micro == true }}
run: echo "SPC_BUILD_MICRO=--build-micro" >> $GITHUB_ENV
- if: ${{ inputs.build-fpm == true }}
run: echo "SPC_BUILD_FPM=--build-fpm" >> $GITHUB_ENV
# If there's no dependencies cache, fetch sources, with or without debug
- run: SPC_USE_ARCH=${{ inputs.operating-system }} ./bin/spc-alpine-docker download --with-php=${{ inputs.version }} --all ${{ env.SPC_BUILD_DEBUG }}
# Run build command
- run: SPC_USE_ARCH=${{ inputs.operating-system }} ./bin/spc-alpine-docker build ${{ inputs.extensions }} ${{ env.SPC_BUILD_DEBUG }} ${{ env.SPC_BUILD_CLI }} ${{ env.SPC_BUILD_MICRO }} ${{ env.SPC_BUILD_FPM }}
# Upload cli executable
- if: ${{ inputs.build-cli == true }}
uses: actions/upload-artifact@v3
with:
name: php-${{ inputs.version }}-linux-${{ inputs.operating-system }}
path: buildroot/bin/php
# Upload micro self-extracted executable
- if: ${{ inputs.build-micro == true }}
uses: actions/upload-artifact@v3
with:
name: micro-${{ inputs.version }}-linux-${{ inputs.operating-system }}
path: buildroot/bin/micro.sfx
# Upload fpm executable
- if: ${{ inputs.build-fpm == true }}
uses: actions/upload-artifact@v3
with:
name: php-fpm-${{ inputs.version }}-linux-${{ inputs.operating-system }}
path: buildroot/bin/php-fpm
# Upload extensions metadata
- uses: actions/upload-artifact@v3
with:
name: license-files
path: buildroot/license/
- uses: actions/upload-artifact@v3
with:
name: build-meta
path: |
buildroot/build-extensions.json
buildroot/build-libraries.json

View File

@ -1,4 +1,4 @@
name: CI
name: CI on x86_64
on:
workflow_dispatch:
@ -90,7 +90,7 @@ jobs:
run: echo "SPC_BUILD_FPM=--build-fpm" >> $GITHUB_ENV
# If there's no dependencies cache, fetch sources, with or without debug
- run: CACHE_API_EXEC=yes ./bin/spc fetch --with-php=${{ inputs.version }} --all ${{ env.SPC_BUILD_DEBUG }}
- run: CACHE_API_EXEC=yes ./bin/spc download --with-php=${{ inputs.version }} --all ${{ env.SPC_BUILD_DEBUG }}
# Run build command
- run: ./bin/spc build ${{ inputs.extensions }} ${{ env.SPC_BUILD_DEBUG }} ${{ env.SPC_BUILD_CLI }} ${{ env.SPC_BUILD_MICRO }} ${{ env.SPC_BUILD_FPM }}

80
bin/spc-alpine-docker Executable file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env sh
# This file is using docker to run commands
self_dir=$(cd "$(dirname "$0")";pwd)
# Detect docker can run
if ! which docker >/dev/null; then
echo "Docker is not installed, please install docker first !"
exit 1
fi
DOCKER_EXECUTABLE="docker"
if [ $(id -u) -ne 0 ]; then
if ! docker info > /dev/null 2>&1; then
if [ "$SPC_USE_SUDO" != "yes" ]; then
echo "Docker command requires sudo"
echo -n 'To use sudo to run docker, run "export SPC_USE_SUDO=yes" and run command again'
exit 1
fi
DOCKER_EXECUTABLE="sudo docker"
fi
fi
# to check if qemu-docker run
if [ "$SPC_USE_ARCH" = "" ]; then
SPC_USE_ARCH=x86_64
fi
case $SPC_USE_ARCH in
x86_64)
ALPINE_FROM=alpine:edge
;;
aarch64)
ALPINE_FROM=multiarch/alpine:aarch64-edge
echo -e "\e[033m* Using different arch needs to setup qemu-static for docker !\e[0m"
$DOCKER_EXECUTABLE run --rm --privileged multiarch/qemu-user-static:register --reset > /dev/null
;;
armv7l)
ALPINE_FROM=multiarch/alpine:armv7-edge
echo -e "\e[033m* Using different arch needs to setup qemu-static for docker !\e[0m"
$DOCKER_EXECUTABLE run --rm --privileged multiarch/qemu-user-static:register --reset > /dev/null
;;
*)
echo "Current arch is not supported to run in docker: $SPC_USE_ARCH"
exit 1
;;
esac
if [ "$SPC_USE_MIRROR" = "yes" ]; then
SPC_USE_MIRROR="RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories"
else
SPC_USE_MIRROR="RUN echo 'Using original repo'"
fi
# Detect docker env is setup
if ! $DOCKER_EXECUTABLE images | grep -q cwcc-spc-$SPC_USE_ARCH; then
echo "Docker container does not exist. Building docker image ..."
ALPINE_DOCKERFILE=$(cat << EOF
FROM $ALPINE_FROM
$SPC_USE_MIRROR
RUN apk update
RUN apk add bash file wget cmake gcc g++ jq autoconf git libstdc++ linux-headers make m4 libgcc binutils bison flex pkgconfig automake curl
RUN apk add build-base xz php81 php81-common php81-pcntl php81-tokenizer php81-phar php81-posix php81-xml composer
RUN mkdir /app
WORKDIR /app
ADD ./src /app/src
ADD ./composer.json /app/composer.json
ADD ./bin /app/bin
RUN composer update --no-dev
EOF
)
echo "$ALPINE_DOCKERFILE" > $(pwd)/Dockerfile
$DOCKER_EXECUTABLE build -t cwcc-spc-$SPC_USE_ARCH .
rm $(pwd)/Dockerfile
fi
# Run docker
$DOCKER_EXECUTABLE run --rm -it -e SPC_FIX_DEPLOY_ROOT=$(pwd) -v $(pwd)/config:/app/config -v $(pwd)/src:/app/src -v $(pwd)/buildroot:/app/buildroot -v $(pwd)/source:/app/source -v $(pwd)/downloads:/app/downloads cwcc-spc-$SPC_USE_ARCH bin/spc $@

View File

@ -15,7 +15,8 @@
"symfony/console": "^6 || ^5 || ^4",
"zhamao/logger": "^1.0",
"crazywhalecc/cli-helper": "^0.1.0",
"nunomaduro/collision": "*"
"nunomaduro/collision": "*",
"ext-pcntl": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.2 != 3.7.0",

View File

@ -1,4 +1,11 @@
{
"php-src": {
"type": "custom",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"brotli": {
"type": "ghtar",
"repo": "google/brotli",
@ -229,13 +236,6 @@
"path": "LICENSE.txt"
}
},
"php-src": {
"type": "custom",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"postgresql": {
"type": "custom",
"license": {

View File

@ -7,7 +7,7 @@
> - partial with issue link: supported but not perfect due to issue
| | Linux | macOS | Windows |
|------------|----------------------------------------------------------------|---------------------------------------------------------------------|---------|
|------------|---------------------------------------------------------------------|--------------------------------------------------------------------|---------|
| bcmath | yes | yes | |
| bz2 | yes | yes | |
| calendar | yes | yes | |
@ -49,8 +49,8 @@
| soap | yes | yes | |
| sockets | yes | yes | |
| sqlite3 | yes | yes | |
| swow | yes | [no](https://github.com/crazywhalecc/static-php-cli/issues/32) | |
| swoole | [no](https://github.com/crazywhalecc/static-php-cli/issues/32) | [partial](https://github.com/crazywhalecc/static-php-cli/issues/32) | |
| swow | yes | yes | |
| swoole | [partial](https://github.com/crazywhalecc/static-php-cli/issues/32) | yes | |
| tokenizer | yes | yes | |
| xml | yes | yes | |
| xmlreader | yes, untested | yes, untested | |

View File

@ -6,6 +6,7 @@ namespace SPC\builder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\util\CustomExt;
@ -40,8 +41,9 @@ abstract class BuilderBase
/**
* 构建指定列表的 libs
*
* @throws RuntimeException
* @throws FileSystemException
* @throws RuntimeException
* @throws WrongUsageException
*/
public function buildLibs(array $libraries): void
{
@ -80,13 +82,14 @@ abstract class BuilderBase
$this->addLib($lib);
}
// 统计还没 fetch 到本地的库
$this->checkLibsSource();
// 计算依赖,经过这里的遍历,如果没有抛出异常,说明依赖符合要求,可以继续下面的
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
$this->initSource(libs: $libraries);
// 构建库
foreach ($this->libs as $lib) {
match ($lib->tryBuild()) {
BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'),
@ -154,6 +157,11 @@ abstract class BuilderBase
public function proveExts(array $extensions): void
{
CustomExt::loadCustomExt();
$this->initSource(sources: ['php-src']);
if ($this->getPHPVersionID() >= 80000) {
$this->initSource(sources: ['micro']);
}
$this->initSource(exts: $extensions);
foreach ($extensions as $extension) {
$class = CustomExt::getExtClass($extension);
$ext = new $class($extension, $this);
@ -248,4 +256,52 @@ abstract class BuilderBase
);
}
}
protected function initSource(?array $sources = null, ?array $libs = null, ?array $exts = null): void
{
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
throw new WrongUsageException('Download lock file "downloads/.lock.json" not found, maybe you need to download sources first ?');
}
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true);
$sources_extracted = [];
// source check exist
if (is_array($sources)) {
foreach ($sources as $source) {
$sources_extracted[$source] = true;
}
}
// lib check source exist
if (is_array($libs)) {
foreach ($libs as $lib) {
// get source name for lib
$source = Config::getLib($lib, 'source');
$sources_extracted[$source] = true;
}
}
// ext check source exist
if (is_array($exts)) {
foreach ($exts as $ext) {
// get source name for ext
if (Config::getExt($ext, 'type') !== 'external') {
continue;
}
$source = Config::getExt($ext, 'source');
$sources_extracted[$source] = true;
}
}
// start check
foreach ($sources_extracted as $source => $item) {
if (!isset($lock[$source])) {
throw new WrongUsageException('Source [' . $source . '] not downloaded, you should download it first !');
}
// check source dir exist
$check = $lock[$source]['move_path'] === null ? SOURCE_PATH . '/' . $source : SOURCE_PATH . '/' . $lock[$source]['move_path'];
if (!is_dir($check)) {
FileSystem::extractSource($source, DOWNLOAD_PATH . '/' . ($lock[$source]['filename'] ?? $lock[$source]['dirname']), $lock[$source]['move_path']);
}
}
}
}

View File

@ -13,11 +13,8 @@ class swoole extends Extension
public function getUnixConfigureArg(): string
{
$arg = '--enable-swoole';
if ($this->builder->getLib('openssl')) {
$arg .= ' --enable-openssl';
} else {
$arg .= ' --disable-openssl --without-openssl';
}
$arg .= $this->builder->getLib('openssl') ? ' --enable-openssl' : ' --disable-openssl --without-openssl';
$arg .= $this->builder->getLib('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : '';
// curl hook is buggy for static php
$arg .= ' --disable-swoole-curl';
return $arg;

View File

@ -10,7 +10,7 @@ use SPC\builder\traits\UnixBuilderTrait;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\util\Patcher;
use SPC\store\SourcePatcher;
/**
* Linux 系统环境下的构建器
@ -46,7 +46,10 @@ class LinuxBuilder extends BuilderBase
public function __construct(?string $cc = null, ?string $cxx = null, ?string $arch = null)
{
// 初始化一些默认参数
$this->cc = $cc ?? 'musl-gcc';
$this->cc = $cc ?? match (SystemUtil::getOSRelease()['dist']) {
'alpine' => 'gcc',
default => 'musl-gcc'
};
$this->cxx = $cxx ?? 'g++';
$this->arch = $arch ?? php_uname('m');
$this->gnu_arch = arch2gnu($this->arch);
@ -135,6 +138,9 @@ class LinuxBuilder extends BuilderBase
)
);
}
if ($this->getExt('swoole')) {
$extra_libs .= ' -lstdc++';
}
$envs = $this->pkgconf_env . ' ' .
"CC='{$this->cc}' " .
@ -158,11 +164,11 @@ class LinuxBuilder extends BuilderBase
$envs = "{$envs} CFLAGS='{$cflags}' LIBS='-ldl -lpthread'";
Patcher::patchPHPBeforeConfigure($this);
SourcePatcher::patchPHPBuildconf($this);
shell()->cd(SOURCE_PATH . '/php-src')->exec('./buildconf --force');
Patcher::patchPHPConfigure($this);
SourcePatcher::patchPHPConfigure($this);
shell()->cd(SOURCE_PATH . '/php-src')
->exec(
@ -183,6 +189,8 @@ class LinuxBuilder extends BuilderBase
$envs
);
SourcePatcher::patchPHPAfterConfigure($this);
file_put_contents('/tmp/comment', $this->note_section);
// 清理

View File

@ -13,44 +13,6 @@ class SystemUtil
{
use UnixSystemUtilTrait;
/**
* 查找并选择编译器命令
*
* @throws RuntimeException
*/
public static function selectCC(): string
{
logger()->debug('Choose cc');
if (self::findCommand('clang')) {
logger()->info('using clang');
return 'clang';
}
if (self::findCommand('gcc')) {
logger()->info('using gcc');
return 'gcc';
}
throw new RuntimeException('no supported cc found');
}
/**
* 查找并选择编译器命令
*
* @throws RuntimeException
*/
public static function selectCXX(): string
{
logger()->debug('Choose cxx');
if (self::findCommand('clang++')) {
logger()->info('using clang++');
return 'clang++';
}
if (self::findCommand('g++')) {
logger()->info('using g++');
return 'g++';
}
return self::selectCC();
}
#[ArrayShape(['dist' => 'mixed|string', 'ver' => 'mixed|string'])]
public static function getOSRelease(): array
{

View File

@ -22,7 +22,7 @@ namespace SPC\builder\linux\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\util\Patcher;
use SPC\store\SourcePatcher;
class libpng extends LinuxLibraryBase
{
@ -41,7 +41,7 @@ class libpng extends LinuxLibraryBase
};
// patch configure
Patcher::patchUnixLibpng();
SourcePatcher::patchUnixLibpng();
shell()->cd($this->source_dir)
->exec('chmod +x ./configure')

View File

@ -10,7 +10,7 @@ use SPC\builder\traits\UnixBuilderTrait;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\util\Patcher;
use SPC\store\SourcePatcher;
/**
* macOS 系统环境下的构建器
@ -136,11 +136,11 @@ class MacOSBuilder extends BuilderBase
}
// patch before configure
Patcher::patchPHPBeforeConfigure($this);
SourcePatcher::patchPHPBuildconf($this);
shell()->cd(SOURCE_PATH . '/php-src')->exec('./buildconf --force');
Patcher::patchPHPConfigure($this);
SourcePatcher::patchPHPConfigure($this);
if ($this->getLib('libxml2') || $this->getExt('iconv')) {
$extra_libs .= ' -liconv';
@ -166,6 +166,8 @@ class MacOSBuilder extends BuilderBase
$this->configure_env
);
SourcePatcher::patchPHPAfterConfigure($this);
$this->cleanMake();
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {

View File

@ -20,7 +20,7 @@ declare(strict_types=1);
namespace SPC\builder\macos\library;
use SPC\util\Patcher;
use SPC\store\SourcePatcher;
class curl extends MacOSLibraryBase
{
@ -32,7 +32,7 @@ class curl extends MacOSLibraryBase
protected function build()
{
Patcher::patchCurlMacOS();
SourcePatcher::patchCurlMacOS();
$this->unixBuild();
}
}

View File

@ -6,6 +6,8 @@ namespace SPC\command;
use Psr\Log\LogLevel;
use SPC\ConsoleApplication;
use SPC\exception\ExceptionHandler;
use SPC\exception\WrongUsageException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -84,12 +86,23 @@ abstract class BaseCommand extends Command
if ($this->shouldExecute()) {
try {
return $this->handle();
} catch (\Throwable $e) {
} catch (WrongUsageException $e) {
$msg = explode("\n", $e->getMessage());
foreach ($msg as $v) {
logger()->error($v);
}
return self::FAILURE;
} catch (\Throwable $e) {
// 不开 debug 模式就不要再显示复杂的调试栈信息了
if ($this->getOption('debug')) {
ExceptionHandler::getInstance()->handle($e);
} else {
$msg = explode("\n", $e->getMessage());
foreach ($msg as $v) {
logger()->error($v);
}
}
return self::FAILURE;
}
}
return self::SUCCESS;

View File

@ -73,14 +73,22 @@ class BuildCliCommand extends BuildCommand
// 统计时间
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Build complete, used ' . $time . ' s !');
$build_root_path = BUILD_ROOT_PATH;
$cwd = getcwd();
$fixed = '';
if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) {
str_replace($cwd, '', $build_root_path);
$build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . $build_root_path;
$fixed = ' (host system)';
}
if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('Static php binary path: ' . BUILD_ROOT_PATH . '/bin/php');
logger()->info('Static php binary path' . $fixed . ': ' . $build_root_path . '/bin/php');
}
if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
logger()->info('phpmicro binary path: ' . BUILD_ROOT_PATH . '/bin/micro.sfx');
logger()->info('phpmicro binary path' . $fixed . ': ' . $build_root_path . '/bin/micro.sfx');
}
if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) {
logger()->info('Static php-fpm binary path: ' . BUILD_ROOT_PATH . '/bin/php-fpm');
logger()->info('Static php-fpm binary path' . $fixed . ': ' . $build_root_path . '/bin/php-fpm');
}
// 导出相关元数据
file_put_contents(BUILD_ROOT_PATH . '/build-extensions.json', json_encode($extensions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
@ -88,7 +96,7 @@ class BuildCliCommand extends BuildCommand
// 导出 LICENSE
$dumper = new LicenseDumper();
$dumper->addExts($extensions)->addLibs($libraries)->addSources(['php-src'])->dump(BUILD_ROOT_PATH . '/license');
logger()->info('License path: ' . BUILD_ROOT_PATH . '/license/');
logger()->info('License path' . $fixed . ': ' . $build_root_path . '/license/');
return 0;
} catch (WrongUsageException $e) {
logger()->critical($e->getMessage());

View File

@ -18,6 +18,7 @@ class DoctorCommand extends BaseCommand
$this->output->writeln('<info>Doctor check complete !</info>');
} catch (\Throwable $e) {
$this->output->writeln('<error>' . $e->getMessage() . '</error>');
pcntl_signal(SIGINT, SIG_IGN);
return 1;
}
return 0;

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\Config;
use SPC\store\Downloader;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('download', 'Download required sources', ['fetch'])]
class DownloadCommand extends BaseCommand
{
protected string $php_major_ver;
public function configure()
{
$this->addArgument('sources', InputArgument::REQUIRED, 'The sources will be compiled, comma separated');
$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->getOption('clean')) {
$input->setArgument('sources', '');
}
parent::initialize($input, $output);
}
/**
* @throws DownloaderException
* @throws RuntimeException
* @throws FileSystemException
*/
public function handle(): int
{
// 删除旧资源
if ($this->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(BUILD_ROOT_PATH);
logger()->alert('I will remove these dir after 5 seconds !');
sleep(5);
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('rmdir /s /q ' . SOURCE_PATH);
f_passthru('rmdir /s /q ' . DOWNLOAD_PATH);
f_passthru('rmdir /s /q ' . BUILD_ROOT_PATH);
} else {
f_passthru('rm -rf ' . SOURCE_PATH . '/*');
f_passthru('rm -rf ' . DOWNLOAD_PATH . '/*');
f_passthru('rm -rf ' . BUILD_ROOT_PATH . '/*');
}
return 0;
}
// Define PHP major version
$ver = $this->php_major_ver = $this->getOption('with-php') ?? '8.1';
define('SPC_BUILD_PHP_VERSION', $ver);
preg_match('/^\d+\.\d+$/', $ver, $matches);
if (!$matches) {
logger()->error("bad version arg: {$ver}, x.y required!");
return 1;
}
// Use shallow-clone can reduce git resource download
if ($this->getOption('shallow-clone')) {
define('GIT_SHALLOW_CLONE', true);
}
// To read config
Config::getSource('openssl');
// use openssl 1.1
if ($this->getOption('with-openssl11')) {
logger()->debug('Using openssl 1.1');
// 手动修改配置
Config::$source['openssl']['regex'] = '/href="(?<file>openssl-(?<version>1.[^"]+)\.tar\.gz)\"/';
}
// get source list that will be downloaded
$sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources'))));
if (empty($sources)) {
$sources = array_keys(Config::getSources());
}
$chosen_sources = $sources;
// Download them
f_mkdir(DOWNLOAD_PATH);
$cnt = count($chosen_sources);
$ni = 0;
foreach ($chosen_sources as $source) {
++$ni;
logger()->info("Fetching source {$source} [{$ni}/{$cnt}]");
Downloader::downloadSource($source, Config::getSource($source));
}
// 打印拉取资源用时
$time = round(microtime(true) - START_TIME, 3);
logger()->info('Download complete, used ' . $time . ' s !');
return 0;
}
}

View File

@ -1,234 +0,0 @@
<?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 SPC\util\Util;
use Symfony\Component\Console\Attribute\AsCommand;
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 */
#[AsCommand('fetch', 'Fetch required sources')]
class FetchSourceCommand extends BaseCommand
{
protected string $php_major_ver;
public function configure()
{
$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 handle(): int
{
try {
// 匹配版本
$ver = $this->php_major_ver = $this->getOption('with-php') ?? '8.1';
define('SPC_BUILD_PHP_VERSION', $ver);
preg_match('/^\d+\.\d+$/', $ver, $matches);
if (!$matches) {
logger()->error("bad version arg: {$ver}, x.y required!");
return 1;
}
// 删除旧资源
if ($this->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 ($this->getOption('shallow-clone')) {
define('GIT_SHALLOW_CLONE', true);
}
// 读取源配置随便读一个source用于缓存 source 配置
Config::getSource('openssl');
// 是否启用openssl11
if ($this->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(',', $this->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(',', $this->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 ($this->getOption('hash')) {
$hash = $this->doHash($chosen_sources);
$this->output->writeln($hash);
return 0;
}
// 创建目录
f_mkdir(SOURCE_PATH);
f_mkdir(DOWNLOAD_PATH);
// 下载 PHP
array_unshift($chosen_sources, 'php-src');
// 下载别的依赖资源
$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 ($this->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
{
// swow 需要软链接内部的文件夹才能正常编译
if (!file_exists(SOURCE_PATH . '/php-src/ext/swow') && Util::getPHPVersionID() >= 80000) {
Patcher::patchSwow();
// patch 一些 PHP 的资源,以便编译
Patcher::patchMicroThings();
}
// 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();
}
// 标记 patch 完成,避免重复 patch
file_put_contents(SOURCE_PATH . '/.patched', '');
}
}

View File

@ -38,7 +38,7 @@ class SortConfigCommand extends BaseCommand
case 'source':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/source.json'), true);
ConfigValidator::validateSource($file);
ksort($file);
uksort($file, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : ($a < $b ? -1 : 1)));
file_put_contents(ROOT_DIR . '/config/source.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
break;
case 'ext':

View File

@ -116,8 +116,12 @@ class CheckListHandler
private function emitFix(CheckResult $result)
{
pcntl_signal(SIGINT, function () {
$this->output->writeln('<error>You cancelled fix</error>');
});
$fix = $this->fix_map[$result->getFixItem()];
$fix_result = call_user_func($fix, ...$result->getFixParams());
pcntl_signal(SIGINT, SIG_IGN);
if ($fix_result) {
$this->output->writeln('<info>Fix done</info>');
} else {

View File

@ -39,4 +39,10 @@ class CheckResult
{
return empty($this->message);
}
public function setFixItem(string $fix_item = '', array $fix_params = [])
{
$this->fix_item = $fix_item;
$this->fix_params = $fix_params;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace SPC\doctor\item;
use SPC\builder\linux\SystemUtil;
use SPC\doctor\AsCheckItem;
use SPC\doctor\AsFixItem;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
class LinuxMuslCheck
{
#[AsCheckItem('if musl-libc is installed', limit_os: 'Linux')]
public function checkMusl(): ?CheckResult
{
$file = '/lib/ld-musl-x86_64.so.1';
$result = null;
if (file_exists($file)) {
return CheckResult::ok();
}
// non-exist, need to recognize distro
$distro = SystemUtil::getOSRelease();
return match ($distro['dist']) {
'ubuntu', 'alpine', 'debian' => CheckResult::fail('musl-libc is not installed on your system', 'fix-musl', [$distro]),
default => CheckResult::fail('musl-libc is not installed on your system'),
};
}
#[AsFixItem('fix-musl')]
public function fixMusl(array $distro): bool
{
$install_cmd = match ($distro['dist']) {
'ubuntu', 'debian' => 'apt install musl musl-tools -y',
'alpine' => 'apk add musl musl-utils musl-dev',
default => throw new RuntimeException('Current linux distro is not supported for auto-install musl packages'),
};
$prefix = '';
if (get_current_user() !== 'root') {
$prefix = 'sudo ';
logger()->warning('Current user is not root, using sudo for running command');
}
try {
shell(true)->exec($prefix . $install_cmd);
return true;
} catch (RuntimeException) {
return false;
}
}
}

View File

@ -125,4 +125,12 @@ class Config
}
return self::$ext;
}
public static function getSources(): array
{
if (self::$source === null) {
self::$source = FileSystem::loadConfigArray('source');
}
return self::$source;
}
}

View File

@ -148,6 +148,37 @@ class Downloader
return [$source['url'] . end($versions), end($versions), key($versions)];
}
/**
* @throws DownloaderException
* @throws RuntimeException
* @throws FileSystemException
*/
public static function downloadFile(string $name, string $url, string $filename, ?string $move_path = null): void
{
logger()->debug("Downloading {$url}");
pcntl_signal(SIGINT, function () use ($filename) {
if (file_exists(DOWNLOAD_PATH . '/' . $filename)) {
logger()->warning('Deleting download file: ' . $filename);
unlink(DOWNLOAD_PATH . '/' . $filename);
}
});
self::curlDown(url: $url, path: DOWNLOAD_PATH . "/{$filename}");
pcntl_signal(SIGINT, SIG_IGN);
logger()->debug("Locking {$filename}");
self::lockSource($name, ['source_type' => 'archive', 'filename' => $filename, 'move_path' => $move_path]);
}
public static function lockSource(string $name, array $data): void
{
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
$lock = [];
} else {
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
}
$lock[$name] = $data;
FileSystem::writeFile(DOWNLOAD_PATH . '/.lock.json', json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
/**
* 通过链接下载资源到本地并解压
*
@ -183,25 +214,32 @@ class Downloader
}
}
public static function downloadGit(string $name, string $url, string $branch, ?string $path = null): void
public static function downloadGit(string $name, string $url, string $branch, ?string $move_path = null): void
{
if ($path !== null) {
$path = SOURCE_PATH . "/{$path}";
} else {
$path = DOWNLOAD_PATH . "/{$name}";
}
$download_path = DOWNLOAD_PATH . "/{$name}";
if (file_exists($download_path)) {
logger()->notice("{$name} git source already fetched");
} else {
FileSystem::removeDir($download_path);
}
logger()->debug("cloning {$name} source");
$check = !defined('DEBUG_MODE') ? ' -q' : '';
pcntl_signal(SIGINT, function () use ($download_path) {
if (is_dir($download_path)) {
logger()->warning('Removing path ' . $download_path);
FileSystem::removeDir($download_path);
}
});
f_passthru(
'git clone' . $check .
' --config core.autocrlf=false ' .
"--branch \"{$branch}\" " . (defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '') . " --recursive \"{$url}\" \"{$download_path}\""
);
}
pcntl_signal(SIGINT, SIG_IGN);
// Lock
logger()->debug("Locking git source {$name}");
self::lockSource($name, ['source_type' => 'dir', 'dirname' => $name, 'move_path' => $move_path]);
/*
// 复制目录过去
if ($path !== $download_path) {
$dst_path = FileSystem::convertPath($path);
@ -215,64 +253,78 @@ class Downloader
f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"');
break;
}
}
}*/
}
/**
* 拉取资源
*
* @param string $name 资源名称
* @param array $source 资源参数,包含 type、path、rev、url、filename、regex、license
* @param null|array $source 资源参数,包含 type、path、rev、url、filename、regex、license
* @throws DownloaderException
* @throws RuntimeException
* @throws FileSystemException
* @throws RuntimeException
*/
public static function fetchSource(string $name, array $source): void
public static function downloadSource(string $name, ?array $source = null): void
{
// 如果是 git 类型,且没有设置 path那我就需要指定一下 path
if ($source['type'] === 'git' && !isset($source['path'])) {
$source['path'] = $name;
if ($source === null) {
$source = Config::getSource($name);
}
// 避免重复 fetch
if (!isset($source['path']) && is_dir(FileSystem::convertPath(DOWNLOAD_PATH . "/{$name}")) || isset($source['path']) && is_dir(FileSystem::convertPath(SOURCE_PATH . "/{$source['path']}"))) {
logger()->notice("{$name} source already extracted");
// load lock file
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
$lock = [];
} else {
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
}
// If lock file exists, skip downloading
if (isset($lock[$name])) {
if ($lock[$name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename'])) {
logger()->notice("source [{$name}] already downloaded: " . $lock[$name]['filename']);
return;
}
if ($lock[$name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname'])) {
logger()->notice("source [{$name}] already downloaded: " . $lock[$name]['dirname']);
return;
}
}
try {
switch ($source['type']) {
case 'bitbuckettag': // 从 BitBucket 的 Tag 拉取
[$url, $filename] = self::getLatestBitbucketTag($name, $source);
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
self::downloadFile($name, $url, $filename, $source['path'] ?? null);
break;
case 'ghtar': // 从 GitHub 的 TarBall 拉取
[$url, $filename] = self::getLatestGithubTarball($name, $source);
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
self::downloadFile($name, $url, $filename, $source['path'] ?? null);
break;
case 'ghtagtar': // 根据 GitHub 的 Tag 拉取相应版本的 Tar
[$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags');
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
self::downloadFile($name, $url, $filename, $source['path'] ?? null);
break;
case 'ghrel': // 通过 GitHub Release 来拉取
[$url, $filename] = self::getLatestGithubRelease($name, $source);
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
self::downloadFile($name, $url, $filename, $source['path'] ?? null);
break;
case 'filelist': // 通过网站提供的 filelist 使用正则提取后拉取
[$url, $filename] = self::getFromFileList($name, $source);
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
self::downloadFile($name, $url, $filename, $source['path'] ?? null);
break;
case 'url': // 通过直链拉取
$url = $source['url'];
$filename = $source['filename'] ?? basename($source['url']);
self::downloadUrl($name, $url, $filename, $source['path'] ?? null);
self::downloadFile($name, $url, $filename, $source['path'] ?? null);
break;
case 'git': // 通过拉回 Git 仓库的形式拉取
self::downloadGit($name, $source['url'], $source['rev'], $source['path'] ?? null);
break;
case 'custom':
case 'custom': // 自定义,可能是通过复杂 API 形式获取的文件,需要手写 crawler
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\\store\\source');
foreach ($classes as $class) {
if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) {
(new $class())->fetch();
break;
}
}
break;

View File

@ -6,10 +6,11 @@ namespace SPC\store;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use Symfony\Component\Finder\Iterator\RecursiveDirectoryIterator;
class FileSystem
{
private static array $_extract_hook = [];
/**
* @throws FileSystemException
*/
@ -118,20 +119,16 @@ class FileSystem
public static function copyDir(string $from, string $to): void
{
$iterator = new \RecursiveIteratorIterator(new RecursiveDirectoryIterator($from, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO), \RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $item) {
/**
* @var \SplFileInfo $item
*/
$target = $to . substr($item->getPathname(), strlen($from));
if ($item->isDir()) {
logger()->info("mkdir {$target}");
f_mkdir($target, recursive: true);
} else {
logger()->info("copying {$item} to {$target}");
@f_mkdir(dirname($target), recursive: true);
copy($item->getPathname(), $target);
}
$dst_path = FileSystem::convertPath($to);
$src_path = FileSystem::convertPath($from);
switch (PHP_OS_FAMILY) {
case 'Windows':
f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
break;
case 'Linux':
case 'Darwin':
f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"');
break;
}
}
@ -143,41 +140,56 @@ class FileSystem
* @throws FileSystemException
* @throws RuntimeException
*/
public static function extractSource(string $name, string $filename): void
public static function extractSource(string $name, string $filename, ?string $move_path = null): void
{
// if source hook is empty, load it
if (self::$_extract_hook === []) {
SourcePatcher::init();
}
if ($move_path !== null) {
$move_path = SOURCE_PATH . '/' . $move_path;
}
logger()->info("extracting {$name} source");
try {
if (PHP_OS_FAMILY !== 'Windows') {
if (f_mkdir(directory: SOURCE_PATH . "/{$name}", recursive: true) !== true) {
$target = $move_path ?? (SOURCE_PATH . "/{$name}");
// Git source, just move
if (is_dir($filename)) {
self::copyDir($filename, $target);
self::emitSourceExtractHook($name);
return;
}
if (PHP_OS_FAMILY === 'Darwin' || PHP_OS_FAMILY === 'Linux') {
if (f_mkdir(directory: $target, recursive: true) !== true) {
throw new FileSystemException('create ' . $name . 'source dir failed');
}
switch (self::extname($filename)) {
case 'xz':
case 'txz':
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
f_passthru("tar -xf {$filename} -C {$target} --strip-components 1");
// f_passthru("cat {$filename} | xz -d | tar -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
break;
case 'gz':
case 'tgz':
f_passthru("tar -xzf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1");
break;
case 'bz2':
f_passthru("tar -xjf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1");
break;
case 'zip':
f_passthru("unzip {$filename} -d " . SOURCE_PATH . "/{$name}");
f_passthru("unzip {$filename} -d {$target}");
break;
// case 'zstd':
// case 'zst':
// passthru('cat ' . $filename . ' | zstd -d | tar -x -C ".SOURCE_PATH . "/' . $name . ' --strip-components 1', $ret);
// break;
case 'tar':
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
f_passthru("tar -xf {$filename} -C {$target} --strip-components 1");
break;
default:
throw new FileSystemException('unknown archive format: ' . $filename);
}
} else {
} elseif (PHP_OS_FAMILY === 'Windows') {
// find 7z
$_7zExe = self::findCommandPath('7z', [
'C:\Program Files\7-Zip-Zstandard',
@ -201,13 +213,13 @@ class FileSystem
case 'gz':
case 'tgz':
case 'bz2':
f_passthru("\"{$_7zExe}\" x -so {$filename} | tar -f - -x -C " . SOURCE_PATH . "/{$name} --strip-components 1");
f_passthru("\"{$_7zExe}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1");
break;
case 'tar':
f_passthru("tar -xf {$filename} -C " . SOURCE_PATH . "/{$name} --strip-components 1");
f_passthru("tar -xf {$filename} -C {$target} --strip-components 1");
break;
case 'zip':
f_passthru("\"{$_7zExe}\" x {$filename} -o" . SOURCE_PATH . "/{$name}");
f_passthru("\"{$_7zExe}\" x {$filename} -o{$target}");
break;
default:
throw new FileSystemException("unknown archive format: {$filename}");
@ -421,4 +433,18 @@ class FileSystem
}
self::createDir($dir_name);
}
public static function addSourceExtractHook(string $name, callable $callback)
{
self::$_extract_hook[$name][] = $callback;
}
private static function emitSourceExtractHook(string $name)
{
foreach ((self::$_extract_hook[$name] ?? []) as $hook) {
if ($hook($name) === true) {
logger()->info('Patched source [' . $name . '] after extracted');
}
}
}
}

View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace SPC\store;
use SPC\builder\BuilderBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\builder\macos\MacOSBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\util\Util;
class SourcePatcher
{
public static function init()
{
FileSystem::addSourceExtractHook('swow', [SourcePatcher::class, 'patchSwow']);
FileSystem::addSourceExtractHook('micro', [SourcePatcher::class, 'patchMicro']);
FileSystem::addSourceExtractHook('openssl', [SourcePatcher::class, 'patchOpenssl3']);
FileSystem::addSourceExtractHook('openssl', [SourcePatcher::class, 'patchOpenssl11Darwin']);
}
public static function patchPHPBuildconf(BuilderBase $builder): void
{
if ($builder->getExt('curl')) {
logger()->info('patching before-configure for curl checks');
$file1 = "AC_DEFUN([PHP_CHECK_LIBRARY], [\n $3\n])";
$files = FileSystem::readFile(SOURCE_PATH . '/php-src/ext/curl/config.m4');
$file2 = 'AC_DEFUN([PHP_CHECK_LIBRARY], [
save_old_LDFLAGS=$LDFLAGS
ac_stuff="$5"
save_ext_shared=$ext_shared
ext_shared=yes
PHP_EVAL_LIBLINE([$]ac_stuff, LDFLAGS)
AC_CHECK_LIB([$1],[$2],[
LDFLAGS=$save_old_LDFLAGS
ext_shared=$save_ext_shared
$3
],[
LDFLAGS=$save_old_LDFLAGS
ext_shared=$save_ext_shared
unset ac_cv_lib_$1[]_$2
$4
])dnl
])';
file_put_contents(SOURCE_PATH . '/php-src/ext/curl/config.m4', $file1 . "\n" . $files . "\n" . $file2);
}
// if ($builder->getExt('pdo_sqlite')) {
// FileSystem::replaceFile()
// }
}
public static function patchSwow(): bool
{
if (Util::getPHPVersionID() >= 80000) {
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('cd ' . SOURCE_PATH . '/php-src/ext && mklink /D swow swow-src\ext');
} else {
f_passthru('cd ' . SOURCE_PATH . '/php-src/ext && ln -s swow-src/ext swow');
}
return true;
}
return false;
}
public static function patchPHPConfigure(BuilderBase $builder): void
{
$frameworks = $builder instanceof MacOSBuilder ? ' ' . $builder->getFrameworks(true) . ' ' : '';
$patch = [];
if ($curl = $builder->getExt('curl')) {
$patch[] = ['curl check', '/-lcurl/', $curl->getLibFilesString() . $frameworks];
}
if ($bzip2 = $builder->getExt('bz2')) {
$patch[] = ['bzip2 check', '/-lbz2/', $bzip2->getLibFilesString() . $frameworks];
}
if ($pdo_sqlite = $builder->getExt('pdo_sqlite')) {
$patch[] = ['pdo_sqlite linking', '/sqlite3_column_table_name=yes/', 'sqlite3_column_table_name=no'];
}
$patch[] = ['disable capstone', '/have_capstone="yes"/', 'have_capstone="no"'];
foreach ($patch as $item) {
logger()->info('Patching configure: ' . $item[0]);
FileSystem::replaceFile(SOURCE_PATH . '/php-src/configure', REPLACE_FILE_PREG, $item[1], $item[2]);
}
}
public static function patchUnixLibpng(): void
{
FileSystem::replaceFile(
SOURCE_PATH . '/libpng/configure',
REPLACE_FILE_STR,
'-lz',
BUILD_LIB_PATH . '/libz.a'
);
}
public static function patchCurlMacOS(): void
{
FileSystem::replaceFile(
SOURCE_PATH . '/curl/CMakeLists.txt',
REPLACE_FILE_PREG,
'/NOT COREFOUNDATION_FRAMEWORK/m',
'FALSE'
);
FileSystem::replaceFile(
SOURCE_PATH . '/curl/CMakeLists.txt',
REPLACE_FILE_PREG,
'/NOT SYSTEMCONFIGURATION_FRAMEWORK/m',
'FALSE'
);
}
public static function patchMicro(): bool
{
if (!file_exists(SOURCE_PATH . '/php-src/sapi/micro/php_micro.c')) {
return false;
}
$ver_file = SOURCE_PATH . '/php-src/main/php_version.h';
if (!file_exists($ver_file)) {
throw new FileSystemException('Patch failed, cannot find php source files');
}
$version_h = FileSystem::readFile(SOURCE_PATH . '/php-src/main/php_version.h');
preg_match('/#\s*define\s+PHP_MAJOR_VERSION\s+(\d+)\s+#\s*define\s+PHP_MINOR_VERSION\s+(\d+)\s+/m', $version_h, $match);
// $ver = "{$match[1]}.{$match[2]}";
$major_ver = $match[1] . $match[2];
if ($major_ver === '74') {
return false;
}
$check = !defined('DEBUG_MODE') ? ' -q' : '';
// f_passthru('cd ' . SOURCE_PATH . '/php-src && git checkout' . $check . ' HEAD');
$patch_list = [
'static_opcache',
'static_extensions_win32',
'cli_checks',
'disable_huge_page',
'vcruntime140',
'win32',
'zend_stream',
];
$patch_list = array_merge($patch_list, match (PHP_OS_FAMILY) {
'Windows' => [
'cli_static',
],
'Darwin' => [
'macos_iconv',
],
default => [],
});
$patches = [];
$serial = ['80', '81', '82'];
foreach ($patch_list as $patchName) {
if (file_exists(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}.patch")) {
$patches[] = "sapi/micro/patches/{$patchName}.patch";
continue;
}
for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) {
$tryMajMin = $serial[$i];
if (!file_exists(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}_{$tryMajMin}.patch")) {
continue;
}
$patches[] = "sapi/micro/patches/{$patchName}_{$tryMajMin}.patch";
continue 2;
}
throw new RuntimeException("failed finding patch {$patchName}");
}
$patchesStr = str_replace('/', DIRECTORY_SEPARATOR, implode(' ', $patches));
f_passthru(
'cd ' . SOURCE_PATH . '/php-src && ' .
(PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . $patchesStr . ' | patch -p1'
);
return true;
}
public static function patchOpenssl3(): bool
{
if (file_exists(SOURCE_PATH . '/openssl/VERSION.dat') && Util::getPHPVersionID() >= 80000) {
$openssl_c = file_get_contents(SOURCE_PATH . '/php-src/ext/openssl/openssl.c');
$openssl_c = preg_replace('/REGISTER_LONG_CONSTANT\s*\(\s*"OPENSSL_SSLV23_PADDING"\s*.+;/', '', $openssl_c);
file_put_contents(SOURCE_PATH . '/php-src/ext/openssl/openssl.c', $openssl_c);
return true;
}
return false;
}
public static function patchOpenssl11Darwin(): bool
{
if (PHP_OS_FAMILY === 'Darwin' && !file_exists(SOURCE_PATH . '/openssl/VERSION.dat') && file_exists(SOURCE_PATH . '/openssl/test/v3ext.c')) {
FileSystem::replaceFile(
SOURCE_PATH . '/openssl/test/v3ext.c',
REPLACE_FILE_STR,
'#include <stdio.h>',
'#include <stdio.h>' . PHP_EOL . '#include <string.h>'
);
return true;
}
return false;
}
public static function patchPHPAfterConfigure(BuilderBase $param)
{
if ($param instanceof LinuxBuilder) {
FileSystem::replaceFile(SOURCE_PATH . '/php-src/main/php_config.h', REPLACE_FILE_PREG, '/^#define HAVE_STRLCPY 1$/m', '');
FileSystem::replaceFile(SOURCE_PATH . '/php-src/main/php_config.h', REPLACE_FILE_PREG, '/^#define HAVE_STRLCAT 1$/m', '');
}
}
}

View File

@ -6,7 +6,6 @@ namespace SPC\store\source;
use JetBrains\PhpStorm\ArrayShape;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\Downloader;
@ -16,13 +15,12 @@ class PhpSource extends CustomSourceBase
/**
* @throws DownloaderException
* @throws FileSystemException
* @throws RuntimeException
*/
public function fetch()
{
$major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.1';
Downloader::fetchSource('php-src', self::getLatestPHPInfo($major));
Downloader::downloadSource('php-src', self::getLatestPHPInfo($major));
}
/**

View File

@ -12,7 +12,7 @@ class PostgreSQLSource extends CustomSourceBase
public function fetch()
{
Downloader::fetchSource('postgresql', self::getLatestInfo());
Downloader::downloadSource('postgresql', self::getLatestInfo());
}
public function getLatestInfo(): array

View File

@ -4,261 +4,12 @@ declare(strict_types=1);
namespace SPC\util;
use SPC\builder\BuilderBase;
use SPC\builder\macos\MacOSBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
class Patcher
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
public static function patchMicroThings(): void
{
$ver_file = SOURCE_PATH . '/php-src/main/php_version.h';
if (!file_exists($ver_file)) {
throw new FileSystemException('Patch failed, cannot find php source files');
}
$version_h = FileSystem::readFile(SOURCE_PATH . '/php-src/main/php_version.h');
preg_match('/#\s*define\s+PHP_MAJOR_VERSION\s+(\d+)\s+#\s*define\s+PHP_MINOR_VERSION\s+(\d+)\s+/m', $version_h, $match);
// $ver = "{$match[1]}.{$match[2]}";
logger()->info('Patching php');
$major_ver = $match[1] . $match[2];
if ($major_ver === '74') {
return;
}
$check = !defined('DEBUG_MODE') ? ' -q' : '';
// f_passthru('cd ' . SOURCE_PATH . '/php-src && git checkout' . $check . ' HEAD');
$patch_list = [
'static_opcache',
'static_extensions_win32',
'cli_checks',
'disable_huge_page',
'vcruntime140',
'win32',
'zend_stream',
];
$patch_list = array_merge($patch_list, match (PHP_OS_FAMILY) {
'Windows' => [
'cli_static',
],
'Darwin' => [
'macos_iconv',
],
default => [],
});
$patches = [];
$serial = ['80', '81', '82'];
foreach ($patch_list as $patchName) {
if (file_exists(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}.patch")) {
$patches[] = "sapi/micro/patches/{$patchName}.patch";
continue;
}
for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) {
$tryMajMin = $serial[$i];
if (!file_exists(SOURCE_PATH . "/php-src/sapi/micro/patches/{$patchName}_{$tryMajMin}.patch")) {
continue;
}
$patches[] = "sapi/micro/patches/{$patchName}_{$tryMajMin}.patch";
continue 2;
}
throw new RuntimeException("failed finding patch {$patchName}");
}
$patchesStr = str_replace('/', DIRECTORY_SEPARATOR, implode(' ', $patches));
f_passthru(
'cd ' . SOURCE_PATH . '/php-src && ' .
(PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . $patchesStr . ' | patch -p1'
);
}
public static function patchOpenssl3(): void
{
logger()->info('Patching PHP with openssl 3.0');
$openssl_c = file_get_contents(SOURCE_PATH . '/php-src/ext/openssl/openssl.c');
$openssl_c = preg_replace('/REGISTER_LONG_CONSTANT\s*\(\s*"OPENSSL_SSLV23_PADDING"\s*.+;/', '', $openssl_c);
file_put_contents(SOURCE_PATH . '/php-src/ext/openssl/openssl.c', $openssl_c);
}
/**
* @throws RuntimeException
*/
public static function patchSwow(): void
{
logger()->info('Patching swow');
if (PHP_OS_FAMILY === 'Windows') {
f_passthru('cd ' . SOURCE_PATH . '/php-src/ext && mklink /D swow swow-src\ext');
} else {
f_passthru('cd ' . SOURCE_PATH . '/php-src/ext && ln -s swow-src/ext swow');
}
}
public static function patchPHPBeforeConfigure(BuilderBase $builder): void
{
if ($builder->getExt('curl')) {
logger()->info('patching before-configure for curl checks');
$file1 = "AC_DEFUN([PHP_CHECK_LIBRARY], [\n $3\n])";
$files = FileSystem::readFile(SOURCE_PATH . '/php-src/ext/curl/config.m4');
$file2 = 'AC_DEFUN([PHP_CHECK_LIBRARY], [
save_old_LDFLAGS=$LDFLAGS
ac_stuff="$5"
save_ext_shared=$ext_shared
ext_shared=yes
PHP_EVAL_LIBLINE([$]ac_stuff, LDFLAGS)
AC_CHECK_LIB([$1],[$2],[
LDFLAGS=$save_old_LDFLAGS
ext_shared=$save_ext_shared
$3
],[
LDFLAGS=$save_old_LDFLAGS
ext_shared=$save_ext_shared
unset ac_cv_lib_$1[]_$2
$4
])dnl
])';
file_put_contents(SOURCE_PATH . '/php-src/ext/curl/config.m4', $file1 . "\n" . $files . "\n" . $file2);
}
// if ($builder->getExt('pdo_sqlite')) {
// FileSystem::replaceFile()
// }
}
/**
* @throws FileSystemException
* @throws RuntimeException
*/
public static function patchPHPConfigure(BuilderBase $builder): void
{
$frameworks = $builder instanceof MacOSBuilder ? ' ' . $builder->getFrameworks(true) . ' ' : '';
$curl = $builder->getExt('curl');
if ($curl) {
logger()->info('patching configure for curl checks');
FileSystem::replaceFile(
SOURCE_PATH . '/php-src/configure',
REPLACE_FILE_PREG,
'/-lcurl/',
$curl->getLibFilesString() . $frameworks
);
}
$bzip2 = $builder->getExt('bz2');
if ($bzip2) {
logger()->info('patching configure for bzip2 checks');
FileSystem::replaceFile(
SOURCE_PATH . '/php-src/configure',
REPLACE_FILE_PREG,
'/-lbz2/',
$bzip2->getLibFilesString() . $frameworks
);
}
$pdo_sqlite = $builder->getExt('pdo_sqlite');
if ($pdo_sqlite) {
logger()->info('patching configure for pdo_sqlite linking');
FileSystem::replaceFile(
SOURCE_PATH . '/php-src/configure',
REPLACE_FILE_PREG,
'/sqlite3_column_table_name=yes/',
'sqlite3_column_table_name=no'
);
}
logger()->info('patching configure for disable capstone');
FileSystem::replaceFile(
SOURCE_PATH . '/php-src/configure',
REPLACE_FILE_PREG,
'/have_capstone="yes"/',
'have_capstone="no"'
);
if (property_exists($builder, 'arch') && php_uname('m') !== $builder->arch) {
// cross-compiling
switch ($builder->arch) {
case 'aarch64':
case 'arm64':
// almost all bsd/linux supports this
logger()->info('patching configure for shm mmap checks (cross-compiling)');
FileSystem::replaceFile(
SOURCE_PATH . '/php-src/configure',
REPLACE_FILE_PREG,
'/have_shm_mmap_anon=no/',
'have_shm_mmap_anon=yes'
);
FileSystem::replaceFile(
SOURCE_PATH . '/php-src/configure',
REPLACE_FILE_PREG,
'/have_shm_mmap_posix=no/',
'have_shm_mmap_posix=yes'
);
break;
case 'x86_64':
break;
default:
throw new RuntimeException('unsupported arch while patching php configure: ' . $builder->arch);
}
}
}
public static function patchUnixLibpng(): void
{
FileSystem::replaceFile(
SOURCE_PATH . '/libpng/configure',
REPLACE_FILE_STR,
'-lz',
BUILD_LIB_PATH . '/libz.a'
);
}
public static function patchCurlMacOS(): void
{
FileSystem::replaceFile(
SOURCE_PATH . '/curl/CMakeLists.txt',
REPLACE_FILE_PREG,
'/NOT COREFOUNDATION_FRAMEWORK/m',
'FALSE'
);
FileSystem::replaceFile(
SOURCE_PATH . '/curl/CMakeLists.txt',
REPLACE_FILE_PREG,
'/NOT SYSTEMCONFIGURATION_FRAMEWORK/m',
'FALSE'
);
}
/**
* @throws FileSystemException
*/
public static function patchDarwinOpenssl11(): void
{
FileSystem::replaceFile(
SOURCE_PATH . '/openssl/test/v3ext.c',
REPLACE_FILE_STR,
'#include <stdio.h>',
'#include <stdio.h>' . PHP_EOL . '#include <string.h>'
);
}
public static function patchLinuxPkgConfig(string $path): void
{
logger()->info("fixing pc {$path}");
$workspace = BUILD_ROOT_PATH;
if ($workspace === '/') {
$workspace = '';
}
$content = file_get_contents($path);
$content = preg_replace('/^prefix=.+$/m', "prefix={$workspace}", $content);
$content = preg_replace('/^libdir=.+$/m', 'libdir=${prefix}/lib', $content);
$content = preg_replace('/^includedir=.+$/m', 'includedir=${prefix}/include', $content);
file_put_contents($path, $content);
}
/**
* @throws FileSystemException
* @throws RuntimeException

View File

@ -57,4 +57,9 @@ const PKGCONF_PATCH_LIBDIR = 4;
const PKGCONF_PATCH_INCLUDEDIR = 8;
const PKGCONF_PATCH_ALL = 15;
// Custom download type
const DOWNLOAD_TYPE_NONE = 0;
const DOWNLOAD_TYPE_ARCHIVE = 1;
const DOWNLOAD_TYPE_DIR = 2;
ConsoleLogger::$date_format = 'H:i:s';