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 @@
+