mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-03 23:05:41 +08:00
V3 refactor/craft (#1091)
This commit is contained in:
57
TODO.md
57
TODO.md
@@ -1,57 +0,0 @@
|
||||
# v3 TODO List
|
||||
|
||||
Tracking items identified during the v2 → v3 migration audit.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
- [ ] Implement `craft` command (drives full build from `craft.yml`; should be easier with v3 vendor/registry mode)
|
||||
- [x] Migrate `micro:combine` command (combine `micro.sfx` with PHP code + INI injection)
|
||||
- [ ] Implement `dump-extensions` command (extract required extensions from `composer.json` / `composer.lock`)
|
||||
- [ ] Design and implement v3 dev toolchain commands (WIP — needs design decision):
|
||||
- [ ] `dev:extensions` / equivalent listing command
|
||||
- [ ] `dev:php-version`, `dev:ext-version`, `dev:lib-version`
|
||||
- [ ] Doc generation commands (`dev:gen-ext-docs`, `dev:gen-ext-dep-docs`, `dev:gen-lib-dep-docs`) — pending v3 doc design
|
||||
|
||||
---
|
||||
|
||||
## Source Patches (SourcePatcher → Artifact migration)
|
||||
|
||||
The following v2 `SourcePatcher` hooks are not yet migrated to v3 `src/Package/Artifact/` classes:
|
||||
|
||||
- [ ] Migrate `patchSQLSRVWin32` — removes `/sdl` compile flag to prevent Zend build failure on Windows
|
||||
- [ ] Migrate `patchSQLSRVPhp85` — fixes `pdo_sqlsrv` directory layout for PHP 8.5
|
||||
- [ ] Migrate `patchYamlWin32` — patches `config.w32` `_a.lib` detection logic for the `yaml` extension
|
||||
- [ ] Migrate `patchImagickWith84` — applies PHP 8.4 compatibility patch for `imagick` based on version detection
|
||||
|
||||
---
|
||||
|
||||
## Extension Package Classes (Unix)
|
||||
|
||||
Extensions that had non-trivial v2 build logic and are missing a v3 `src/Package/Extension/` class:
|
||||
|
||||
- [x] `gettext` — macOS: fix `config.m4` bracket syntax for cross-version compatibility + append frameworks to linker flags (critical for macOS linking; this is a Unix-side gap, not Windows-only)
|
||||
|
||||
---
|
||||
|
||||
## Windows Extensions (Early Stage)
|
||||
|
||||
Windows extension support is still in early stage. The following extensions had Windows-specific configure args or patches in v2 and are pending v3 Windows implementation:
|
||||
|
||||
- [ ] `amqp` — Windows configure args
|
||||
- [ ] `com_dotnet` — Windows-only extension
|
||||
- [ ] `dom` — remove `dllmain.c` from `config.w32`
|
||||
- [ ] `ev` — fix `PHP_EV_SHARED` in `config.w32`
|
||||
- [ ] `gmssl` — add `CHECK_LIB("gmssl.lib")` to `config.w32`
|
||||
- [ ] `intl` — fix `PHP_INTL_SHARED` in `config.w32`
|
||||
- [ ] `lz4` — Windows configure args
|
||||
- [ ] `mbregex` — Windows configure args
|
||||
- [ ] `sqlsrv` / `pdo_sqlsrv` — complex conditional build logic (independent `sqlsrv` without `pdo_sqlsrv`)
|
||||
- [ ] `xml` — remove `dllmain.c` from `config.w32`; handles `soap`, `xmlreader`, `xmlwriter`, `simplexml`
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] Write v3 user documentation (currently zero v3 docs)
|
||||
10
config/pkg/ext/ext-deepclone.yml
Normal file
10
config/pkg/ext/ext-deepclone.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
ext-deepclone:
|
||||
type: php-extension
|
||||
artifact:
|
||||
source:
|
||||
type: ghtagtar
|
||||
repo: symfony/php-ext-deepclone
|
||||
extract: php-src/ext/deepclone
|
||||
metadata:
|
||||
license-files: [LICENSE]
|
||||
license: PHP-3.01
|
||||
224
src/StaticPHP/Command/CraftCommand.php
Normal file
224
src/StaticPHP/Command/CraftCommand.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Doctor\Doctor;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
#[AsCommand('craft', 'Build static-php from craft.yml')]
|
||||
class CraftCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml');
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$craft_file = $this->getArgument('craft');
|
||||
if (!file_exists($craft_file)) {
|
||||
$this->output->writeln('<error>craft.yml not found, please create one!</error>');
|
||||
return static::USER_ERROR;
|
||||
}
|
||||
|
||||
$craft = $this->validateAndParseCraftFile($craft_file);
|
||||
|
||||
// set verbosity
|
||||
$this->output->setVerbosity($craft['verbosity']);
|
||||
|
||||
// apply env
|
||||
array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}"));
|
||||
|
||||
// run doctor
|
||||
if ($craft['craft-options']['doctor']) {
|
||||
$doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX);
|
||||
if ($doctor->checkAll()) {
|
||||
Doctor::markPassed();
|
||||
$this->output->writeln('');
|
||||
} else {
|
||||
$this->output->writeln('<error>Doctor check failed, please fix the issues and try again.</error>');
|
||||
return static::ENVIRONMENT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// parse download-options to installer's dl options
|
||||
$build_options = $craft['build-options'];
|
||||
if (!$craft['craft-options']['download']) {
|
||||
$build_options['no-download'] = true;
|
||||
}
|
||||
foreach ($craft['download-options'] as $k => $v) {
|
||||
$build_options["dl-{$k}"] = $v;
|
||||
}
|
||||
|
||||
// parse SAPI
|
||||
foreach ($craft['sapi'] as $name) {
|
||||
$build_options["build-{$name}"] = true;
|
||||
}
|
||||
|
||||
// clean build
|
||||
if ($craft['clean-build']) {
|
||||
FileSystem::resetDir(BUILD_ROOT_PATH);
|
||||
FileSystem::resetDir(SOURCE_PATH);
|
||||
}
|
||||
|
||||
$starttime = microtime(true);
|
||||
// run installer
|
||||
$installer = new PackageInstaller($build_options);
|
||||
$installer->addBuildPackage('php');
|
||||
$installer->run(true);
|
||||
|
||||
$usedtime = round(microtime(true) - $starttime, 1);
|
||||
$this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
$this->output->writeln("<info>✔ BUILD SUCCESSFUL ({$usedtime} s)</info>");
|
||||
$this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
$installer->printBuildPackageOutputs();
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse craft.yml file to array.
|
||||
*
|
||||
* @param string $craft_file craft.yml path
|
||||
* @return array{
|
||||
* php-version: string,
|
||||
* extensions: array<string>,
|
||||
* shared-extensions: array<string>,
|
||||
* packages: array<string>,
|
||||
* sapi: array<string>,
|
||||
* verbosity: int,
|
||||
* debug: bool,
|
||||
* clean-build: bool,
|
||||
* build-options: array<string, mixed>,
|
||||
* download-options: array<string, mixed>,
|
||||
* extra-env: array<string, string>,
|
||||
* craft-options: array{
|
||||
* doctor: bool,
|
||||
* download: bool,
|
||||
* build: bool
|
||||
* }
|
||||
* } Parsed craft content
|
||||
*/
|
||||
private function validateAndParseCraftFile(string $craft_file): array
|
||||
{
|
||||
$build_options = $this->getApplication()->find('build:php')->getDefinition()->getOptions();
|
||||
$download_options = $this->getApplication()->find('download')->getDefinition()->getOptions();
|
||||
try {
|
||||
$craft = Yaml::parseFile($craft_file);
|
||||
} catch (ParseException $e) {
|
||||
throw new ValidationException("Craft file '{$craft_file}' is broken: {$e->getMessage()}");
|
||||
}
|
||||
if (!is_assoc_array($craft)) {
|
||||
throw new ValidationException("Craft file '{$craft_file}' must be an associative array.");
|
||||
}
|
||||
|
||||
// check php-version
|
||||
if (isset($craft['php-version']) && !preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', strval($craft['php-version']))) {
|
||||
throw new ValidationException("Craft file '{$craft_file}' has invalid 'php-version' field, it should be in format of '8.0.0'.");
|
||||
}
|
||||
|
||||
// check php extensions field
|
||||
if (!isset($craft['extensions'])) {
|
||||
throw new ValidationException("Craft file '{$craft_file}' must have 'extensions' field.");
|
||||
}
|
||||
// parse extension if not list
|
||||
if (is_string($craft['extensions'])) {
|
||||
$craft['extensions'] = parse_extension_list($craft['extensions']);
|
||||
}
|
||||
|
||||
// check shared-extensions field
|
||||
if (!isset($craft['shared-extensions'])) {
|
||||
$craft['shared-extensions'] = [];
|
||||
} elseif (is_string($craft['shared-extensions'])) {
|
||||
$craft['shared-extensions'] = parse_extension_list($craft['shared-extensions']);
|
||||
}
|
||||
|
||||
// check libs and additional packages
|
||||
$v2_libs = parse_comma_list($craft['libs'] ?? []);
|
||||
$v3_packages = parse_comma_list($craft['packages'] ?? []);
|
||||
$craft['packages'] = array_merge($v2_libs, $v3_packages);
|
||||
|
||||
// check PHP SAPI
|
||||
if (!isset($craft['sapi'])) {
|
||||
throw new ValidationException('Craft file "sapi" is required.');
|
||||
}
|
||||
if (is_string($craft['sapi'])) {
|
||||
$craft['sapi'] = parse_comma_list($craft['sapi']);
|
||||
}
|
||||
|
||||
// verbosity
|
||||
$verbosity_level = $craft['verbosity'] ?? OutputInterface::VERBOSITY_NORMAL;
|
||||
$debug = $craft['debug'] ?? false;
|
||||
if ($debug) {
|
||||
$verbosity_level = OutputInterface::VERBOSITY_DEBUG;
|
||||
}
|
||||
$craft['verbosity'] = $verbosity_level;
|
||||
|
||||
// clean-build (if true, reset before all builds)
|
||||
$craft['clean-build'] ??= false;
|
||||
|
||||
// build-options
|
||||
if (isset($craft['build-options'])) {
|
||||
if (!is_assoc_array($craft['build-options'])) {
|
||||
throw new ValidationException('Craft file "build" options must be an associative array.');
|
||||
}
|
||||
foreach ($craft['build-options'] as $key => $value) {
|
||||
if (!isset($build_options[$key])) {
|
||||
throw new ValidationException('Craft file "build" option "' . $key . '" is invalid.');
|
||||
}
|
||||
if ($build_options[$key]->isArray() && !is_array($value)) {
|
||||
throw new ValidationException('Craft file "build" option "' . $key . '" must be an array.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$craft['build-options'] = [];
|
||||
}
|
||||
|
||||
// download-options
|
||||
if (isset($craft['download-options'])) {
|
||||
if (!is_assoc_array($craft['download-options'])) {
|
||||
throw new ValidationException('Craft file "download" options must be an associative array.');
|
||||
}
|
||||
foreach ($craft['download-options'] as $key => $value) {
|
||||
if (!isset($download_options[$key])) {
|
||||
throw new ValidationException('Craft file "download" option "' . $key . '" is invalid.');
|
||||
}
|
||||
if ($download_options[$key]->isArray() && !is_array($value)) {
|
||||
throw new ValidationException('Craft file "download" option "' . $key . '" must be an array.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$craft['download-options'] = [];
|
||||
}
|
||||
|
||||
// post-parse: parse php-version field to download options
|
||||
if (isset($craft['php-version'])) {
|
||||
$craft['download-options']['with-php'] = strval($craft['php-version']);
|
||||
$craft['download-options']['ignore-cache'] = (($craft['download-options']['ignore-cache'] ?? false) === true ? true : 'php-src');
|
||||
}
|
||||
|
||||
// extra-env
|
||||
if (isset($craft['extra-env'])) {
|
||||
if (!is_assoc_array($craft['extra-env'])) {
|
||||
throw new ValidationException('Craft file "extra-env" must be an associative array.');
|
||||
}
|
||||
} else {
|
||||
$craft['extra-env'] = [];
|
||||
}
|
||||
|
||||
// craft-options
|
||||
$craft['craft-options']['doctor'] ??= true;
|
||||
$craft['craft-options']['download'] ??= true;
|
||||
|
||||
return $craft;
|
||||
}
|
||||
}
|
||||
@@ -62,8 +62,10 @@ class ResetCommand extends BaseCommand
|
||||
InteractiveTerm::indicateProgress("Removing: {$path}");
|
||||
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
Shell::passthruCallback(fn () => InteractiveTerm::advance());
|
||||
// Force delete on Windows to handle git directories
|
||||
$this->removeDirectoryWindows($path);
|
||||
Shell::passthruCallback(null);
|
||||
} else {
|
||||
// Use FileSystem::removeDir for Unix systems
|
||||
FileSystem::removeDir($path);
|
||||
@@ -88,7 +90,6 @@ class ResetCommand extends BaseCommand
|
||||
|
||||
// Try using PowerShell for force deletion
|
||||
$escaped_path = escapeshellarg($path);
|
||||
Shell::passthruCallback(fn () => InteractiveTerm::advance());
|
||||
|
||||
// Use PowerShell Remove-Item with -Force and -Recurse
|
||||
$result = cmd()->execWithResult("powershell -Command \"Remove-Item -Path {$escaped_path} -Recurse -Force -ErrorAction SilentlyContinue\"", false);
|
||||
@@ -106,6 +107,5 @@ class ResetCommand extends BaseCommand
|
||||
if (is_dir($path)) {
|
||||
FileSystem::removeDir($path);
|
||||
}
|
||||
Shell::passthruCallback(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace StaticPHP;
|
||||
use StaticPHP\Command\BuildLibsCommand;
|
||||
use StaticPHP\Command\BuildTargetCommand;
|
||||
use StaticPHP\Command\CheckUpdateCommand;
|
||||
use StaticPHP\Command\CraftCommand;
|
||||
use StaticPHP\Command\Dev\DumpCapabilitiesCommand;
|
||||
use StaticPHP\Command\Dev\DumpStagesCommand;
|
||||
use StaticPHP\Command\Dev\EnvCommand;
|
||||
@@ -67,6 +68,7 @@ class ConsoleApplication extends Application
|
||||
new ResetCommand(),
|
||||
new CheckUpdateCommand(),
|
||||
new MicroCombineCommand(),
|
||||
new CraftCommand(),
|
||||
|
||||
// dev commands
|
||||
new ShellCommand(),
|
||||
|
||||
@@ -154,10 +154,10 @@ class PackageInstaller
|
||||
$this->resolvePackages();
|
||||
}
|
||||
|
||||
if ($this->interactive && !$disable_delay_msg) {
|
||||
// show install or build options in terminal with beautiful output
|
||||
$this->printInstallerInfo();
|
||||
// show install or build options in terminal with beautiful output
|
||||
$this->printInstallerInfo();
|
||||
|
||||
if ($this->interactive && !$disable_delay_msg) {
|
||||
InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL);
|
||||
sleep(2);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,14 @@ class TargetPackage extends LibraryPackage
|
||||
if ($input !== null && $input->hasOption($key)) {
|
||||
return $input->getOption($key);
|
||||
}
|
||||
|
||||
// try builder options
|
||||
$builder = ApplicationContext::has(PackageBuilder::class)
|
||||
? ApplicationContext::get(PackageBuilder::class)
|
||||
: null;
|
||||
if ($builder !== null && ($option = $builder->getOption($key)) !== null) {
|
||||
return $option;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
|
||||
@@ -287,10 +287,11 @@ class FileSystem
|
||||
/**
|
||||
* Remove directory recursively
|
||||
*
|
||||
* @param string $dir Directory to remove
|
||||
* @return bool Success status
|
||||
* @param string $dir Directory to remove
|
||||
* @param null|callable $callback Callback for every single scan items
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function removeDir(string $dir): bool
|
||||
public static function removeDir(string $dir, ?callable $callback = null): bool
|
||||
{
|
||||
$dir = self::convertPath($dir);
|
||||
logger()->debug('Removing path recursively: "' . $dir . '"');
|
||||
@@ -311,7 +312,9 @@ class FileSystem
|
||||
}
|
||||
// 遍历目录
|
||||
foreach ($scan_list as $v) {
|
||||
InteractiveTerm::advance();
|
||||
if ($callback) {
|
||||
$callback($v);
|
||||
}
|
||||
// Unix 系统排除这俩目录
|
||||
if ($v == '.' || $v == '..') {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user