diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a7166ec2e..13441a81f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,11 @@ > If your PR involves the changes mentioned below and completed the action, please tick the corresponding option. > If a modification is not involved, please skip it directly. -- [ ] If you modified `*.php`, run `composer cs-fix` at local machine. -- [ ] If it's an extension or dependency update, make sure adding related extensions in `src/global/test-extensions.php`. -- [ ] If you changed the behavior of static-php-cli, update docs in `./docs/`. -- [ ] If you updated `config/xxx.json` content, run `bin/spc dev:sort-config xxx`. +- If you modified `*.php` or `*.json`, run them locally to ensure your changes are valid: + - [ ] `PHP_CS_FIXER_IGNORE_ENV=1 composer cs-fix` + - [ ] `composer analyse` + - [ ] `composer test` + - [ ] `bin/spc dev:sort-config` +- If it's an extension or dependency update, please ensure the following: + - [ ] Add your test combination to `src/globals/test-extensions.php`. + - [ ] If adding new or fixing bugs, add commit message containing `extension test` or `test extensions` to trigger full test suite. diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index da30d0086..1c04f5c93 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -1,16 +1,14 @@ -name: "Extension matrix tests" +name: "Extension Matrix Tests" on: workflow_dispatch: - pull_request: - branches: [ "main" ] - paths: - - '.github/workflows/ext-matrix-tests.yml' + push: jobs: test: name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" runs-on: ${{ matrix.operating-system }} + if: contains(github.event.head_commit.message, 'extension test') || contains(github.event.head_commit.message, 'test extensions') strategy: fail-fast: false matrix: diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker index b69e07101..9583b4dd9 100755 --- a/bin/spc-gnu-docker +++ b/bin/spc-gnu-docker @@ -94,7 +94,7 @@ ENV PATH="/app/bin:/cmake/bin:$PATH" ENV SPC_LIBC=glibc ADD ./config/env.ini /app/config/env.ini -RUN bin/spc doctor --auto-fix --debug +RUN CC=gcc bin/spc doctor --auto-fix --debug RUN curl -o make.tgz -fsSL https://ftp.gnu.org/gnu/make/make-4.4.tar.gz && \ tar -zxvf make.tgz && \ diff --git a/config/env.ini b/config/env.ini index ba8652d5f..895a08e9b 100644 --- a/config/env.ini +++ b/config/env.ini @@ -42,6 +42,9 @@ SPC_CONCURRENCY=${CPU_COUNT} SPC_SKIP_PHP_VERSION_CHECK="no" ; Ignore some check item for bin/spc doctor command, comma separated (e.g. SPC_SKIP_DOCTOR_CHECK_ITEMS="if homebrew has installed") SPC_SKIP_DOCTOR_CHECK_ITEMS="" +; extra modules that xcaddy will include in the FrankenPHP build +SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/frankenphp/caddy --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" + ; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them ; only useful for builds targeting not pure-static linking ; default paths @@ -68,8 +71,8 @@ CXX=${SPC_LINUX_DEFAULT_CXX} AR=${SPC_LINUX_DEFAULT_AR} LD=ld.gold ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build -SPC_DEFAULT_C_FLAGS="-fpic -Os" -SPC_DEFAULT_CXX_FLAGS="-fpic -Os" +SPC_DEFAULT_C_FLAGS="-fPIC -Os" +SPC_DEFAULT_CXX_FLAGS="-fPIC -Os" ; extra libs for building php executable, used in `make` command for building php (this value may changed by extension build process, space separated) SPC_EXTRA_LIBS= ; upx executable path @@ -89,7 +92,7 @@ SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; *** default build vars for building php *** ; CFLAGS for configuring php -SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fpie" +SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fPIE" ; CPPFLAGS for configuring php SPC_CMD_VAR_PHP_CONFIGURE_CPPFLAGS="-I${BUILD_INCLUDE_PATH}" ; LDFLAGS for configuring php @@ -97,7 +100,7 @@ SPC_CMD_VAR_PHP_CONFIGURE_LDFLAGS="-L${BUILD_LIB_PATH}" ; LIBS for configuring php SPC_CMD_VAR_PHP_CONFIGURE_LIBS="-ldl -lpthread -lm" ; EXTRA_CFLAGS for `make` php -SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fpie ${SPC_DEFAULT_C_FLAGS}" +SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}" ; EXTRA_LIBS for `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS="-ldl -lpthread -lm" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so diff --git a/config/pkg.json b/config/pkg.json index 5760c0b18..a2ec8a146 100644 --- a/config/pkg.json +++ b/config/pkg.json @@ -42,5 +42,17 @@ "extract-files": { "upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe" } + }, + "go-mod-frankenphp-x86_64-linux": { + "type": "custom" + }, + "go-mod-frankenphp-aarch64-linux": { + "type": "custom" + }, + "go-mod-frankenphp-x86_64-macos": { + "type": "custom" + }, + "go-mod-frankenphp-aarch64-macos": { + "type": "custom" } } diff --git a/docs/deps-craft-yml.md b/docs/deps-craft-yml.md index 9e660be88..0ee0e11d0 100644 --- a/docs/deps-craft-yml.md +++ b/docs/deps-craft-yml.md @@ -42,6 +42,9 @@ build-options: # Set micro SAPI as win32 mode, without this, micro SAPI will be compiled as a console application (only for Windows, default: false) enable-micro-win32: false +# Build options for shared extensions (list or comma-separated are both accepted) +shared-extensions: [ ] + # Download options download-options: # Use custom url for specified sources, format: "{source-name}:{url}" (e.g. "php-src:https://example.com/php-8.4.0.tar.gz") diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index e9caabfce..6303ffec5 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -401,6 +401,9 @@ public function getBuildTypeName(int $type): string if (($type & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) { $ls[] = 'embed'; } + if (($type & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { + $ls[] = 'frankenphp'; + } return implode(', ', $ls); } @@ -507,6 +510,26 @@ public function emitPatchPoint(string $point_name): void } } + public function checkBeforeBuildPHP(int $rule): void + { + if (($rule & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { + // frankenphp only support linux and macOS + if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { + throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); + } + // frankenphp needs package go-mod-frankenphp installed + $pkg_dir = PKG_ROOT_PATH . '/go-mod-frankenphp-' . arch2gnu(php_uname('m')) . '-' . osfamily2shortname(); + if (!file_exists("{$pkg_dir}/bin/go") || !file_exists("{$pkg_dir}/bin/xcaddy")) { + global $argv; + throw new WrongUsageException("FrankenPHP SAPI requires go-mod-frankenphp package, please install it first: {$argv[0]} install-pkg go-mod-frankenphp"); + } + // frankenphp needs libxml2 libs + if (PHP_OS_FAMILY === 'Darwin' && !$this->getLib('libxml2')) { + throw new WrongUsageException('FrankenPHP SAPI for macOS requires libxml2 library, please include `xml` extension in your build.'); + } + } + } + /** * Generate micro extension test php code. */ diff --git a/src/SPC/builder/freebsd/BSDBuilder.php b/src/SPC/builder/freebsd/BSDBuilder.php index 04fd43d30..65ebea571 100644 --- a/src/SPC/builder/freebsd/BSDBuilder.php +++ b/src/SPC/builder/freebsd/BSDBuilder.php @@ -96,6 +96,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; shell()->cd(SOURCE_PATH . '/php-src') ->exec( @@ -143,6 +144,10 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void } $this->buildEmbed(); } + if ($enableFrankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 315d7df99..addd2f575 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -110,10 +110,11 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ? ('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : ''; - $enable_cli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; - $enable_fpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; - $enable_micro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; - $enable_embed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; + $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; + $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; + $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; $mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : ''; // prepare build php envs @@ -125,7 +126,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void ]); // process micro upx patch if micro sapi enabled - if ($enable_micro) { + if ($enableMicro) { if (version_compare($this->getMicroVersion(), '0.2.0') < 0) { // for phpmicro 0.1.x $this->processMicroUPXLegacy(); @@ -137,10 +138,10 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void shell()->cd(SOURCE_PATH . '/php-src') ->exec( getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . - ($enable_cli ? '--enable-cli ' : '--disable-cli ') . - ($enable_fpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . - ($enable_embed ? "--enable-embed={$embed_type} " : '--disable-embed ') . - ($enable_micro ? '--enable-micro=all-static ' : '--disable-micro ') . + ($enableCli ? '--enable-cli ' : '--disable-cli ') . + ($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . + ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . + ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') . $config_file_path . $config_file_scan_dir . $disable_jit . @@ -156,25 +157,29 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $this->cleanMake(); - if ($enable_cli) { + if ($enableCli) { logger()->info('building cli'); $this->buildCli(); } - if ($enable_fpm) { + if ($enableFpm) { logger()->info('building fpm'); $this->buildFpm(); } - if ($enable_micro) { + if ($enableMicro) { logger()->info('building micro'); $this->buildMicro(); } - if ($enable_embed) { + if ($enableEmbed) { logger()->info('building embed'); - if ($enable_micro) { + if ($enableMicro) { FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); } $this->buildEmbed(); } + if ($enableFrankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/builder/macos/MacOSBuilder.php b/src/SPC/builder/macos/MacOSBuilder.php index a05221201..153da08c3 100644 --- a/src/SPC/builder/macos/MacOSBuilder.php +++ b/src/SPC/builder/macos/MacOSBuilder.php @@ -122,6 +122,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; // prepare build php envs $mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : ''; @@ -180,6 +181,10 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void } $this->buildEmbed(); } + if ($enableFrankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 0d367de19..adde474a7 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -12,6 +12,7 @@ use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; +use SPC\store\Downloader; use SPC\store\FileSystem; use SPC\util\DependencyUtil; use SPC\util\SPCConfigUtil; @@ -218,6 +219,21 @@ protected function sanityCheck(int $build_target): void throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output)); } } + + // sanity check for frankenphp + if (($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { + logger()->info('running frankenphp sanity check'); + $frankenphp = BUILD_BIN_PATH . '/frankenphp'; + if (!file_exists($frankenphp)) { + throw new RuntimeException('FrankenPHP binary not found: ' . $frankenphp); + } + [$ret, $output] = shell() + ->setEnv(['LD_LIBRARY_PATH' => BUILD_LIB_PATH]) + ->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']'); + } + } } /** @@ -277,4 +293,64 @@ protected function patchPhpScripts(): void FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str); } } + + /** + * @throws WrongUsageException + * @throws RuntimeException + */ + protected function buildFrankenphp(): void + { + $os = match (PHP_OS_FAMILY) { + 'Linux' => 'linux', + 'Windows' => 'win', + 'Darwin' => 'macos', + 'BSD' => 'freebsd', + default => throw new RuntimeException('Unsupported OS: ' . PHP_OS_FAMILY), + }; + $arch = arch2gnu(php_uname('m')); + + // define executables for go and xcaddy + $xcaddy_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/xcaddy"; + + $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; + $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; + $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); + // make it possible to build from a different frankenphp directory! + if (!str_contains($xcaddyModules, '--with github.com/dunglas/frankenphp')) { + $xcaddyModules = '--with github.com/dunglas/frankenphp ' . $xcaddyModules; + } + if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { + logger()->warning('caddy-cbrotli module is enabled, but brotli library is not built. Disabling caddy-cbrotli.'); + $xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules); + } + $lrt = PHP_OS_FAMILY === 'Linux' ? '-lrt' : ''; + $releaseInfo = json_decode(Downloader::curlExec('https://api.github.com/repos/php/frankenphp/releases/latest'), true); + $frankenPhpVersion = $releaseInfo['tag_name']; + $libphpVersion = $this->getPHPVersion(); + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $libphpVersion = preg_replace('/\.\d$/', '', $libphpVersion); + } + $debugFlags = $this->getOption('--with-debug') ? "'-w -s' " : ''; + + $config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list, with_dependencies: true); + + $env = [ + 'PATH' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin:" . getenv('PATH'), + 'GOROOT' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}", + 'GOBIN' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin", + 'GOPATH' => PKG_ROOT_PATH . '/go', + 'CGO_ENABLED' => '1', + 'CGO_CFLAGS' => $config['cflags'], + 'CGO_LDFLAGS' => "{$config['ldflags']} {$config['libs']} {$lrt}", + 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . + '-ldflags \"-linkmode=external -extldflags \'-pie\' ' . $debugFlags . + '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . + "{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . + "-tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + shell()->cd(BUILD_BIN_PATH) + ->setEnv($env) + ->exec("{$xcaddy_exec} build --output frankenphp {$xcaddyModules}"); + } } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 353605b12..29cedf8f3 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -32,7 +32,8 @@ public function configure(): void $this->addOption('build-micro', null, null, 'Build micro SAPI'); $this->addOption('build-cli', null, null, 'Build cli SAPI'); $this->addOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); - $this->addOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); + $this->addOption('build-embed', null, InputOption::VALUE_OPTIONAL, 'Build embed SAPI (not available on Windows)'); + $this->addOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); $this->addOption('build-all', null, null, 'Build all SAPI'); $this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions'); $this->addOption('disable-opcache-jit', null, null, 'disable opcache jit'); @@ -83,7 +84,8 @@ public function handle(): int $this->output->writeln("\t--build-micro\tBuild phpmicro SAPI"); $this->output->writeln("\t--build-fpm\tBuild php-fpm SAPI"); $this->output->writeln("\t--build-embed\tBuild embed SAPI/libphp"); - $this->output->writeln("\t--build-all\tBuild all SAPI: cli, micro, fpm, embed"); + $this->output->writeln("\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp"); + $this->output->writeln("\t--build-all\tBuild all SAPI: cli, micro, fpm, embed, frankenphp"); return static::FAILURE; } if ($rule === BUILD_TARGET_ALL) { @@ -194,6 +196,9 @@ public function handle(): int // validate libs and extensions $builder->validateLibsAndExts(); + // check some things before building all the things + $builder->checkBeforeBuildPHP($rule); + // clean builds and sources if ($this->input->getOption('with-clean')) { logger()->info('Cleaning source and previous build dir...'); @@ -303,7 +308,18 @@ private function parseRules(array $shared_extensions = []): int $rule |= ($this->getOption('build-cli') ? BUILD_TARGET_CLI : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE); + $embed = $this->getOption('build-embed'); + if (!$embed && !empty($shared_extensions)) { + $embed = true; + } + if ($embed) { + if ($embed === true) { + $embed = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + } + $rule |= BUILD_TARGET_EMBED; + f_putenv('SPC_CMD_VAR_PHP_EMBED_TYPE=' . ($embed === 'static' ? 'static' : 'shared')); + } + $rule |= ($this->getOption('build-frankenphp') ? (BUILD_TARGET_FRANKENPHP | BUILD_TARGET_EMBED) : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; } diff --git a/src/SPC/command/CraftCommand.php b/src/SPC/command/CraftCommand.php index 8d3cc2bee..9a2ac4419 100644 --- a/src/SPC/command/CraftCommand.php +++ b/src/SPC/command/CraftCommand.php @@ -49,7 +49,7 @@ public function handle(): int } $static_extensions = implode(',', $craft['extensions']); - $shared_extensions = implode(',', $craft['shared-extensions']); + $shared_extensions = implode(',', $craft['shared-extensions'] ?? []); $libs = implode(',', $craft['libs']); // init log diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index 919d1f15d..9435202fa 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -9,6 +9,7 @@ use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; +use SPC\store\pkg\CustomPackage; use SPC\store\source\CustomSourceBase; /** @@ -358,10 +359,13 @@ public static function downloadPackage(string $name, ?array $pkg = null, bool $f ]); break; case 'custom': // Custom download method, like API-based download or other - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'); + $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); foreach ($classes as $class) { - if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { - (new $class())->fetch($force); + if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { + $cls = new $class(); + if (in_array($name, $cls->getSupportName())) { + (new $class())->fetch($name, $force, $pkg); + } break; } } @@ -646,6 +650,17 @@ private static function isAlreadyDownloaded(string $name, bool $force, int $down return true; } } + + // If lock file exists, skip downloading for source mode + if (!$force && $download_as === SPC_DOWNLOAD_PACKAGE && isset($lock[$name])) { + if ( + $lock[$name]['source_type'] === SPC_SOURCE_ARCHIVE && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename']) || + $lock[$name]['source_type'] === SPC_SOURCE_GIT && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname']) + ) { + logger()->notice("Package [{$name}] already downloaded: " . ($lock[$name]['filename'] ?? $lock[$name]['dirname'])); + return true; + } + } return false; } } diff --git a/src/SPC/store/PackageManager.php b/src/SPC/store/PackageManager.php index ae2c93800..d5c1043d3 100644 --- a/src/SPC/store/PackageManager.php +++ b/src/SPC/store/PackageManager.php @@ -6,6 +6,7 @@ use SPC\exception\FileSystemException; use SPC\exception\WrongUsageException; +use SPC\store\pkg\CustomPackage; class PackageManager { @@ -32,6 +33,20 @@ public static function installPackage(string $pkg_name, ?array $config = null, b // Download package Downloader::downloadPackage($pkg_name, $config, $force); + if (Config::getPkg($pkg_name)['type'] === 'custom') { + // Custom extract function + $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); + foreach ($classes as $class) { + if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { + $cls = new $class(); + if (in_array($pkg_name, $cls->getSupportName())) { + (new $class())->extract($pkg_name); + break; + } + } + } + return; + } // After download, read lock file name $lock = LockFile::get($pkg_name); $source_type = $lock['source_type']; diff --git a/src/SPC/store/pkg/CustomPackage.php b/src/SPC/store/pkg/CustomPackage.php new file mode 100644 index 000000000..89edb17e3 --- /dev/null +++ b/src/SPC/store/pkg/CustomPackage.php @@ -0,0 +1,17 @@ + 'amd64', + 'aarch64' => 'arm64', + default => throw new \InvalidArgumentException('Unsupported architecture: ' . $name), + }; + $os = match (explode('-', $name)[4]) { + 'linux' => 'linux', + 'macos' => 'darwin', + default => throw new \InvalidArgumentException('Unsupported OS: ' . $name), + }; + $go_version = '1.24.4'; + $config = [ + 'type' => 'url', + 'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz", + ]; + Downloader::downloadPackage($name, $config, $force); + } + + public function extract(string $name): void + { + $pkgroot = PKG_ROOT_PATH; + $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true); + $source_type = $lock[$name]['source_type']; + $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); + $extract = $lock[$name]['move_path'] === null ? "{$pkgroot}/{$name}" : $lock[$name]['move_path']; + + FileSystem::extractPackage($name, $source_type, $filename, $extract); + + // install xcaddy + $go_exec = PKG_ROOT_PATH . "{$pkgroot}/{$name}/bin/go"; + // $xcaddy_exec = PKG_ROOT_PATH . "$pkgroot/$name/bin/xcaddy"; + shell() + ->appendEnv([ + 'PATH' => "{$pkgroot}/{$name}/bin:" . getenv('PATH'), + 'GOROOT' => "{$pkgroot}/{$name}", + 'GOBIN' => "{$pkgroot}/{$name}/bin", + 'GOPATH' => "{$pkgroot}/go", + ]) + ->exec("{$go_exec} install github.com/caddyserver/xcaddy/cmd/xcaddy@latest"); + } +} diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index eae2ef2bd..dbc8ce236 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -164,6 +164,12 @@ public static function validateAndParseCraftFile(mixed $craft_file, Command $com if (is_string($craft['extensions'])) { $craft['extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['extensions']))); } + if (!isset($craft['shared-extensions'])) { + $craft['shared-extensions'] = []; + } + if (is_string($craft['shared-extensions'] ?? [])) { + $craft['shared-extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['shared-extensions']))); + } // check libs if (isset($craft['libs']) && is_string($craft['libs'])) { $craft['libs'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['libs']))); diff --git a/src/SPC/util/UnixShell.php b/src/SPC/util/UnixShell.php index 0f320d50c..bb4e8d5eb 100644 --- a/src/SPC/util/UnixShell.php +++ b/src/SPC/util/UnixShell.php @@ -44,14 +44,7 @@ public function exec(string $cmd): UnixShell { /* @phpstan-ignore-next-line */ logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); - $env_str = $this->getEnvString(); - if (!empty($env_str)) { - $cmd = "{$env_str} {$cmd}"; - } - if ($this->cd !== null) { - $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } + $cmd = $this->getExecString($cmd); if (!$this->debug) { $cmd .= ' 1>/dev/null 2>&1'; } @@ -99,10 +92,7 @@ public function execWithResult(string $cmd, bool $with_log = true): array /* @phpstan-ignore-next-line */ logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd)); } - logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); - if ($this->cd !== null) { - $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } + $cmd = $this->getExecString($cmd); exec($cmd, $out, $code); return [$code, $out]; } @@ -126,4 +116,21 @@ public function getEnvString(): string } return trim($str); } + + /** + * @param string $cmd + * @return string + */ + private function getExecString(string $cmd): string + { + logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); + $env_str = $this->getEnvString(); + if (!empty($env_str)) { + $cmd = "{$env_str} {$cmd}"; + } + if ($this->cd !== null) { + $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; + } + return $cmd; + } } diff --git a/src/globals/defines.php b/src/globals/defines.php index eab2fcc4e..ab37ace9b 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -62,7 +62,8 @@ const BUILD_TARGET_MICRO = 2; // build micro const BUILD_TARGET_FPM = 4; // build fpm const BUILD_TARGET_EMBED = 8; // build embed -const BUILD_TARGET_ALL = 15; // build all +const BUILD_TARGET_FRANKENPHP = 16; // build frankenphp +const BUILD_TARGET_ALL = BUILD_TARGET_CLI | BUILD_TARGET_MICRO | BUILD_TARGET_FPM | BUILD_TARGET_EMBED | BUILD_TARGET_FRANKENPHP; // build all // doctor error fix policy const FIX_POLICY_DIE = 1; // die directly diff --git a/src/globals/functions.php b/src/globals/functions.php index 8718f4abf..998b2d1cc 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -102,6 +102,17 @@ function osfamily2dir(): string }; } +function osfamily2shortname(): string +{ + return match (PHP_OS_FAMILY) { + 'Windows' => 'win', + 'Darwin' => 'macos', + 'Linux' => 'linux', + 'BSD' => 'bsd', + default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY), + }; +} + function shell(?bool $debug = null): UnixShell { /* @noinspection PhpUnhandledExceptionInspection */ diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index cfde83f6c..79acc9849 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -40,6 +40,9 @@ // compress with upx $upx = false; +// whether to test frankenphp build, only available for macos and linux +$frankenphp = true; + // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; @@ -208,7 +211,13 @@ function quote2(string $param): string passthru($prefix . $build_cmd . ' --build-cli --build-micro', $retcode); break; case 'build_embed_cmd': - passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : ' --build-embed'), $retcode); + if ($frankenphp) { + passthru("{$prefix}install-pkg go-mod-frankenphp --debug", $retcode); + if ($retcode !== 0) { + break; + } + } + passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : (' --build-embed' . ($frankenphp ? ' --build-frankenphp' : ''))), $retcode); break; case 'doctor_cmd': passthru($prefix . $doctor_cmd, $retcode);