From b700517416c9c265432d7afe20344039e21706e3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 25 Apr 2025 16:07:39 +0800 Subject: [PATCH] Add skeleton commands --- box.json | 1 + composer.json | 3 +- composer.lock | 159 ++++++++++- src/SPC/ConsoleApplication.php | 8 +- src/SPC/command/dev/ExtSkeletonCommand.php | 122 +++++++++ src/SPC/command/dev/LibSkeletonCommand.php | 108 ++++++++ src/SPC/command/dev/SkeletonCommand.php | 256 ++++++++++++++++++ src/SPC/command/dev/SourceSkeletonCommand.php | 96 +++++++ src/SPC/store/FileSystem.php | 20 +- src/SPC/util/PhpPrinter.php | 13 + 10 files changed, 781 insertions(+), 5 deletions(-) create mode 100644 src/SPC/command/dev/ExtSkeletonCommand.php create mode 100644 src/SPC/command/dev/LibSkeletonCommand.php create mode 100644 src/SPC/command/dev/SkeletonCommand.php create mode 100644 src/SPC/command/dev/SourceSkeletonCommand.php create mode 100644 src/SPC/util/PhpPrinter.php diff --git a/box.json b/box.json index f6eecf1ca..bf0705e34 100644 --- a/box.json +++ b/box.json @@ -8,6 +8,7 @@ "directories": [ "config", "src", + "vendor/nette", "vendor/psr", "vendor/laravel/prompts", "vendor/illuminate", diff --git a/composer.json b/composer.json index 97d835fb5..09327fecf 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "ext-mbstring": "*", "ext-zlib": "*", "laravel/prompts": "^0.1.12", + "nette/php-generator": "^4.1", "symfony/console": "^5.4 || ^6 || ^7", "zhamao/logger": "^1.0" }, @@ -44,7 +45,7 @@ ], "scripts": { "analyse": "phpstan analyse --memory-limit 300M", - "cs-fix": "php-cs-fixer fix", + "cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix", "test": "vendor/bin/phpunit tests/ --no-coverage", "build:phar": "vendor/bin/box compile" }, diff --git a/composer.lock b/composer.lock index 6b2465b6d..861a4ab93 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7e3bd0c36428da870d1a0ae206c3b83e", + "content-hash": "16ba97446d26c4c5f7849ea182be9787", "packages": [ { "name": "illuminate/collections", @@ -260,6 +260,161 @@ }, "time": "2024-08-12T22:06:33+00:00" }, + { + "name": "nette/php-generator", + "version": "v4.1.8", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "reference": "42806049a7774a2bd316c958f5dcf01c6b5c56fa", + "shasum": "" + }, + "require": { + "nette/utils": "^3.2.9 || ^4.0", + "php": "8.0 - 8.4" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.18 || ^5.0", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.4 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.1.8" + }, + "time": "2025-03-31T00:29:29+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.6", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "ce708655043c7050eb050df361c5e313cf708309" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309", + "reference": "ce708655043c7050eb050df361c5e313cf708309", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.6" + }, + "time": "2025-03-30T21:06:30+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -7545,7 +7700,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "~8.4.0", + "php": ">= 8.3", "ext-mbstring": "*", "ext-zlib": "*" }, diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index bab8a0083..de2d11d9c 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -8,14 +8,17 @@ use SPC\command\BuildPHPCommand; use SPC\command\DeleteDownloadCommand; use SPC\command\dev\AllExtCommand; +use SPC\command\dev\ExtSkeletonCommand; use SPC\command\dev\ExtVerCommand; use SPC\command\dev\GenerateExtDepDocsCommand; use SPC\command\dev\GenerateExtDocCommand; use SPC\command\dev\GenerateLibDepDocsCommand; +use SPC\command\dev\LibSkeletonCommand; use SPC\command\dev\LibVerCommand; use SPC\command\dev\PackLibCommand; use SPC\command\dev\PhpVerCommand; use SPC\command\dev\SortConfigCommand; +use SPC\command\dev\SourceSkeletonCommand; use SPC\command\DoctorCommand; use SPC\command\DownloadCommand; use SPC\command\DumpExtensionsCommand; @@ -32,7 +35,7 @@ */ final class ConsoleApplication extends Application { - public const VERSION = '2.5.2'; + public const VERSION = '2.5.3'; public function __construct() { @@ -67,6 +70,9 @@ public function __construct() new GenerateExtDepDocsCommand(), new GenerateLibDepDocsCommand(), new PackLibCommand(), + new ExtSkeletonCommand(), + new LibSkeletonCommand(), + new SourceSkeletonCommand(), ] ); } diff --git a/src/SPC/command/dev/ExtSkeletonCommand.php b/src/SPC/command/dev/ExtSkeletonCommand.php new file mode 100644 index 000000000..f744a5d60 --- /dev/null +++ b/src/SPC/command/dev/ExtSkeletonCommand.php @@ -0,0 +1,122 @@ +getArgument('name'); + if ($name !== null && is_string($r = $this->validateExtName($name))) { + throw new InvalidArgumentException($r); + } + } + + /** + * @throws ExceptionInterface|FileSystemException + */ + public function handle(): int + { + $result = ['type' => 'external']; + // Get extension name + $ext_name = $this->input->getArgument('name'); + + // apply source name + $result['source'] = $ext_name; + + // Select extension support + $ext_support = multiselect('Please select extension support for [' . $ext_name . '].', [ + 'Linux' => 'Linux', + 'Darwin' => 'MacOS', + 'Windows' => 'Windows', + ], default: ['Linux', 'Darwin'], required: true, hint: 'Use the space bar to select options, press enter to the next step'); + + // $input->setArgument('name', $ext_name); + $a = new ArrayInput(['name' => $ext_name, '--is-middle-step' => true]); + $this->getApplication()->find('dev:source-skel')->run($a, $this->output); + + // check if extension depends on other extensions + $ext_depends = confirm('Does this extension depend on other extensions?', default: false) ? multiselect('Please select extension dependencies', array_keys(Config::getExts()), hint: 'Use the space bar to select options, press enter to the next step') : []; + if ($ext_depends) { + $result['ext-depends'] = $ext_depends; + } + + // check if extension suggests other extensions + $ext_suggests = confirm('Does this extension suggest other extensions?', default: false) ? multiselect('Please select extension suggestions', array_keys(Config::getExts()), hint: 'Use the space bar to select options, press enter to the next step') : []; + if ($ext_suggests) { + $result['ext-suggests'] = $ext_suggests; + } + + // select extension build arg type (--enable-xxx, --with-xxx, with-xxx=PATH, custom) + if (in_array('Linux', $ext_support) || in_array('Darwin', $ext_support)) { + $ext_build_args_unix = select('Please select *nix (Linux, macOS) extension build arg type', [ + 'enable' => '--enable-' . strtolower($ext_name), + 'with' => '--with-' . strtolower($ext_name), + 'with-xxx=' => '--with-' . strtolower($ext_name) . '={buildroot}', + 'custom' => 'custom', + ], default: 'enable'); + } + if (in_array('Windows', $ext_support)) { + $ext_build_args_windows = select('Please select Windows extension build arg type', [ + 'enable' => '--enable-' . strtolower($ext_name), + 'with' => '--with-' . strtolower($ext_name), + 'with-xxx=' => '--with-' . strtolower($ext_name) . '={buildroot}', + 'custom' => 'custom', + ], default: 'enable'); + } + $ext_build_args_unix ??= null; + $ext_build_args_windows ??= $ext_build_args_unix; + if ($ext_build_args_windows === $ext_build_args_unix) { + $result['arg-type'] = $ext_build_args_windows; + } else { + $result['arg-type-unix'] = $ext_build_args_unix; + $result['arg-type-windows'] = $ext_build_args_windows; + } + + // check if extension depends on other libraries + if (confirm('Does this extension depend on other libraries?', default: false)) { + // Select library dependencies, or create a new library skeleton + if (select('You can select existing libraries or create a new library skeleton', ['Create a new library skeleton', 'Select existing libraries'], default: 'Select existing libraries') === 'Create a new library skeleton') { + $lib_name = text('Please input new library name', required: true, validate: [$this, 'validateLibName']); + $lib_name = strtolower($lib_name); + $input = new ArrayInput(['name' => $lib_name, '--is-middle-step' => true]); + $this->getApplication()->find('dev:lib-skel')->run($input, $this->output); + } else { + // Select existing libraries + $ext_libs = multiselect('Please select library dependencies', array_keys(Config::getLibs()), hint: 'Use the space bar to select options, press enter to the next step'); + } + } else { + $ext_libs = []; + } + if (!empty($ext_libs)) { + $result['lib-depends'] = $ext_libs; + } + $this->output->writeln('Extension config generated!'); + $this->output->writeln(sprintf('%s', json_encode($result, JSON_PRETTY_PRINT))); + SkeletonCommand::$cache['ext'][$ext_name] = $result; + if (!$this->getOption('is-middle-step')) { + $this->generateAll(); + } + return static::SUCCESS; + } +} diff --git a/src/SPC/command/dev/LibSkeletonCommand.php b/src/SPC/command/dev/LibSkeletonCommand.php new file mode 100644 index 000000000..c91b66a66 --- /dev/null +++ b/src/SPC/command/dev/LibSkeletonCommand.php @@ -0,0 +1,108 @@ +input->getArgument('name'); + $result = []; + + // Select extension support + $lib_support = multiselect('Please select lib support OS for ' . $lib_name, [ + 'Linux' => 'Linux', + 'Darwin' => 'MacOS', + 'Windows' => 'Windows', + ], default: ['Linux', 'Darwin'], hint: 'Use the space bar to select options, press enter to the next step'); + $result['lib-support'] = $lib_support; + if (in_array('Linux', $lib_support) || in_array('Darwin', $lib_support)) { + // ask static-libs-unix + if (select('Please select static lib type for *nix', [ + 'current' => 'Use ' . (str_starts_with($lib_name, 'lib') ? "{$lib_name}.a" : "lib{$lib_name}.a") . ' as default', + 'custom' => 'Specify custom static lib files', + ]) === 'custom') { + $result['static-libs-unix'] = explode("\n", textarea( + "Please input [{$lib_name}] static lib files for *nix (Linux and macOS)", + default: str_starts_with($lib_name, 'lib') ? "{$lib_name}.a" : '', + validate: [$this, 'validateStaticLibs'], + hint: 'Each line is a static lib name, e.g. libfoo.a', + transform: fn ($x) => implode("\n", array_filter(explode("\n", trim($x)), fn ($v) => trim($v) !== '')) + )); + } else { + $result['static-libs-unix'] = [str_starts_with($lib_name, 'lib') ? "{$lib_name}.a" : "lib{$lib_name}.a"]; + } + } + if (in_array('Windows', $lib_support)) { + // ask static-libs-win + $result['static-libs-windows'] = textarea("Please input [{$lib_name}] static lib files for Windows", default: str_starts_with($lib_name, 'lib') ? "{$lib_name}.lib" : '', hint: 'Each line is a static lib name, e.g. foo.lib'); + } + // ask for a lib source + $a = new ArrayInput(['name' => $lib_name, '--is-middle-step' => true]); + $this->getApplication()->find('dev:source-skel')->run($a, $this->output); + $result['source'] = $lib_name; + + // ask for lib depends + if (confirm('Does this library depend on other libraries?', default: false)) { + // Select library dependencies, or create a new library skeleton + if (select('You can select existing libraries or create a new library skeleton', ['Create a new library skeleton', 'Select existing libraries'], default: 'Select existing libraries') === 'Create a new library skeleton') { + $lib_name = text('Please input new library name', required: true, validate: [$this, 'validateLibName']); + $lib_name = strtolower($lib_name); + $input = new ArrayInput(['name' => $lib_name]); + $this->run($input, $this->output); + } else { + // Select existing libraries + $lib_depends = multiselect('Please select library dependencies', array_keys(Config::getLibs()), hint: 'Use the space bar to select options, press enter to the next step'); + } + } else { + $lib_depends = []; + } + if (!empty($lib_depends)) { + $result['lib-depends'] = $lib_depends; + } + + // ask for using autoconf, cmake or other + if (in_array('Darwin', $lib_support) || in_array('Linux', $lib_support)) { + $build_tool = select("Please select [{$lib_name}] *nix build tool", [ + 'cmake' => 'CMake (CMakeLists.txt)', + 'autoconf' => 'Autoconf (./configure)', + 'other' => 'Other', + ], default: 'cmake'); + $result['build-tool-unix'] = $build_tool; + } + if (in_array('Windows', $lib_support)) { + $build_tool = select("Please select [{$lib_name}] Windows build tool", [ + 'cmake' => 'CMake (CMakeLists.txt)', + 'sln' => 'Visual Studio Solution (XXX.sln)', + 'other' => 'Other', + ], default: 'cmake'); + $result['build-tool-windows'] = $build_tool; + } + + $this->output->writeln("Generated library config for {$lib_name}!"); + SkeletonCommand::$cache['lib'][$lib_name] = $result; + if (!$this->getOption('is-middle-step')) { + $this->generateAll(); + } + return static::SUCCESS; + } +} diff --git a/src/SPC/command/dev/SkeletonCommand.php b/src/SPC/command/dev/SkeletonCommand.php new file mode 100644 index 000000000..5766161c2 --- /dev/null +++ b/src/SPC/command/dev/SkeletonCommand.php @@ -0,0 +1,256 @@ +>, + * lib: array + * }>>, + * source: array> + * } + */ + protected static array $cache = [ + 'ext' => [], + 'lib' => [], + 'source' => [], + ]; + + public function configure(): void + { + $this->addArgument('name', InputArgument::REQUIRED); + $this->addOption('is-middle-step', null, null, 'Middle step does not create final file'); + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + if (!$input->isInteractive() || PHP_OS_FAMILY === 'Windows') { + throw new LogicException('This command is not supported in non-interactive mode or on Windows.'); + } + } + + /** + * @throws FileSystemException + */ + public function validateExtName(string $name): ?string + { + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { + return 'Extension name must be alphanumeric and underscore only'; + } + if (isset(Config::getExts()[$name]) || isset(self::$cache['ext'][$name])) { + return "Extension {$name} already exists"; + } + return null; + } + + /** + * @throws FileSystemException + */ + public function validateLibName(string $name): ?string + { + if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) { + return 'Library name must be alphanumeric and underscore only'; + } + if (isset(Config::getLibs()[$name]) || isset(self::$cache['lib'][$name])) { + return "Library {$name} already exists"; + } + return null; + } + + public function validateStaticLibs(string $libs): ?string + { + $libs = explode("\n", $libs); + foreach ($libs as $lib) { + if (!preg_match('/^[a-zA-Z0-9_.\-+]+$/', trim($lib))) { + return 'Illegal static lib name'; + } + } + return null; + } + + /** + * Generate extension class + * + * @param string $ext_name Extension name + * @throws FileSystemException + */ + protected function generateExtensionClass(string $ext_name): string + { + $class_name = str_replace('-', '_', $ext_name); + + // use php-generator + $printer = new PhpPrinter(); + $file = new PhpFile(); + $file->setStrictTypes() + ->addComment('Remove this file if you do not need to patch the extension') + ->addNamespace('SPC\builder\extension') + ->addUse(Extension::class) + ->addUse(CustomExt::class) + ->addClass($ext_name) + ->setExtends(Extension::class) + ->addAttribute(CustomExt::class, [$ext_name]); + $path = WORKING_DIR . '/src/SPC/builder/extension/' . $class_name . '.php'; + FileSystem::writeFile($path, $printer->printFile($file)); + return $path; + } + + /** + * @return array + * @throws FileSystemException + */ + protected function generateLibraryClass(string $lib_name): array + { + $printer = new PhpPrinter(); + + $lib_config = self::$cache['lib'][$lib_name]; + + // class name needs to convert - to _ + $class_name = str_replace('-', '_', $lib_name); + + // check lib-support, if includes linux and macOS at the same time, use unix trait + + // generate base class + if (in_array('Linux', $lib_config['lib-support'])) { + $linux_file = new PhpFile(); + $linux_namespace = $linux_file->setStrictTypes() + ->addNamespace('SPC\builder\linux\library') + ->addUse(LinuxLibraryBase::class); + $linux_class = $linux_namespace->addClass($class_name) + ->setExtends(LinuxLibraryBase::class); + } + if (in_array('Darwin', $lib_config['lib-support'])) { + $macos_file = new PhpFile(); + $macos_namespace = $macos_file->setStrictTypes() + ->addNamespace('SPC\builder\macos\library') + ->addUse(MacOSLibraryBase::class); + $macos_class = $macos_namespace->addClass($class_name) + ->setExtends(MacOSLibraryBase::class); + } + // generate build function + if (isset($linux_class) || isset($macos_class)) { + $unix_build_method = new Method('build'); + $unix_build_method->setProtected()->setReturnType('void'); + + switch ($lib_config['build-tool-unix']) { + case 'cmake': + $unix_build_method->addBody(<<<'FILE' +\SPC\Store\FileSystem::resetDir($this->source_dir . '/build-dir'); +shell()->cd($this->source_dir . '/build-dir') + ->setEnv(['CFLAGS' => $this->getLibExtraCFlags(), 'LDFLAGS' => $this->getLibExtraLdFlags(), 'LIBS' => $this->getLibExtraLibs()]) + ->execWithEnv( + 'cmake ' . + '-DCMAKE_BUILD_TYPE=Release ' . + '-DCMAKE_TOOLCHAIN_FILE=' . $this->builder->cmake_toolchain_file . ' ' . + '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' . + '-DCMAKE_INSTALL_LIBDIR=lib ' . + '-DSHARE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' . + '-DBUILD_SHARED_LIBS=OFF ' . + '..' + ) + ->execWithEnv('cmake --build . -j ' . $this->builder->concurrency) + ->execWithEnv('make install'); +FILE); + break; + case 'autoconf': + $unix_build_method->addBody(<<<'FILE' +shell()->cd($this->source_dir) + ->setEnv(['CFLAGS' => $this->getLibExtraCFlags(), 'LDFLAGS' => $this->getLibExtraLdFlags(), 'LIBS' => $this->getLibExtraLibs()]) + ->execWithEnv( + './configure --disable-shared --enable-static ' . + '--prefix=' . BUILD_ROOT_PATH . ' ' + ) + ->execWithEnv("make -j{$this->builder->concurrency}") + ->execWithEnv('make install'); +FILE); + break; + case 'other': + $unix_build_method->addBody(<<<'FILE' +shell()->cd($this->source_dir) + ->setEnv(['CFLAGS' => $this->getLibExtraCFlags(), 'LDFLAGS' => $this->getLibExtraLdFlags(), 'LIBS' => $this->getLibExtraLibs()]) + ->execWithEnv('echo "your build command here, e.g. make xxx"'); +FILE); + break; + } + + // if lib-support is linux only, add build method to linux class + if (isset($linux_class) && !isset($macos_class)) { + $linux_class->setMethods([$unix_build_method]); + $linux_class->addConstant('NAME', $lib_name)->setPublic(); + } elseif (!isset($linux_class) && isset($macos_class)) { + $macos_class->setMethods([$unix_build_method]); + $macos_class->addConstant('NAME', $lib_name)->setPublic(); + } elseif (isset($linux_class, $macos_class)) { + // we need to add unix trait + $unix_trait_file = new PhpFile(); + $unix_trait = $unix_trait_file->setStrictTypes() + ->addNamespace('SPC\builder\unix\library') + ->addTrait($class_name); + $unix_trait->setMethods([$unix_build_method]); + // add trait to linux and macos class + $linux_class->addTrait('SPC\builder\unix\library\\' . $class_name); + $macos_class->addTrait('SPC\builder\unix\library\\' . $class_name); + $linux_class->addConstant('NAME', $lib_name)->setPublic(); + $macos_class->addConstant('NAME', $lib_name)->setPublic(); + } + } + + // generate trait file + $wrote = []; + if (isset($macos_file)) { + $path = WORKING_DIR . '/src/SPC/builder/macos/library/' . $class_name . '.php'; + FileSystem::writeFile($path, $printer->printFile($macos_file)); + $wrote[] = $path; + } + if (isset($linux_file)) { + $path = WORKING_DIR . '/src/SPC/builder/linux/library/' . $class_name . '.php'; + FileSystem::writeFile($path, $printer->printFile($linux_file)); + $wrote[] = $path; + } + if (isset($unix_trait_file)) { + $path = WORKING_DIR . '/src/SPC/builder/unix/library/' . $class_name . '.php'; + FileSystem::writeFile($path, $printer->printFile($unix_trait_file)); + $wrote[] = $path; + } + return $wrote; + } + + protected function generateAll(): void + { + // gen exts + foreach (self::$cache['ext'] as $ext_name => $ext_content) { + $wrote = $this->generateExtensionClass($ext_name); + $this->output->writeln(sprintf('Generated extension [%s]:', $ext_name)); + $this->output->writeln("\t{$wrote}"); + } + + // gen libs + foreach (self::$cache['lib'] as $lib_name => $lib_content) { + $wrote = $this->generateLibraryClass($lib_name); + $this->output->writeln('Generated library [' . $lib_name . '] skeleton:'); + foreach ($wrote as $line) { + $this->output->writeln("\t{$line}"); + } + } + } +} diff --git a/src/SPC/command/dev/SourceSkeletonCommand.php b/src/SPC/command/dev/SourceSkeletonCommand.php new file mode 100644 index 000000000..653a60e88 --- /dev/null +++ b/src/SPC/command/dev/SourceSkeletonCommand.php @@ -0,0 +1,96 @@ +input->getArgument('name'); + $result = []; + + // select a source type + $source_type = select("Please select source [{$source_name}] download method", [ + 'url' => 'Direct URL (e.g. http://a.com/file.zip)', + 'git' => 'Git (e.g. https://github.com/user/repo.git)', + 'filelist' => 'Crawl from web server index (filelist)', + 'ghtar' => 'GitHub Release Tarball', + 'ghtagtar' => 'GitHub Tag Tarball', + 'ghrel' => 'GitHub Release asset', + ], default: 'external', scroll: 6, required: true); + + $result['type'] = $source_type; + + switch ($source_type) { + case 'url': + $result['url'] = text('Please enter source URL', required: true, validate: fn ($x) => filter_var($x, FILTER_VALIDATE_URL) ? null : 'Invalid URL'); + break; + case 'git': + $result['rev'] = text('Please enter git branch name', default: 'main', required: true); + $result['url'] = text('Please enter git URL', required: true, validate: fn ($x) => filter_var($x, FILTER_VALIDATE_URL) ? null : 'Invalid URL'); + break; + case 'filelist': + $result['url'] = text('Please enter filelist fetch URL', required: true, validate: fn ($x) => filter_var($x, FILTER_VALIDATE_URL) ? null : 'Invalid URL'); + $result['regex'] = text('Please enter match file regex', default: '/href="(?' . $source_name . '-(?[^"]+)\.tar\.gz)"/', required: true, hint: 'Regex must contain named groups "file" and "version"'); + break; + case 'ghtar': + case 'ghtagtar': + $result['repo'] = text('Please enter GitHub repo name (e.g. phpredis/phpredis)', required: true, validate: fn ($x) => preg_match('/^[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+$/', $x) ? null : 'Invalid repo name'); + break; + case 'ghrel': + $result['repo'] = text('Please enter GitHub repo name (e.g. phpredis/phpredis)', required: true, validate: fn ($x) => preg_match('/^[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+$/', $x) ? null : 'Invalid repo name'); + $result['match'] = text('Please enter regex to match asset name', default: $source_name . '.+\.tar\.gz', required: true); + break; + } + + // select license + $license = select("Please select source [{$source_name}] license", [ + 'none' => 'None', + 'file-license' => '"LICENSE" file in source root', + 'file-copying' => '"COPYING" file in source root', + 'custom-file' => 'Custom file in source root', + 'custom-text' => 'Custom text', + ], default: 'none', required: true); + switch ($license) { + case 'file-license': + $result['license']['type'] = 'file'; + $result['license']['path'] = 'LICENSE'; + break; + case 'file-copying': + $result['license']['type'] = 'file'; + $result['license']['path'] = 'copying'; + break; + case 'custom-file': + $result['license']['type'] = 'file'; + $result['license']['path'] = text('Please enter custom license file name', required: true, hint: 'File name must be relative to source root, e.g. LICENSE.txt'); + break; + case 'custom-text': + $result['license']['type'] = 'text'; + $result['license']['text'] = textarea('Please enter custom license text', required: true); + break; + case 'none': + $result['license']['type'] = 'text'; + $result['license']['text'] = 'No license'; + break; + } + + // Select extension support + $this->output->writeln("Source {$source_name} added!"); + $this->output->writeln('' . json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . ''); + SkeletonCommand::$cache['source'][$source_name] = $result; + if (!$this->getOption('is-middle-step')) { + // write source to config + } + return static::SUCCESS; + } +} diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 16047427b..185f2ea6a 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -30,9 +30,27 @@ public static function loadConfigArray(string $config, ?string $config_dir = nul if (!is_array($json)) { throw new FileSystemException('Reading ' . $try . ' failed'); } - return $json; + $result = $json; + break; } } + $try_custom = $config_dir !== null ? [FileSystem::convertPath($config_dir . '/' . $config . '.custom.json')] : [ + WORKING_DIR . '/config/' . $config . '.custom.json', + ROOT_DIR . '/config/' . $config . '.custom.json', + ]; + foreach ($try_custom as $try) { + if (file_exists($try)) { + $json = json_decode(self::readFile($try), true); + if (!is_array($json)) { + throw new FileSystemException('Reading ' . $try . ' failed'); + } + $result = array_merge($result ?? [], $json); + break; + } + } + if (isset($result)) { + return $result; + } throw new FileSystemException('Reading ' . $config . '.json failed'); } diff --git a/src/SPC/util/PhpPrinter.php b/src/SPC/util/PhpPrinter.php new file mode 100644 index 000000000..525689aaa --- /dev/null +++ b/src/SPC/util/PhpPrinter.php @@ -0,0 +1,13 @@ +