From 626bdc35091fee6b554428419cc489441020429b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:11:48 +0800 Subject: [PATCH 1/7] Allow fallback to builder options --- src/StaticPHP/Package/TargetPackage.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/StaticPHP/Package/TargetPackage.php b/src/StaticPHP/Package/TargetPackage.php index d29cf923..ca80d83a 100644 --- a/src/StaticPHP/Package/TargetPackage.php +++ b/src/StaticPHP/Package/TargetPackage.php @@ -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; } From 4f1ed70c96d48fdc2ff5df5b65ecd277c65728c8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:11:59 +0800 Subject: [PATCH 2/7] Move out callback --- src/StaticPHP/Command/ResetCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/ResetCommand.php b/src/StaticPHP/Command/ResetCommand.php index 207f6484..85928c2f 100644 --- a/src/StaticPHP/Command/ResetCommand.php +++ b/src/StaticPHP/Command/ResetCommand.php @@ -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); } } From 39a975dc90c9a9a980982bb8b8b36cff31882da6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:12:17 +0800 Subject: [PATCH 3/7] Just skip sleep --- src/StaticPHP/Package/PackageInstaller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index ac4aa2e0..b4c041df 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -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); } From 6976e9db9640f7c891694b079834dfe6d762a032 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:12:32 +0800 Subject: [PATCH 4/7] Allow callback for removeDir --- src/StaticPHP/Util/FileSystem.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index cbb46603..5e86a1d9 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -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; From 8ee9d134b3206bfd08c4908aa39c1dd11872fe59 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 01:12:53 +0800 Subject: [PATCH 5/7] Add craft command --- src/StaticPHP/Command/CraftCommand.php | 224 +++++++++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 2 files changed, 226 insertions(+) create mode 100644 src/StaticPHP/Command/CraftCommand.php diff --git a/src/StaticPHP/Command/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php new file mode 100644 index 00000000..9b1126ac --- /dev/null +++ b/src/StaticPHP/Command/CraftCommand.php @@ -0,0 +1,224 @@ +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('craft.yml not found, please create one!'); + 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('Doctor check failed, please fix the issues and try again.'); + 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("✔ BUILD SUCCESSFUL ({$usedtime} s)"); + $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, + * shared-extensions: array, + * packages: array, + * sapi: array, + * verbosity: int, + * debug: bool, + * clean-build: bool, + * build-options: array, + * download-options: array, + * extra-env: array, + * 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; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 0c8fec83..21e3ee8b 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -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(), From e7bf945b96fe2666110a1c17664722bb4b9973e2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 12:51:16 +0800 Subject: [PATCH 6/7] Forward-port #1094 --- config/pkg/ext/ext-deepclone.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 config/pkg/ext/ext-deepclone.yml diff --git a/config/pkg/ext/ext-deepclone.yml b/config/pkg/ext/ext-deepclone.yml new file mode 100644 index 00000000..ae54dbb5 --- /dev/null +++ b/config/pkg/ext/ext-deepclone.yml @@ -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 From bca1fb54108447cca86ce15a1fc17b7f85bee3ee Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:01:34 +0800 Subject: [PATCH 7/7] Remove TODO.md --- TODO.md | 57 --------------------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 52d6133a..00000000 --- a/TODO.md +++ /dev/null @@ -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)