From 605c06f85c809a54725556a074b0779601b93056 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:39:13 +0800 Subject: [PATCH 1/8] Add pie support for downloading sources --- config/source.json | 5 +- src/SPC/store/Downloader.php | 295 +++++++++++++++++------------------ 2 files changed, 145 insertions(+), 155 deletions(-) diff --git a/config/source.json b/config/source.json index 1546b23b..c6ec4b0d 100644 --- a/config/source.json +++ b/config/source.json @@ -999,9 +999,8 @@ } }, "spx": { - "type": "git", - "rev": "master", - "url": "https://github.com/NoiseByNorthwest/php-spx.git", + "type": "pie", + "repo": "noisebynorthwest/php-spx", "path": "php-src/ext/spx", "license": { "type": "file", diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index 3e50c93d..e94bfedc 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -16,6 +16,42 @@ use SPC\util\SPCTarget; */ class Downloader { + /** + * Get latest version from PIE config (Packagist) + * + * @param string $name Source name + * @param array $source Source meta info: [repo] + * @return array [url, filename] + */ + public static function getPIEInfo(string $name, array $source): array + { + $packagist_url = "https://repo.packagist.org/p2/{$source['repo']}.json"; + logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = json_decode(self::curlExec( + url: $packagist_url, + retries: self::getRetryAttempts() + ), true); + if (!isset($data['packages'][$source['repo']]) || !is_array($data['packages'][$source['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + // get the first version + $first = $data['packages'][$source['repo']][0] ?? []; + // check 'type' => 'php-ext' or contains 'php-ext' key + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $source['repo']); + $version = $first['version'] ?? 'unknown'; + // file name use: $name-$version.$dist_type + return [$dist_url, "{$name}-{$version}.{$dist_type}"]; + } + /** * Get latest version from BitBucket tag * @@ -317,84 +353,7 @@ class Downloader if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) { return; } - - try { - switch ($pkg['type']) { - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags'); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'url': // Direct download URL - $url = $pkg['url']; - $filename = $pkg['filename'] ?? basename($pkg['url']); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'git': // Git repo - self::downloadGit( - $name, - $pkg['url'], - $pkg['rev'], - $pkg['submodules'] ?? null, - $pkg['extract'] ?? null, - self::getRetryAttempts(), - SPC_DOWNLOAD_PRE_BUILT - ); - break; - case 'local': - // Local directory, do nothing, just lock it - logger()->debug("Locking local source {$name}"); - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $pkg['dirname'], - 'move_path' => $pkg['extract'] ?? null, - 'lock_as' => SPC_DOWNLOAD_PACKAGE, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); - if (isset($pkg['func']) && is_callable($pkg['func'])) { - $pkg['name'] = $name; - $pkg['func']($force, $pkg, SPC_DOWNLOAD_PACKAGE); - break; - } - foreach ($classes as $class) { - 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; - } - } - } - break; - default: - throw new DownloaderException('unknown source type: ' . $pkg['type']); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException('Download failed! ' . $e->getMessage()); - } + self::downloadByType($pkg['type'], $name, $pkg, $force, SPC_DOWNLOAD_PACKAGE); } /** @@ -439,80 +398,7 @@ class Downloader return; } - try { - switch ($source['type']) { - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags'); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'url': // Direct download URL - $url = $source['url']; - $filename = $source['filename'] ?? basename($source['url']); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'git': // Git repo - self::downloadGit( - $name, - $source['url'], - $source['rev'], - $source['submodules'] ?? null, - $source['path'] ?? null, - self::getRetryAttempts(), - $download_as - ); - break; - case 'local': - // Local directory, do nothing, just lock it - logger()->debug("Locking local source {$name}"); - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $source['dirname'], - 'move_path' => $source['extract'] ?? null, - 'lock_as' => $download_as, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - if (isset($source['func']) && is_callable($source['func'])) { - $source['name'] = $name; - $source['func']($force, $source, $download_as); - break; - } - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'); - foreach ($classes as $class) { - if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { - (new $class())->fetch($force, $source, $download_as); - break; - } - } - break; - default: - throw new DownloaderException('unknown source type: ' . $source['type']); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException('Download failed! ' . $e->getMessage()); - } + self::downloadByType($source['type'], $name, $source, $force, $download_as); } /** @@ -713,4 +599,109 @@ class Downloader } return false; } + + /** + * Download by type. + * + * @param string $type Types + * @param string $name Download item name + * @param array{ + * url?: string, + * repo?: string, + * rev?: string, + * path?: string, + * filename?: string, + * dirname?: string, + * match?: string, + * prefer-stable?: bool, + * extract?: string, + * submodules?: array, + * provide-pre-built?: bool, + * func?: ?callable, + * license?: array + * } $conf Download item config + * @param bool $force Force download + * @param int $download_as Lock source type + */ + private static function downloadByType(string $type, string $name, array $conf, bool $force, int $download_as): void + { + try { + switch ($type) { + case 'pie': // Packagist + [$url, $filename] = self::getPIEInfo($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'bitbuckettag': // BitBucket Tag + [$url, $filename] = self::getLatestBitbucketTag($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'ghtar': // GitHub Release (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'ghtagtar': // GitHub Tag (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $conf, 'tags'); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'ghrel': // GitHub Release (uploaded) + [$url, $filename] = self::getLatestGithubRelease($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); + break; + case 'filelist': // Basic File List (regex based crawler) + [$url, $filename] = self::getFromFileList($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'url': // Direct download URL + $url = $conf['url']; + $filename = $conf['filename'] ?? basename($conf['url']); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'git': // Git repo + self::downloadGit($name, $conf['url'], $conf['rev'], $conf['submodules'] ?? null, $conf['path'] ?? $conf['extract'] ?? null, self::getRetryAttempts(), $download_as); + break; + case 'local': // Local directory, do nothing, just lock it + LockFile::lockSource($name, [ + 'source_type' => SPC_SOURCE_LOCAL, + 'dirname' => $conf['dirname'], + 'move_path' => $conf['path'] ?? $conf['extract'] ?? null, + 'lock_as' => $download_as, + ]); + break; + case 'custom': // Custom download method, like API-based download or other + if (isset($conf['func']) && is_callable($conf['func'])) { + $conf['name'] = $name; + $conf['func']($force, $conf, $download_as); + break; + } + $classes = [ + ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'), + ...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, $conf, $download_as); + break; + } + if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { + $cls = new $class(); + if (in_array($name, $cls->getSupportName())) { + (new $class())->fetch($name, $force, $conf); + break; + } + } + } + break; + default: + throw new DownloaderException("Unknown download type: {$type}"); + } + } catch (\Throwable $e) { + // Because sometimes files downloaded through the command line are not automatically deleted after a failure. + // Here we need to manually delete the file if it is detected to exist. + if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { + logger()->warning("Deleting download file: {$filename}"); + unlink(DOWNLOAD_PATH . '/' . $filename); + } + throw new DownloaderException("Download failed: {$e->getMessage()}"); + } + } } From e2fd3e18d6dbfaa04ef06f2da90c1c84dd7b9a1e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:41:02 +0800 Subject: [PATCH 2/8] Refactor ConfigValidator, make it more strict --- config/ext.json | 34 +- config/lib.json | 31 +- config/source.json | 19 +- src/SPC/store/LockFile.php | 4 +- src/SPC/util/ConfigValidator.php | 534 +++++++++++++++++++++---------- 5 files changed, 401 insertions(+), 221 deletions(-) diff --git a/config/ext.json b/config/ext.json index 59ff4352..d5c70793 100644 --- a/config/ext.json +++ b/config/ext.json @@ -453,7 +453,7 @@ "type": "external", "source": "msgpack", "arg-type-unix": "with", - "arg-type-win": "enable", + "arg-type-windows": "enable", "ext-depends": [ "session" ] @@ -885,6 +885,22 @@ "mysqli" ] }, + "swoole-hook-odbc": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "notes": true, + "type": "addon", + "arg-type": "none", + "ext-depends": [ + "pdo", + "swoole" + ], + "lib-depends": [ + "unixodbc" + ] + }, "swoole-hook-pgsql": { "support": { "Windows": "no", @@ -914,22 +930,6 @@ "swoole" ] }, - "swoole-hook-odbc": { - "support": { - "Windows": "no", - "BSD": "wip" - }, - "notes": true, - "type": "addon", - "arg-type": "none", - "ext-depends": [ - "pdo", - "swoole" - ], - "lib-depends": [ - "unixodbc" - ] - }, "swow": { "support": { "BSD": "wip" diff --git a/config/lib.json b/config/lib.json index 81335539..39e47613 100644 --- a/config/lib.json +++ b/config/lib.json @@ -203,7 +203,6 @@ "libcares" ], "cpp-library": true, - "provide-pre-built": true, "frameworks": [ "CoreFoundation" ] @@ -560,6 +559,21 @@ "zstd" ] }, + "liburing": { + "source": "liburing", + "pkg-configs": [ + "liburing", + "liburing-ffi" + ], + "static-libs-linux": [ + "liburing.a", + "liburing-ffi.a" + ], + "headers-linux": [ + "liburing/", + "liburing.h" + ] + }, "libuuid": { "source": "libuuid", "static-libs-unix": [ @@ -940,20 +954,5 @@ "zstd.h", "zstd_errors.h" ] - }, - "liburing": { - "source": "liburing", - "pkg-configs": [ - "liburing", - "liburing-ffi" - ], - "static-libs-linux": [ - "liburing.a", - "liburing-ffi.a" - ], - "headers-linux": [ - "liburing/", - "liburing.h" - ] } } diff --git a/config/source.json b/config/source.json index c6ec4b0d..da58158c 100644 --- a/config/source.json +++ b/config/source.json @@ -669,6 +669,15 @@ "path": "LICENSE.md" } }, + "liburing": { + "type": "ghtar", + "repo": "axboe/liburing", + "prefer-stable": true, + "license": { + "type": "file", + "path": "COPYING" + } + }, "libuuid": { "type": "git", "url": "https://github.com/static-php/libuuid.git", @@ -990,7 +999,6 @@ }, "snappy": { "type": "git", - "repo": "google/snappy", "rev": "main", "url": "https://github.com/google/snappy", "license": { @@ -1154,14 +1162,5 @@ "type": "file", "path": "LICENSE" } - }, - "liburing": { - "type": "ghtar", - "repo": "axboe/liburing", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } } } diff --git a/src/SPC/store/LockFile.php b/src/SPC/store/LockFile.php index cab5b9da..88ecd6cb 100644 --- a/src/SPC/store/LockFile.php +++ b/src/SPC/store/LockFile.php @@ -155,8 +155,8 @@ class LockFile * @param string $name Source name * @param array{ * source_type: string, - * dirname: ?string, - * filename: ?string, + * dirname?: ?string, + * filename?: ?string, * move_path: ?string, * lock_as: int * } $data Source data diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index 58dedea6..3cd828ed 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -11,6 +11,150 @@ use Symfony\Component\Yaml\Yaml; class ConfigValidator { + /** + * Global field type definitions + * Maps field names to their expected types and validation rules + * Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json) + */ + private const array FIELD_TYPES = [ + // String fields + 'url' => 'string', // url + 'regex' => 'string', // regex pattern + 'rev' => 'string', // revision/branch + 'repo' => 'string', // repository name + 'match' => 'string', // match pattern (aaa*bbb) + 'filename' => 'string', // filename + 'path' => 'string', // copy path + 'extract' => 'string', // copy path (alias of path) + 'dirname' => 'string', // directory name for local source + 'source' => 'string', // the source name that this item uses + 'match-pattern-linux' => 'string', // pre-built match pattern for linux + 'match-pattern-macos' => 'string', // pre-built match pattern for macos + 'match-pattern-windows' => 'string', // pre-built match pattern for windows + + // Boolean fields + 'prefer-stable' => 'bool', // prefer stable releases + 'provide-pre-built' => 'bool', // provide pre-built binaries + 'notes' => 'bool', // whether to show notes in docs + 'cpp-library' => 'bool', // whether this is a C++ library + 'cpp-extension' => 'bool', // whether this is a C++ extension + 'build-with-php' => 'bool', // whether if this extension can be built to shared with PHP source together + 'zend-extension' => 'bool', // whether this is a zend extension + 'unix-only' => 'bool', // whether this extension is only for unix-like systems + + // Array fields + 'submodules' => 'array', // git submodules list (for git source type) + 'lib-depends' => 'list', + 'lib-suggests' => 'list', + 'ext-depends' => 'list', + 'ext-suggests' => 'list', + 'static-libs' => 'list', + 'pkg-configs' => 'list', // required pkg-config files without suffix (e.g. [libwebp]) + 'headers' => 'list', // required header files + 'bin' => 'list', // required binary files + 'frameworks' => 'list', // shared library frameworks (macOS) + + // Object/assoc array fields + 'support' => 'object', // extension OS support docs + 'extract-files' => 'object', // pkg.json extract files mapping with match pattern + 'alt' => 'object|bool', // alternative source/package + 'license' => 'object|array', // license information + 'target' => 'array', // extension build targets (default: [static], alternate: [shared] or both) + + // Special/mixed fields + 'func' => 'callable', // custom download function for custom source/package type + 'type' => 'string', // type field (validated separately) + ]; + + /** + * Source/Package download type validation rules + * Maps type names to [required_props, optional_props] + */ + private const array SOURCE_TYPE_FIELDS = [ + 'filelist' => [['url', 'regex'], []], + 'git' => [['url', 'rev'], ['path', 'extract', 'submodules']], + 'ghtagtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], + 'ghtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], + 'ghrel' => [['repo', 'match'], ['path', 'extract', 'prefer-stable']], + 'url' => [['url'], ['filename', 'path', 'extract']], + 'bitbuckettag' => [['repo'], ['path', 'extract']], + 'local' => [['dirname'], ['path', 'extract']], + 'pie' => [['repo'], ['path']], + 'custom' => [[], ['func']], + ]; + + /** + * Source.json specific fields [field_name => required] + * Note: 'type' is validated separately in validateSourceTypeConfig + * Field types are defined in FIELD_TYPES constant + */ + private const array SOURCE_FIELDS = [ + 'type' => true, // source type (must be SOURCE_TYPE_FIELDS key) + 'provide-pre-built' => false, // whether to provide pre-built binaries + 'alt' => false, // alternative source configuration + 'license' => false, // license information for source + // ... other fields are validated based on source type + ]; + + /** + * Lib.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array LIB_FIELDS = [ + 'type' => false, // lib type (lib/package/target/root) + 'source' => false, // the source name that this lib uses + 'lib-depends' => false, // required libraries + 'lib-suggests' => false, // suggested libraries + 'static-libs' => false, // Generated static libraries + 'pkg-configs' => false, // Generated pkg-config files + 'cpp-library' => false, // whether this is a C++ library + 'headers' => false, // Generated header files + 'bin' => false, // Generated binary files + 'frameworks' => false, // Used shared library frameworks (macOS) + ]; + + /** + * Ext.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array EXT_FIELDS = [ + 'type' => true, // extension type (builtin/external/addon/wip) + 'source' => false, // the source name that this extension uses + 'support' => false, // extension OS support docs + 'notes' => false, // whether to show notes in docs + 'cpp-extension' => false, // whether this is a C++ extension + 'build-with-php' => false, // whether if this extension can be built to shared with PHP source together + 'target' => false, // extension build targets (default: [static], alternate: [shared] or both) + 'lib-depends' => false, + 'lib-suggests' => false, + 'ext-depends' => false, + 'ext-suggests' => false, + 'frameworks' => false, + 'zend-extension' => false, // whether this is a zend extension + 'unix-only' => false, // whether this extension is only for unix-like systems + ]; + + /** + * Pkg.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array PKG_FIELDS = [ + 'type' => true, // package type (same as source type) + 'extract-files' => false, // files to extract mapping (source pattern => target path) + ]; + + /** + * Pre-built.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array PRE_BUILT_FIELDS = [ + 'repo' => true, // repository name for pre-built binaries + 'prefer-stable' => false, // prefer stable releases + 'match-pattern-linux' => false, // pre-built match pattern for linux + 'match-pattern-macos' => false, // pre-built match pattern for macos + 'match-pattern-windows' => false, // pre-built match pattern for windows + ]; + /** * Validate source.json * @@ -22,33 +166,20 @@ class ConfigValidator // Validate basic source type configuration self::validateSourceTypeConfig($src, $name, 'source'); - // Check source-specific fields + // Validate all source-specific fields using unified method + self::validateConfigFields($src, $name, 'source', self::SOURCE_FIELDS); + + // Check for unknown fields + self::validateAllowedFields($src, $name, 'source', self::SOURCE_FIELDS); + // check if alt is valid - if (isset($src['alt'])) { - if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) { - throw new ValidationException("source {$name} alt must be object or boolean"); - } - if (is_assoc_array($src['alt'])) { - // validate alt source recursively - self::validateSource([$name . '_alt' => $src['alt']]); - } - } - - // check if provide-pre-built is boolean - if (isset($src['provide-pre-built']) && !is_bool($src['provide-pre-built'])) { - throw new ValidationException("source {$name} provide-pre-built must be boolean"); - } - - // check if prefer-stable is boolean - if (isset($src['prefer-stable']) && !is_bool($src['prefer-stable'])) { - throw new ValidationException("source {$name} prefer-stable must be boolean"); + if (isset($src['alt']) && is_assoc_array($src['alt'])) { + // validate alt source recursively + self::validateSource([$name . '_alt' => $src['alt']]); } // check if license is valid if (isset($src['license'])) { - if (!is_array($src['license'])) { - throw new ValidationException("source {$name} license must be an object or array"); - } if (is_assoc_array($src['license'])) { self::checkSingleLicense($src['license'], $name); } elseif (is_list_array($src['license'])) { @@ -58,8 +189,6 @@ class ConfigValidator } self::checkSingleLicense($license, $name); } - } else { - throw new ValidationException("source {$name} license must be an object or array"); } } } @@ -71,9 +200,8 @@ class ConfigValidator if (!is_array($data)) { throw new ValidationException('lib.json is broken'); } - // check each lib + foreach ($data as $name => $lib) { - // check if lib is an assoc array if (!is_assoc_array($lib)) { throw new ValidationException("lib {$name} is not an object"); } @@ -89,36 +217,22 @@ class ConfigValidator if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) { throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}"); } - // check if source is string - if (isset($lib['source']) && !is_string($lib['source'])) { - throw new ValidationException("lib {$name} source must be string"); - } - // check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array + + // Validate basic fields using unified method + self::validateConfigFields($lib, $name, 'lib', self::LIB_FIELDS); + + // Validate list array fields with suffixes $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - foreach ($suffixes as $suffix) { - if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) { - throw new ValidationException("lib {$name} lib-depends must be a list"); - } - if (isset($lib['lib-suggests' . $suffix]) && !is_list_array($lib['lib-suggests' . $suffix])) { - throw new ValidationException("lib {$name} lib-suggests must be a list"); - } - if (isset($lib['static-libs' . $suffix]) && !is_list_array($lib['static-libs' . $suffix])) { - throw new ValidationException("lib {$name} static-libs must be a list"); - } - if (isset($lib['pkg-configs' . $suffix]) && !is_list_array($lib['pkg-configs' . $suffix])) { - throw new ValidationException("lib {$name} pkg-configs must be a list"); - } - if (isset($lib['headers' . $suffix]) && !is_list_array($lib['headers' . $suffix])) { - throw new ValidationException("lib {$name} headers must be a list"); - } - if (isset($lib['bin' . $suffix]) && !is_list_array($lib['bin' . $suffix])) { - throw new ValidationException("lib {$name} bin must be a list"); - } - } - // check if frameworks is a list array - if (isset($lib['frameworks']) && !is_list_array($lib['frameworks'])) { - throw new ValidationException("lib {$name} frameworks must be a list"); + $fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; + self::validateListArrayFields($lib, $name, 'lib', $fields, $suffixes); + + // Validate frameworks (special case without suffix) + if (isset($lib['frameworks'])) { + self::validateFieldType('frameworks', $lib['frameworks'], $name, 'lib'); } + + // Check for unknown fields + self::validateAllowedFields($lib, $name, 'lib', self::LIB_FIELDS); } } @@ -127,61 +241,34 @@ class ConfigValidator if (!is_array($data)) { throw new ValidationException('ext.json is broken'); } - // check each extension + foreach ($data as $name => $ext) { - // check if ext is an assoc array if (!is_assoc_array($ext)) { throw new ValidationException("ext {$name} is not an object"); } - // check if ext has valid type + if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) { throw new ValidationException("ext {$name} type is invalid"); } - // check if external ext has source + + // Check source field requirement if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) { throw new ValidationException("ext {$name} does not assign any source"); } - // check if source is string - if (isset($ext['source']) && !is_string($ext['source'])) { - throw new ValidationException("ext {$name} source must be string"); - } - // check if support is valid - if (isset($ext['support']) && !is_assoc_array($ext['support'])) { - throw new ValidationException("ext {$name} support must be an object"); - } - // check if notes is boolean - if (isset($ext['notes']) && !is_bool($ext['notes'])) { - throw new ValidationException("ext {$name} notes must be boolean"); - } - // check if [lib-depends|lib-suggests|ext-depends][-windows|-unix|-macos|-linux] are valid list array + + // Validate basic fields using unified method + self::validateConfigFields($ext, $name, 'ext', self::EXT_FIELDS); + + // Validate list array fields with suffixes $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - foreach ($suffixes as $suffix) { - if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) { - throw new ValidationException("ext {$name} lib-depends must be a list"); - } - if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) { - throw new ValidationException("ext {$name} lib-suggests must be a list"); - } - if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) { - throw new ValidationException("ext {$name} ext-depends must be a list"); - } - } - // check if arg-type is valid - if (isset($ext['arg-type'])) { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - if (!in_array($ext['arg-type'], $valid_arg_types)) { - throw new ValidationException("ext {$name} arg-type is invalid"); - } - } - // check if arg-type with suffix is valid - foreach ($suffixes as $suffix) { - if (isset($ext['arg-type' . $suffix])) { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - if (!in_array($ext['arg-type' . $suffix], $valid_arg_types)) { - throw new ValidationException("ext {$name} arg-type{$suffix} is invalid"); - } - } - } + $fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; + self::validateListArrayFields($ext, $name, 'ext', $fields, $suffixes); + + // Validate arg-type fields + self::validateArgTypeFields($ext, $name, $suffixes); + + // Check for unknown fields + self::validateAllowedFields($ext, $name, 'ext', self::EXT_FIELDS); } } @@ -200,12 +287,11 @@ class ConfigValidator // Validate basic source type configuration (reuse from source validation) self::validateSourceTypeConfig($pkg, $name, 'pkg'); - // Check pkg-specific fields - // check if extract-files is valid + // Validate all pkg-specific fields using unified method + self::validateConfigFields($pkg, $name, 'pkg', self::PKG_FIELDS); + + // Validate extract-files content (object validation is done by validateFieldType) if (isset($pkg['extract-files'])) { - if (!is_assoc_array($pkg['extract-files'])) { - throw new ValidationException("pkg {$name} extract-files must be an object"); - } // check each extract file mapping foreach ($pkg['extract-files'] as $source => $target) { if (!is_string($source) || !is_string($target)) { @@ -213,6 +299,9 @@ class ConfigValidator } } } + + // Check for unknown fields + self::validateAllowedFields($pkg, $name, 'pkg', self::PKG_FIELDS); } } @@ -227,18 +316,11 @@ class ConfigValidator throw new ValidationException('pre-built.json is broken'); } - // Check required fields - if (!isset($data['repo'])) { - throw new ValidationException('pre-built.json must have [repo] field'); - } - if (!is_string($data['repo'])) { - throw new ValidationException('pre-built.json [repo] must be string'); - } + // Validate all fields using unified method + self::validateConfigFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); - // Check optional prefer-stable field - if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) { - throw new ValidationException('pre-built.json [prefer-stable] must be boolean'); - } + // Check for unknown fields + self::validateAllowedFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); // Check match pattern fields (at least one must exist) $pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows']; @@ -247,9 +329,6 @@ class ConfigValidator foreach ($pattern_fields as $field) { if (isset($data[$field])) { $has_pattern = true; - if (!is_string($data[$field])) { - throw new ValidationException("pre-built.json [{$field}] must be string"); - } // Validate pattern contains required placeholders if (!str_contains($data[$field], '{name}')) { throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder"); @@ -403,6 +482,53 @@ class ConfigValidator return $craft; } + /** + * Validate a field based on its global type definition + * + * @param string $field Field name + * @param mixed $value Field value + * @param string $name Item name (for error messages) + * @param string $type Item type (for error messages) + * @return bool Returns true if validation passes + */ + private static function validateFieldType(string $field, mixed $value, string $name, string $type): bool + { + // Check if field exists in FIELD_TYPES + if (!isset(self::FIELD_TYPES[$field])) { + // Try to strip suffix and check base field name + $suffixes = ['-windows', '-unix', '-macos', '-linux']; + $base_field = $field; + foreach ($suffixes as $suffix) { + if (str_ends_with($field, $suffix)) { + $base_field = substr($field, 0, -strlen($suffix)); + break; + } + } + + if (!isset(self::FIELD_TYPES[$base_field])) { + // Unknown field is not allowed - strict validation + throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); + } + + // Use base field type for validation + $expected_type = self::FIELD_TYPES[$base_field]; + } else { + $expected_type = self::FIELD_TYPES[$field]; + } + + return match ($expected_type) { + 'string' => is_string($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be string"), + 'bool' => is_bool($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be boolean"), + 'array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be array"), + 'list' => is_list_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be a list"), + 'object' => is_assoc_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object"), + 'object|bool' => (is_assoc_array($value) || is_bool($value)) ?: throw new ValidationException("{$type} {$name} [{$field}] must be object or boolean"), + 'object|array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object or array"), + 'callable' => true, // Skip validation for callable + default => true, + }; + } + private static function checkSingleLicense(array $license, string $name): void { if (!is_assoc_array($license)) { @@ -414,9 +540,6 @@ class ConfigValidator if (!in_array($license['type'], ['file', 'text'])) { throw new ValidationException("source {$name} license type is invalid"); } - if (!in_array($license['type'], ['file', 'text'])) { - throw new ValidationException("source {$name} license type is invalid"); - } if ($license['type'] === 'file' && !isset($license['path'])) { throw new ValidationException("source {$name} license file must have path"); } @@ -440,68 +563,127 @@ class ConfigValidator if (!is_string($item['type'])) { throw new ValidationException("{$config_type} {$name} type prop must be string"); } - if (!in_array($item['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom'])) { + + if (!isset(self::SOURCE_TYPE_FIELDS[$item['type']])) { throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid"); } - // Validate type-specific requirements - switch ($item['type']) { - case 'filelist': - if (!isset($item['url'], $item['regex'])) { - throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props"); + [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; + + // Check required fields exist + foreach ($required as $prop) { + if (!isset($item[$prop])) { + $props = implode('] and [', $required); + throw new ValidationException("{$config_type} {$name} needs [{$props}] props"); + } + } + + // Validate field types using global field type definitions + foreach (array_merge($required, $optional) as $prop) { + if (isset($item[$prop])) { + self::validateFieldType($prop, $item[$prop], $name, $config_type); + } + } + } + + /** + * Validate that fields with suffixes are list arrays + */ + private static function validateListArrayFields(array $item, string $name, string $type, array $fields, array $suffixes): void + { + foreach ($fields as $field) { + foreach ($suffixes as $suffix) { + $key = $field . $suffix; + if (isset($item[$key])) { + self::validateFieldType($key, $item[$key], $name, $type); } - if (!is_string($item['url']) || !is_string($item['regex'])) { - throw new ValidationException("{$config_type} {$name} [url] and [regex] must be string"); + } + } + } + + /** + * Validate arg-type fields with suffixes + */ + private static function validateArgTypeFields(array $item, string $name, array $suffixes): void + { + $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; + + foreach (array_merge([''], $suffixes) as $suffix) { + $key = 'arg-type' . $suffix; + if (isset($item[$key]) && !in_array($item[$key], $valid_arg_types)) { + throw new ValidationException("ext {$name} {$key} is invalid"); + } + } + } + + /** + * Unified method to validate config fields based on field definitions + * + * @param array $item Item data to validate + * @param string $name Item name for error messages + * @param string $type Config type (source, lib, ext, pkg, pre-built) + * @param array $field_definitions Field definitions [field_name => required (bool)] + */ + private static function validateConfigFields(array $item, string $name, string $type, array $field_definitions): void + { + foreach ($field_definitions as $field => $required) { + if ($required && !isset($item[$field])) { + throw new ValidationException("{$type} {$name} must have [{$field}] field"); + } + + if (isset($item[$field])) { + self::validateFieldType($field, $item[$field], $name, $type); + } + } + } + + /** + * Validate that item only contains allowed fields + * This method checks for unknown fields based on the config type + * + * @param array $item Item data to validate + * @param string $name Item name for error messages + * @param string $type Config type (source, lib, ext, pkg, pre-built) + * @param array $field_definitions Field definitions [field_name => required (bool)] + */ + private static function validateAllowedFields(array $item, string $name, string $type, array $field_definitions): void + { + // For source and pkg types, we need to check SOURCE_TYPE_FIELDS as well + $allowed_fields = array_keys($field_definitions); + + // For source/pkg, add allowed fields from SOURCE_TYPE_FIELDS based on the type + if (in_array($type, ['source', 'pkg']) && isset($item['type'], self::SOURCE_TYPE_FIELDS[$item['type']])) { + [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; + $allowed_fields = array_merge($allowed_fields, $required, $optional); + } + + // For lib and ext types, add fields with suffixes + if (in_array($type, ['lib', 'ext'])) { + $suffixes = ['-windows', '-unix', '-macos', '-linux']; + $base_fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; + if ($type === 'ext') { + $base_fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; + // Add arg-type fields + foreach (array_merge([''], $suffixes) as $suffix) { + $allowed_fields[] = 'arg-type' . $suffix; } - break; - case 'git': - if (!isset($item['url'], $item['rev'])) { - throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props"); + } + foreach ($base_fields as $field) { + foreach ($suffixes as $suffix) { + $allowed_fields[] = $field . $suffix; } - if (!is_string($item['url']) || !is_string($item['rev'])) { - throw new ValidationException("{$config_type} {$name} [url] and [rev] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'ghtagtar': - case 'ghtar': - if (!isset($item['repo'])) { - throw new ValidationException("{$config_type} {$name} needs [repo] prop"); - } - if (!is_string($item['repo'])) { - throw new ValidationException("{$config_type} {$name} [repo] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'ghrel': - if (!isset($item['repo'], $item['match'])) { - throw new ValidationException("{$config_type} {$name} needs [repo] and [match] props"); - } - if (!is_string($item['repo']) || !is_string($item['match'])) { - throw new ValidationException("{$config_type} {$name} [repo] and [match] must be string"); - } - break; - case 'url': - if (!isset($item['url'])) { - throw new ValidationException("{$config_type} {$name} needs [url] prop"); - } - if (!is_string($item['url'])) { - throw new ValidationException("{$config_type} {$name} [url] must be string"); - } - if (isset($item['filename']) && !is_string($item['filename'])) { - throw new ValidationException("{$config_type} {$name} [filename] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'custom': - // custom type has no specific requirements - break; + } + // frameworks is lib-only + if ($type === 'lib') { + $allowed_fields[] = 'frameworks'; + } + } + + // Check each field in item + foreach (array_keys($item) as $field) { + if (!in_array($field, $allowed_fields)) { + throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); + } } } } From 4e4ce282dbece5381cfda430aeb2a1299cb8cb9b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:41:25 +0800 Subject: [PATCH 3/8] Fix zip extract not strip dir bug --- src/SPC/store/FileSystem.php | 59 ++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 7a574925..9cbd68b7 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -584,7 +584,7 @@ class FileSystem 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), - 'zip' => f_passthru("unzip {$filename} -d {$target}"), + 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException('unknown archive format: ' . $filename), }; } elseif (PHP_OS_FAMILY === 'Windows') { @@ -599,7 +599,7 @@ class FileSystem match (self::extname($filename)) { 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), 'xz', 'txz', 'gz', 'tgz', 'bz2' => cmd()->execWithResult("\"{$_7z}\" x -so {$filename} | tar -f - -x -C \"{$target}\" --strip-components 1"), - 'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"), + 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException("unknown archive format: {$filename}"), }; } @@ -644,4 +644,59 @@ class FileSystem SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path), }; } + + /** + * Unzip file with stripping top-level directory + */ + private static function unzipWithStrip(string $zip_file, string $extract_path): void + { + $temp_dir = self::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); + $zip_file = self::convertPath($zip_file); + $extract_path = self::convertPath($extract_path); + + // extract to temp dir + self::createDir($temp_dir); + + if (PHP_OS_FAMILY === 'Windows') { + $mute = defined('DEBUG_MODE') ? '' : ' > NUL'; + // use php-sdk-binary-tools/bin/7za.exe + $_7z = self::convertPath(getenv('PHP_SDK_PATH') . '/bin/7za.exe'); + f_passthru("\"{$_7z}\" x {$zip_file} -o{$temp_dir} -y{$mute}"); + } else { + $mute = defined('DEBUG_MODE') ? '' : ' > /dev/null'; + f_passthru("unzip \"{$zip_file}\" -d \"{$temp_dir}\"{$mute}"); + } + // scan first level dirs (relative, not recursive, include dirs) + $contents = self::scanDirFiles($temp_dir, false, true, true); + if ($contents === false) { + throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); + } + // if extract path already exists, remove it + if (is_dir($extract_path)) { + self::removeDir($extract_path); + } + // if only one dir, move its contents to extract_path using rename + $subdir = self::convertPath("{$temp_dir}/{$contents[0]}"); + if (count($contents) === 1 && is_dir($subdir)) { + rename($subdir, $extract_path); + } else { + // else, move all contents to extract_path + self::createDir($extract_path); + foreach ($contents as $item) { + $subdir = self::convertPath("{$temp_dir}/{$item}"); + if (is_dir($subdir)) { + // move all dir contents to extract_path (strip top-level) + $sub_contents = self::scanDirFiles($subdir, false, true, true); + if ($sub_contents === false) { + throw new FileSystemException('Cannot scan unzip temp sub-dir: ' . $subdir); + } + foreach ($sub_contents as $sub_item) { + rename(self::convertPath("{$subdir}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}")); + } + } else { + rename(self::convertPath("{$temp_dir}/{$item}"), self::convertPath("{$extract_path}/{$item}")); + } + } + } + } } From 49cfcbe92d58abb6c1d2310267001b2ac3505a68 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:41:33 +0800 Subject: [PATCH 4/8] Update docs --- docs/en/develop/source-module.md | 32 ++++++++++++++++++++++++++++++++ docs/zh/develop/source-module.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/docs/en/develop/source-module.md b/docs/en/develop/source-module.md index 4b1b1fc0..51c3ba3c 100644 --- a/docs/en/develop/source-module.md +++ b/docs/en/develop/source-module.md @@ -36,6 +36,7 @@ The following is the source download configuration corresponding to the `libeven The most important field here is `type`. Currently, the types it supports are: - `url`: Directly use URL to download, for example: `https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`. +- `pie`: Download PHP extensions from Packagist using the PIE (PHP Installer for Extensions) standard. - `ghrel`: Use the GitHub Release API to download, download the artifacts uploaded from the latest version released by maintainers. - `ghtar`: Use the GitHub Release API to download. Different from `ghrel`, `ghtar` is downloaded from the `source code (tar.gz)` in the latest Release of the project. @@ -89,6 +90,37 @@ Example (download the imagick extension and extract it to the extension storage } ``` +## Download type - pie + +PIE (PHP Installer for Extensions) type sources refer to downloading PHP extensions from Packagist that follow the PIE standard. +This method automatically fetches extension information from the Packagist repository and downloads the appropriate distribution file. + +The parameters included are: + +- `repo`: The Packagist vendor/package name, such as `vendor/package-name` + +Example (download a PHP extension from Packagist using PIE): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +The PIE download type will automatically detect the extension information from Packagist metadata, +including the download URL, version, and distribution type. +The extension must be marked as `type: php-ext` or contain `php-ext` metadata in its Packagist package definition. +::: + ## Download type - ghrel ghrel will download files from Assets uploaded in GitHub Release. diff --git a/docs/zh/develop/source-module.md b/docs/zh/develop/source-module.md index 00feb3c4..769ffa08 100644 --- a/docs/zh/develop/source-module.md +++ b/docs/zh/develop/source-module.md @@ -30,6 +30,7 @@ static-php-cli 的下载资源模块是一个主要的功能,它包含了所 这里最主要的字段是 `type`,目前它支持的类型有: - `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`。 +- `pie`: 使用 PIE(PHP Installer for Extensions)标准从 Packagist 下载 PHP 扩展。 - `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。 - `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 - `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 @@ -77,6 +78,36 @@ url 类型的资源指的是从 URL 直接下载文件。 } ``` +## 下载类型 - pie + +PIE(PHP Installer for Extensions)类型的资源是从 Packagist 下载遵循 PIE 标准的 PHP 扩展。 +该方法会自动从 Packagist 仓库获取扩展信息,并下载相应的分发文件。 + +包含的参数有: + +- `repo`: Packagist 的 vendor/package 名称,如 `vendor/package-name` + +例子(使用 PIE 从 Packagist 下载 PHP 扩展): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +PIE 下载类型会自动从 Packagist 元数据中检测扩展信息,包括下载 URL、版本和分发类型。 +扩展必须在其 Packagist 包定义中标记为 `type: php-ext` 或包含 `php-ext` 元数据。 +::: + ## 下载类型 - ghrel ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。 From 5e229a0b016f5edc15ea1a34ff81f2af3ff3cf29 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:41:41 +0800 Subject: [PATCH 5/8] Update dependencies --- composer.lock | 289 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 196 insertions(+), 93 deletions(-) diff --git a/composer.lock b/composer.lock index 069a340f..4b28fc61 100644 --- a/composer.lock +++ b/composer.lock @@ -8,7 +8,7 @@ "packages": [ { "name": "illuminate/collections", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -64,7 +64,7 @@ }, { "name": "illuminate/conditionable", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -110,7 +110,7 @@ }, { "name": "illuminate/contracts", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -158,7 +158,7 @@ }, { "name": "illuminate/macroable", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -416,16 +416,16 @@ }, { "name": "symfony/console", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", + "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db", "shasum": "" }, "require": { @@ -490,7 +490,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.3" + "source": "https://github.com/symfony/console/tree/v7.3.4" }, "funding": [ { @@ -510,7 +510,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-22T15:31:00+00:00" }, { "name": "symfony/deprecation-contracts", @@ -916,16 +916,16 @@ }, { "name": "symfony/process", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -957,7 +957,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.3" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -977,7 +977,7 @@ "type": "tidelift" } ], - "time": "2025-08-18T09:42:54+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/service-contracts", @@ -1064,16 +1064,16 @@ }, { "name": "symfony/string", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -1088,7 +1088,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -1131,7 +1130,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.3" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -1151,7 +1150,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "symfony/yaml", @@ -2861,16 +2860,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.87.1", + "version": "v3.89.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6" + "reference": "4dd6768cb7558440d27d18f54909eee417317ce9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2f5170365e2a422d0c5421f9c8818b2c078105f6", - "reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4dd6768cb7558440d27d18f54909eee417317ce9", + "reference": "4dd6768cb7558440d27d18f54909eee417317ce9", "shasum": "" }, "require": { @@ -2885,7 +2884,6 @@ "php": "^7.4 || ^8.0", "react/child-process": "^0.6.6", "react/event-loop": "^1.5", - "react/promise": "^3.3", "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", @@ -2897,12 +2895,13 @@ "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.29.14", + "infection/infection": "^0.31.0", "justinrainbow/json-schema": "^6.5", "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", @@ -2910,7 +2909,6 @@ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/polyfill-php84": "^1.33", "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" }, @@ -2953,7 +2951,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.0" }, "funding": [ { @@ -2961,20 +2959,20 @@ "type": "github" } ], - "time": "2025-09-02T15:27:36+00:00" + "time": "2025-10-18T19:30:16+00:00" }, { "name": "humbug/box", - "version": "4.6.6", + "version": "4.6.8", "source": { "type": "git", "url": "https://github.com/box-project/box.git", - "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2" + "reference": "05d205d99ddb72f3729658a0115db02cfc08912e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/box-project/box/zipball/09646041cb2e0963ab6ca109e1b366337617a0f2", - "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2", + "url": "https://api.github.com/repos/box-project/box/zipball/05d205d99ddb72f3729658a0115db02cfc08912e", + "reference": "05d205d99ddb72f3729658a0115db02cfc08912e", "shasum": "" }, "require": { @@ -2988,13 +2986,13 @@ "fidry/console": "^0.6.0", "fidry/filesystem": "^1.2.1", "humbug/php-scoper": "^0.18.14", - "justinrainbow/json-schema": "^5.2.12", + "justinrainbow/json-schema": "^6.2.0", "nikic/iter": "^2.2", "php": "^8.2", "phpdocumentor/reflection-docblock": "^5.4", "phpdocumentor/type-resolver": "^1.7", "psr/log": "^3.0", - "sebastian/diff": "^5.0", + "sebastian/diff": "^5.0 || ^6.0 || ^7.0", "seld/jsonlint": "^1.10.2", "seld/phar-utils": "^1.2", "symfony/finder": "^6.4.0 || ^7.0.0", @@ -3005,6 +3003,9 @@ "thecodingmachine/safe": "^2.5 || ^3.0", "webmozart/assert": "^1.11" }, + "conflict": { + "marc-mabe/php-enum": "<4.4" + }, "replace": { "symfony/polyfill-php80": "*", "symfony/polyfill-php81": "*", @@ -3070,22 +3071,22 @@ ], "support": { "issues": "https://github.com/box-project/box/issues", - "source": "https://github.com/box-project/box/tree/4.6.6" + "source": "https://github.com/box-project/box/tree/4.6.8" }, - "time": "2025-03-02T18:20:45+00:00" + "time": "2025-10-13T17:13:17+00:00" }, { "name": "humbug/php-scoper", - "version": "0.18.17", + "version": "0.18.18", "source": { "type": "git", "url": "https://github.com/humbug/php-scoper.git", - "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a" + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/php-scoper/zipball/0a2556c7c23776a61cf22689e2f24298ba00e33a", - "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd55d01a937602c9473cfbe0ecab9521cb9740aa", + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa", "shasum": "" }, "require": { @@ -3154,9 +3155,9 @@ "description": "Prefixes all PHP namespaces in a file or directory.", "support": { "issues": "https://github.com/humbug/php-scoper/issues", - "source": "https://github.com/humbug/php-scoper/tree/0.18.17" + "source": "https://github.com/humbug/php-scoper/tree/0.18.18" }, - "time": "2025-02-19T22:50:39+00:00" + "time": "2025-10-15T15:29:47+00:00" }, { "name": "jetbrains/phpstorm-stubs", @@ -3207,30 +3208,40 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.6.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3", + "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -3259,16 +3270,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.0" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-10-10T11:34:09+00:00" }, { "name": "kelunik/certificate", @@ -3502,6 +3513,79 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -4228,16 +4312,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -4282,7 +4361,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4607,16 +4686,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.53", + "version": "10.5.58", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "32768472ebfb6969e6c7399f1c7b09009723f653" + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653", - "reference": "32768472ebfb6969e6c7399f1c7b09009723f653", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", "shasum": "" }, "require": { @@ -4637,10 +4716,10 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.3", + "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", + "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.1", @@ -4688,7 +4767,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" }, "funding": [ { @@ -4712,7 +4791,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:40:06+00:00" + "time": "2025-09-28T12:04:46+00:00" }, { "name": "psr/event-dispatcher", @@ -5640,16 +5719,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.3", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { @@ -5705,15 +5784,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", @@ -5906,16 +5997,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -5924,7 +6015,7 @@ "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -5972,15 +6063,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", @@ -7108,16 +7211,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", + "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb", "shasum": "" }, "require": { @@ -7171,7 +7274,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.4" }, "funding": [ { @@ -7191,7 +7294,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "thecodingmachine/safe", From f8801e224f03c833b221ec13cee5306474e222c1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:43:05 +0800 Subject: [PATCH 6/8] phpstan fix --- src/SPC/store/Downloader.php | 2 +- src/SPC/util/ConfigValidator.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index e94bfedc..63bec807 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -668,7 +668,7 @@ class Downloader ]); break; case 'custom': // Custom download method, like API-based download or other - if (isset($conf['func']) && is_callable($conf['func'])) { + if (isset($conf['func'])) { $conf['name'] = $name; $conf['func']($force, $conf, $download_as); break; diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index 3cd828ed..445c6242 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -525,7 +525,6 @@ class ConfigValidator 'object|bool' => (is_assoc_array($value) || is_bool($value)) ?: throw new ValidationException("{$type} {$name} [{$field}] must be object or boolean"), 'object|array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object or array"), 'callable' => true, // Skip validation for callable - default => true, }; } From 487980c9a882933f347112b8614be8de41f8e7ca Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 20 Oct 2025 13:48:52 +0800 Subject: [PATCH 7/8] phpunit fix --- tests/SPC/util/ConfigValidatorTest.php | 1 - tests/SPC/util/SPCConfigUtilTest.php | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/SPC/util/ConfigValidatorTest.php b/tests/SPC/util/ConfigValidatorTest.php index f132636f..aba611a4 100644 --- a/tests/SPC/util/ConfigValidatorTest.php +++ b/tests/SPC/util/ConfigValidatorTest.php @@ -50,7 +50,6 @@ class ConfigValidatorTest extends TestCase 'filename' => 'test.tar.gz', 'path' => 'test/path', 'provide-pre-built' => true, - 'prefer-stable' => false, 'license' => [ 'type' => 'file', 'path' => 'LICENSE', diff --git a/tests/SPC/util/SPCConfigUtilTest.php b/tests/SPC/util/SPCConfigUtilTest.php index 92353824..c4b1427a 100644 --- a/tests/SPC/util/SPCConfigUtilTest.php +++ b/tests/SPC/util/SPCConfigUtilTest.php @@ -44,6 +44,9 @@ class SPCConfigUtilTest extends TestCase public function testConfig(): void { + if (PHP_OS_FAMILY !== 'Linux') { + $this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.'); + } // normal $result = (new SPCConfigUtil())->config(['bcmath']); $this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']); From dd752cd5be23c51a0dbcab3294261b4ae816fcf2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 21 Oct 2025 15:50:12 +0800 Subject: [PATCH 8/8] Fix windows 7z unzip strip function, fix windows pkg extract files path --- .gitignore | 3 +++ config/pkg.json | 6 +++--- src/SPC/store/FileSystem.php | 41 ++++++++++++++++++++++++------------ 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 4bf41b55..0d2bd554 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ docker/source/ # default package root directory /pkgroot/** +# Windows PHP SDK binary tools +/php-sdk-binary-tools/** + # default pack:lib and release directory /dist/** packlib_files.txt diff --git a/config/pkg.json b/config/pkg.json index 00e83595..d3b4fb90 100644 --- a/config/pkg.json +++ b/config/pkg.json @@ -23,8 +23,8 @@ "type": "url", "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", "extract-files": { - "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + "nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" } }, "pkg-config-aarch64-linux": { @@ -84,7 +84,7 @@ "repo": "upx/upx", "match": "upx.+-win64\\.zip", "extract-files": { - "upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe" + "upx.exe": "{pkg_root_path}/bin/upx.exe" } }, "zig-aarch64-linux": { diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 9cbd68b7..e66aee0a 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -635,7 +635,7 @@ class FileSystem private static function extractWithType(string $source_type, string $filename, string $extract_path): void { - logger()->debug('Extracting source [' . $source_type . ']: ' . $filename); + logger()->debug("Extracting source [{$source_type}]: {$filename}"); /* @phpstan-ignore-next-line */ match ($source_type) { SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path), @@ -680,23 +680,38 @@ class FileSystem if (count($contents) === 1 && is_dir($subdir)) { rename($subdir, $extract_path); } else { - // else, move all contents to extract_path - self::createDir($extract_path); + // else, if it contains only one dir, strip dir and copy other files + $dircount = 0; + $dir = []; + $top_files = []; foreach ($contents as $item) { - $subdir = self::convertPath("{$temp_dir}/{$item}"); - if (is_dir($subdir)) { - // move all dir contents to extract_path (strip top-level) - $sub_contents = self::scanDirFiles($subdir, false, true, true); - if ($sub_contents === false) { - throw new FileSystemException('Cannot scan unzip temp sub-dir: ' . $subdir); - } - foreach ($sub_contents as $sub_item) { - rename(self::convertPath("{$subdir}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}")); - } + if (is_dir(self::convertPath("{$temp_dir}/{$item}"))) { + ++$dircount; + $dir[] = $item; } else { + $top_files[] = $item; + } + } + // extract dir contents to extract_path + self::createDir($extract_path); + // extract move dir + if ($dircount === 1) { + $sub_contents = self::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true); + if ($sub_contents === false) { + throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); + } + foreach ($sub_contents as $sub_item) { + rename(self::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}")); + } + } else { + foreach ($dir as $item) { rename(self::convertPath("{$temp_dir}/{$item}"), self::convertPath("{$extract_path}/{$item}")); } } + // move top-level files to extract_path + foreach ($top_files as $top_file) { + rename(self::convertPath("{$temp_dir}/{$top_file}"), self::convertPath("{$extract_path}/{$top_file}")); + } } } }