PIE download support & Downloader and ConfigValidator enhance (#934)

This commit is contained in:
Jerry Ma 2025-10-21 16:20:13 +08:00 committed by GitHub
commit b519291fa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 938 additions and 494 deletions

View File

@ -1,13 +1,9 @@
name: Tests name: Tests
on: on:
push:
branches:
- main
paths:
- 'src/globals/test-extensions.php'
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
types: [ opened, synchronize, reopened ]
paths: paths:
- 'src/**' - 'src/**'
- 'config/**' - 'config/**'

3
.gitignore vendored
View File

@ -22,6 +22,9 @@ docker/source/
# default package root directory # default package root directory
/pkgroot/** /pkgroot/**
# Windows PHP SDK binary tools
/php-sdk-binary-tools/**
# default pack:lib and release directory # default pack:lib and release directory
/dist/** /dist/**
packlib_files.txt packlib_files.txt

View File

@ -150,6 +150,7 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log"
if [ -f "$(pwd)/craft.yml" ]; then if [ -f "$(pwd)/craft.yml" ]; then
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml"
fi fi

View File

@ -158,6 +158,7 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot"
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log"
if [ -f "$(pwd)/craft.yml" ]; then if [ -f "$(pwd)/craft.yml" ]; then
MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml"
fi fi

289
composer.lock generated
View File

@ -8,7 +8,7 @@
"packages": [ "packages": [
{ {
"name": "illuminate/collections", "name": "illuminate/collections",
"version": "v11.45.3", "version": "v11.46.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/collections.git", "url": "https://github.com/illuminate/collections.git",
@ -64,7 +64,7 @@
}, },
{ {
"name": "illuminate/conditionable", "name": "illuminate/conditionable",
"version": "v11.45.3", "version": "v11.46.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/conditionable.git", "url": "https://github.com/illuminate/conditionable.git",
@ -110,7 +110,7 @@
}, },
{ {
"name": "illuminate/contracts", "name": "illuminate/contracts",
"version": "v11.45.3", "version": "v11.46.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/contracts.git", "url": "https://github.com/illuminate/contracts.git",
@ -158,7 +158,7 @@
}, },
{ {
"name": "illuminate/macroable", "name": "illuminate/macroable",
"version": "v11.45.3", "version": "v11.46.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/illuminate/macroable.git", "url": "https://github.com/illuminate/macroable.git",
@ -416,16 +416,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v7.3.3", "version": "v7.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -490,7 +490,7 @@
"terminal" "terminal"
], ],
"support": { "support": {
"source": "https://github.com/symfony/console/tree/v7.3.3" "source": "https://github.com/symfony/console/tree/v7.3.4"
}, },
"funding": [ "funding": [
{ {
@ -510,7 +510,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-25T06:35:40+00:00" "time": "2025-09-22T15:31:00+00:00"
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
@ -916,16 +916,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v7.3.3", "version": "v7.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1" "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
"reference": "32241012d521e2e8a9d713adb0812bb773b907f1", "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -957,7 +957,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v7.3.3" "source": "https://github.com/symfony/process/tree/v7.3.4"
}, },
"funding": [ "funding": [
{ {
@ -977,7 +977,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-18T09:42:54+00:00" "time": "2025-09-11T10:12:26+00:00"
}, },
{ {
"name": "symfony/service-contracts", "name": "symfony/service-contracts",
@ -1064,16 +1064,16 @@
}, },
{ {
"name": "symfony/string", "name": "symfony/string",
"version": "v7.3.3", "version": "v7.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/string.git", "url": "https://github.com/symfony/string.git",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" "reference": "f96476035142921000338bad71e5247fbc138872"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
"reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", "reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1088,7 +1088,6 @@
}, },
"require-dev": { "require-dev": {
"symfony/emoji": "^7.1", "symfony/emoji": "^7.1",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0", "symfony/translation-contracts": "^2.5|^3.0",
@ -1131,7 +1130,7 @@
"utf8" "utf8"
], ],
"support": { "support": {
"source": "https://github.com/symfony/string/tree/v7.3.3" "source": "https://github.com/symfony/string/tree/v7.3.4"
}, },
"funding": [ "funding": [
{ {
@ -1151,7 +1150,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-25T06:35:40+00:00" "time": "2025-09-11T14:36:48+00:00"
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
@ -2861,16 +2860,16 @@
}, },
{ {
"name": "friendsofphp/php-cs-fixer", "name": "friendsofphp/php-cs-fixer",
"version": "v3.87.1", "version": "v3.89.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6" "reference": "4dd6768cb7558440d27d18f54909eee417317ce9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2f5170365e2a422d0c5421f9c8818b2c078105f6", "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/4dd6768cb7558440d27d18f54909eee417317ce9",
"reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6", "reference": "4dd6768cb7558440d27d18f54909eee417317ce9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2885,7 +2884,6 @@
"php": "^7.4 || ^8.0", "php": "^7.4 || ^8.0",
"react/child-process": "^0.6.6", "react/child-process": "^0.6.6",
"react/event-loop": "^1.5", "react/event-loop": "^1.5",
"react/promise": "^3.3",
"react/socket": "^1.16", "react/socket": "^1.16",
"react/stream": "^1.4", "react/stream": "^1.4",
"sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0",
@ -2897,12 +2895,13 @@
"symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.33", "symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.33", "symfony/polyfill-php81": "^1.33",
"symfony/polyfill-php84": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
}, },
"require-dev": { "require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7", "facile-it/paraunit": "^1.3.1 || ^2.7",
"infection/infection": "^0.29.14", "infection/infection": "^0.31.0",
"justinrainbow/json-schema": "^6.5", "justinrainbow/json-schema": "^6.5",
"keradus/cli-executor": "^2.2", "keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12", "mikey179/vfsstream": "^1.6.12",
@ -2910,7 +2909,6 @@
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", "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/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
}, },
@ -2953,7 +2951,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", "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": [ "funding": [
{ {
@ -2961,20 +2959,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-09-02T15:27:36+00:00" "time": "2025-10-18T19:30:16+00:00"
}, },
{ {
"name": "humbug/box", "name": "humbug/box",
"version": "4.6.6", "version": "4.6.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/box-project/box.git", "url": "https://github.com/box-project/box.git",
"reference": "09646041cb2e0963ab6ca109e1b366337617a0f2" "reference": "05d205d99ddb72f3729658a0115db02cfc08912e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/box-project/box/zipball/09646041cb2e0963ab6ca109e1b366337617a0f2", "url": "https://api.github.com/repos/box-project/box/zipball/05d205d99ddb72f3729658a0115db02cfc08912e",
"reference": "09646041cb2e0963ab6ca109e1b366337617a0f2", "reference": "05d205d99ddb72f3729658a0115db02cfc08912e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2988,13 +2986,13 @@
"fidry/console": "^0.6.0", "fidry/console": "^0.6.0",
"fidry/filesystem": "^1.2.1", "fidry/filesystem": "^1.2.1",
"humbug/php-scoper": "^0.18.14", "humbug/php-scoper": "^0.18.14",
"justinrainbow/json-schema": "^5.2.12", "justinrainbow/json-schema": "^6.2.0",
"nikic/iter": "^2.2", "nikic/iter": "^2.2",
"php": "^8.2", "php": "^8.2",
"phpdocumentor/reflection-docblock": "^5.4", "phpdocumentor/reflection-docblock": "^5.4",
"phpdocumentor/type-resolver": "^1.7", "phpdocumentor/type-resolver": "^1.7",
"psr/log": "^3.0", "psr/log": "^3.0",
"sebastian/diff": "^5.0", "sebastian/diff": "^5.0 || ^6.0 || ^7.0",
"seld/jsonlint": "^1.10.2", "seld/jsonlint": "^1.10.2",
"seld/phar-utils": "^1.2", "seld/phar-utils": "^1.2",
"symfony/finder": "^6.4.0 || ^7.0.0", "symfony/finder": "^6.4.0 || ^7.0.0",
@ -3005,6 +3003,9 @@
"thecodingmachine/safe": "^2.5 || ^3.0", "thecodingmachine/safe": "^2.5 || ^3.0",
"webmozart/assert": "^1.11" "webmozart/assert": "^1.11"
}, },
"conflict": {
"marc-mabe/php-enum": "<4.4"
},
"replace": { "replace": {
"symfony/polyfill-php80": "*", "symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*", "symfony/polyfill-php81": "*",
@ -3070,22 +3071,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/box-project/box/issues", "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", "name": "humbug/php-scoper",
"version": "0.18.17", "version": "0.18.18",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/humbug/php-scoper.git", "url": "https://github.com/humbug/php-scoper.git",
"reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a" "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/humbug/php-scoper/zipball/0a2556c7c23776a61cf22689e2f24298ba00e33a", "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd55d01a937602c9473cfbe0ecab9521cb9740aa",
"reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a", "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3154,9 +3155,9 @@
"description": "Prefixes all PHP namespaces in a file or directory.", "description": "Prefixes all PHP namespaces in a file or directory.",
"support": { "support": {
"issues": "https://github.com/humbug/php-scoper/issues", "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", "name": "jetbrains/phpstorm-stubs",
@ -3207,30 +3208,40 @@
}, },
{ {
"name": "justinrainbow/json-schema", "name": "justinrainbow/json-schema",
"version": "5.3.0", "version": "6.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/jsonrainbow/json-schema.git", "url": "https://github.com/jsonrainbow/json-schema.git",
"reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/68ba7677532803cc0c5900dd5a4d730537f2b2f3",
"reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", "reference": "68ba7677532803cc0c5900dd5a4d730537f2b2f3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.1" "ext-json": "*",
"marc-mabe/php-enum": "^4.0",
"php": "^7.2 || ^8.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", "friendsofphp/php-cs-fixer": "3.3.0",
"json-schema/json-schema-test-suite": "1.2.0", "json-schema/json-schema-test-suite": "^23.2",
"phpunit/phpunit": "^4.8.35" "marc-mabe/php-enum-phpstan": "^2.0",
"phpspec/prophecy": "^1.19",
"phpstan/phpstan": "^1.12",
"phpunit/phpunit": "^8.5"
}, },
"bin": [ "bin": [
"bin/validate-json" "bin/validate-json"
], ],
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.x-dev"
}
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"JsonSchema\\": "src/JsonSchema/" "JsonSchema\\": "src/JsonSchema/"
@ -3259,16 +3270,16 @@
} }
], ],
"description": "A library to validate a json schema.", "description": "A library to validate a json schema.",
"homepage": "https://github.com/justinrainbow/json-schema", "homepage": "https://github.com/jsonrainbow/json-schema",
"keywords": [ "keywords": [
"json", "json",
"schema" "schema"
], ],
"support": { "support": {
"issues": "https://github.com/jsonrainbow/json-schema/issues", "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", "name": "kelunik/certificate",
@ -3502,6 +3513,79 @@
], ],
"time": "2024-12-08T08:18:47+00:00" "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", "name": "myclabs/deep-copy",
"version": "1.13.4", "version": "1.13.4",
@ -4228,16 +4312,11 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.12.28", "version": "1.12.32",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9"
},
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
"reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4282,7 +4361,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-07-17T17:15:39+00:00" "time": "2025-09-30T10:16:31+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
@ -4607,16 +4686,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "10.5.53", "version": "10.5.58",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653" "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca",
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653", "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4637,10 +4716,10 @@
"phpunit/php-timer": "^6.0.0", "phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1", "sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0", "sebastian/code-unit": "^2.0.0",
"sebastian/comparator": "^5.0.3", "sebastian/comparator": "^5.0.4",
"sebastian/diff": "^5.1.1", "sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0", "sebastian/environment": "^6.1.0",
"sebastian/exporter": "^5.1.2", "sebastian/exporter": "^5.1.4",
"sebastian/global-state": "^6.0.2", "sebastian/global-state": "^6.0.2",
"sebastian/object-enumerator": "^5.0.0", "sebastian/object-enumerator": "^5.0.0",
"sebastian/recursion-context": "^5.0.1", "sebastian/recursion-context": "^5.0.1",
@ -4688,7 +4767,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "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": [ "funding": [
{ {
@ -4712,7 +4791,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-20T14:40:06+00:00" "time": "2025-09-28T12:04:46+00:00"
}, },
{ {
"name": "psr/event-dispatcher", "name": "psr/event-dispatcher",
@ -5640,16 +5719,16 @@
}, },
{ {
"name": "sebastian/comparator", "name": "sebastian/comparator",
"version": "5.0.3", "version": "5.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git", "url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5705,15 +5784,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues", "issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy", "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": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "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", "name": "sebastian/complexity",
@ -5906,16 +5997,16 @@
}, },
{ {
"name": "sebastian/exporter", "name": "sebastian/exporter",
"version": "5.1.2", "version": "5.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git", "url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "955288482d97c19a372d3f31006ab3f37da47adf" "reference": "0735b90f4da94969541dac1da743446e276defa6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
"reference": "955288482d97c19a372d3f31006ab3f37da47adf", "reference": "0735b90f4da94969541dac1da743446e276defa6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5924,7 +6015,7 @@
"sebastian/recursion-context": "^5.0" "sebastian/recursion-context": "^5.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.0" "phpunit/phpunit": "^10.5"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -5972,15 +6063,27 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues", "issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy", "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": [ "funding": [
{ {
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "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", "name": "sebastian/global-state",
@ -7108,16 +7211,16 @@
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
"version": "v7.3.3", "version": "v7.3.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/var-dumper.git", "url": "https://github.com/symfony/var-dumper.git",
"reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", "reference": "b8abe7daf2730d07dfd4b2ee1cecbf0dd2fbdabb",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -7171,7 +7274,7 @@
"dump" "dump"
], ],
"support": { "support": {
"source": "https://github.com/symfony/var-dumper/tree/v7.3.3" "source": "https://github.com/symfony/var-dumper/tree/v7.3.4"
}, },
"funding": [ "funding": [
{ {
@ -7191,7 +7294,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-13T11:49:31+00:00" "time": "2025-09-11T10:12:26+00:00"
}, },
{ {
"name": "thecodingmachine/safe", "name": "thecodingmachine/safe",

View File

@ -453,7 +453,7 @@
"type": "external", "type": "external",
"source": "msgpack", "source": "msgpack",
"arg-type-unix": "with", "arg-type-unix": "with",
"arg-type-win": "enable", "arg-type-windows": "enable",
"ext-depends": [ "ext-depends": [
"session" "session"
] ]
@ -885,6 +885,22 @@
"mysqli" "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": { "swoole-hook-pgsql": {
"support": { "support": {
"Windows": "no", "Windows": "no",
@ -914,22 +930,6 @@
"swoole" "swoole"
] ]
}, },
"swoole-hook-odbc": {
"support": {
"Windows": "no",
"BSD": "wip"
},
"notes": true,
"type": "addon",
"arg-type": "none",
"ext-depends": [
"pdo",
"swoole"
],
"lib-depends": [
"unixodbc"
]
},
"swow": { "swow": {
"support": { "support": {
"BSD": "wip" "BSD": "wip"

View File

@ -203,7 +203,6 @@
"libcares" "libcares"
], ],
"cpp-library": true, "cpp-library": true,
"provide-pre-built": true,
"frameworks": [ "frameworks": [
"CoreFoundation" "CoreFoundation"
] ]
@ -560,6 +559,21 @@
"zstd" "zstd"
] ]
}, },
"liburing": {
"source": "liburing",
"pkg-configs": [
"liburing",
"liburing-ffi"
],
"static-libs-linux": [
"liburing.a",
"liburing-ffi.a"
],
"headers-linux": [
"liburing/",
"liburing.h"
]
},
"libuuid": { "libuuid": {
"source": "libuuid", "source": "libuuid",
"static-libs-unix": [ "static-libs-unix": [
@ -940,20 +954,5 @@
"zstd.h", "zstd.h",
"zstd_errors.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"
]
} }
} }

View File

@ -23,8 +23,8 @@
"type": "url", "type": "url",
"url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip",
"extract-files": { "extract-files": {
"nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", "nasm.exe": "{php_sdk_path}/bin/nasm.exe",
"nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe"
} }
}, },
"pkg-config-aarch64-linux": { "pkg-config-aarch64-linux": {
@ -84,7 +84,7 @@
"repo": "upx/upx", "repo": "upx/upx",
"match": "upx.+-win64\\.zip", "match": "upx.+-win64\\.zip",
"extract-files": { "extract-files": {
"upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe" "upx.exe": "{pkg_root_path}/bin/upx.exe"
} }
}, },
"zig-aarch64-linux": { "zig-aarch64-linux": {

View File

@ -669,6 +669,15 @@
"path": "LICENSE.md" "path": "LICENSE.md"
} }
}, },
"liburing": {
"type": "ghtar",
"repo": "axboe/liburing",
"prefer-stable": true,
"license": {
"type": "file",
"path": "COPYING"
}
},
"libuuid": { "libuuid": {
"type": "git", "type": "git",
"url": "https://github.com/static-php/libuuid.git", "url": "https://github.com/static-php/libuuid.git",
@ -990,7 +999,6 @@
}, },
"snappy": { "snappy": {
"type": "git", "type": "git",
"repo": "google/snappy",
"rev": "main", "rev": "main",
"url": "https://github.com/google/snappy", "url": "https://github.com/google/snappy",
"license": { "license": {
@ -999,9 +1007,8 @@
} }
}, },
"spx": { "spx": {
"type": "git", "type": "pie",
"rev": "master", "repo": "noisebynorthwest/php-spx",
"url": "https://github.com/static-php/php-spx.git",
"path": "php-src/ext/spx", "path": "php-src/ext/spx",
"license": { "license": {
"type": "file", "type": "file",
@ -1155,14 +1162,5 @@
"type": "file", "type": "file",
"path": "LICENSE" "path": "LICENSE"
} }
},
"liburing": {
"type": "ghtar",
"repo": "axboe/liburing",
"prefer-stable": true,
"license": {
"type": "file",
"path": "COPYING"
}
} }
} }

View File

@ -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: 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`. - `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. - `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. - `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. 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 ## Download type - ghrel
ghrel will download files from Assets uploaded in GitHub Release. ghrel will download files from Assets uploaded in GitHub Release.

View File

@ -30,6 +30,7 @@ static-php-cli 的下载资源模块是一个主要的功能,它包含了所
这里最主要的字段是 `type`,目前它支持的类型有: 这里最主要的字段是 `type`,目前它支持的类型有:
- `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz` - `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`
- `pie`: 使用 PIEPHP Installer for Extensions标准从 Packagist 下载 PHP 扩展。
- `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。 - `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。
- `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 - `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。
- `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 - `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。
@ -77,6 +78,36 @@ url 类型的资源指的是从 URL 直接下载文件。
} }
``` ```
## 下载类型 - pie
PIEPHP 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
ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。 ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。

View File

@ -13,7 +13,7 @@ class spx extends Extension
{ {
public function getUnixConfigureArg(bool $shared = false): string public function getUnixConfigureArg(bool $shared = false): string
{ {
$arg = '--enable-spx' . ($shared ? '=shared' : ''); $arg = '--enable-SPX' . ($shared ? '=shared' : '');
if ($this->builder->getLib('zlib') !== null) { if ($this->builder->getLib('zlib') !== null) {
$arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH; $arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH;
} }
@ -29,4 +29,20 @@ class spx extends Extension
); );
return true; return true;
} }
public function patchBeforeBuildconf(): bool
{
FileSystem::replaceFileStr(
$this->source_dir . '/config.m4',
'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"',
'CFLAGS="$CFLAGS -pthread"'
);
FileSystem::replaceFileStr(
$this->source_dir . '/src/php_spx.h',
"extern zend_module_entry spx_module_entry;\n",
"extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n"
);
FileSystem::copy($this->source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h');
return true;
}
} }

View File

@ -288,7 +288,7 @@ class LinuxBuilder extends UnixBuilderBase
shell()->cd(SOURCE_PATH . '/php-src') shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile') ->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') ->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install"); ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi install-build install-headers install-programs");
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
$libDir = BUILD_LIB_PATH; $libDir = BUILD_LIB_PATH;
@ -377,12 +377,12 @@ class LinuxBuilder extends UnixBuilderBase
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); $config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$static = SPCTarget::isStatic() ? '-all-static' : ''; $static = SPCTarget::isStatic() ? '-all-static' : '';
$lib = BUILD_LIB_PATH; $lib = BUILD_LIB_PATH;
return [ return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LIBS' => $config['libs'], 'EXTRA_LIBS' => $config['libs'],
'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie",
]; ]);
} }
/** /**

View File

@ -267,7 +267,7 @@ class MacOSBuilder extends UnixBuilderBase
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src') shell()->cd(SOURCE_PATH . '/php-src')
->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install"); ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi install-build install-headers install-programs");
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') {
$AR = getenv('AR') ?: 'ar'; $AR = getenv('AR') ?: 'ar';
@ -281,10 +281,10 @@ class MacOSBuilder extends UnixBuilderBase
private function getMakeExtraVars(): array private function getMakeExtraVars(): array
{ {
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); $config = (new SPCConfigUtil($this, ['libs_only_deps' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
return [ return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => '-L' . BUILD_LIB_PATH, 'EXTRA_LDFLAGS_PROGRAM' => '-L' . BUILD_LIB_PATH,
'EXTRA_LIBS' => $config['libs'], 'EXTRA_LIBS' => $config['libs'],
]; ]);
} }
} }

View File

@ -222,11 +222,9 @@ class BuildPHPCommand extends BuildCommand
// ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ----------
$build_root_path = BUILD_ROOT_PATH; $build_root_path = BUILD_ROOT_PATH;
$cwd = getcwd();
$fixed = ''; $fixed = '';
$build_root_path = get_display_path($build_root_path);
if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) {
str_replace($cwd, '', $build_root_path);
$build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path);
$fixed = ' (host system)'; $fixed = ' (host system)';
} }
if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {

View File

@ -137,13 +137,18 @@ class ExceptionHandler
self::logError("\n----------------------------------------\n"); self::logError("\n----------------------------------------\n");
self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_OUTPUT_LOG)); // convert log file path if in docker
$spc_log_convert = get_display_path(SPC_OUTPUT_LOG);
$shell_log_convert = get_display_path(SPC_SHELL_LOG);
$spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR);
self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($spc_log_convert));
if (file_exists(SPC_SHELL_LOG)) { if (file_exists(SPC_SHELL_LOG)) {
self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_SHELL_LOG)); self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($shell_log_convert));
} }
if ($e->getExtraLogFiles() !== []) { if ($e->getExtraLogFiles() !== []) {
foreach ($e->getExtraLogFiles() as $key => $file) { foreach ($e->getExtraLogFiles() as $key => $file) {
self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none(SPC_LOGS_DIR . "/{$file}")); self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none("{$spc_logs_dir_convert}/{$file}"));
} }
} }
if (!defined('DEBUG_MODE')) { if (!defined('DEBUG_MODE')) {

View File

@ -16,6 +16,42 @@ use SPC\util\SPCTarget;
*/ */
class Downloader class Downloader
{ {
/**
* Get latest version from PIE config (Packagist)
*
* @param string $name Source name
* @param array $source Source meta info: [repo]
* @return array<int, string> [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 * Get latest version from BitBucket tag
* *
@ -317,84 +353,7 @@ class Downloader
if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) { if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) {
return; return;
} }
self::downloadByType($pkg['type'], $name, $pkg, $force, SPC_DOWNLOAD_PACKAGE);
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());
}
} }
/** /**
@ -439,80 +398,7 @@ class Downloader
return; return;
} }
try { self::downloadByType($source['type'], $name, $source, $force, $download_as);
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());
}
} }
/** /**
@ -713,4 +599,109 @@ class Downloader
} }
return false; 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<string>,
* 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'])) {
$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()}");
}
}
} }

View File

@ -584,7 +584,7 @@ class FileSystem
'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), '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"), 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"),
'bz2' => f_passthru("tar -xjf {$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), default => throw new FileSystemException('unknown archive format: ' . $filename),
}; };
} elseif (PHP_OS_FAMILY === 'Windows') { } elseif (PHP_OS_FAMILY === 'Windows') {
@ -599,7 +599,7 @@ class FileSystem
match (self::extname($filename)) { match (self::extname($filename)) {
'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), '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"), '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}"), default => throw new FileSystemException("unknown archive format: {$filename}"),
}; };
} }
@ -635,7 +635,7 @@ class FileSystem
private static function extractWithType(string $source_type, string $filename, string $extract_path): void 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 */ /* @phpstan-ignore-next-line */
match ($source_type) { match ($source_type) {
SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path), SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path),
@ -644,4 +644,74 @@ class FileSystem
SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path), 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, if it contains only one dir, strip dir and copy other files
$dircount = 0;
$dir = [];
$top_files = [];
foreach ($contents as $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}"));
}
}
}
} }

View File

@ -155,8 +155,8 @@ class LockFile
* @param string $name Source name * @param string $name Source name
* @param array{ * @param array{
* source_type: string, * source_type: string,
* dirname: ?string, * dirname?: ?string,
* filename: ?string, * filename?: ?string,
* move_path: ?string, * move_path: ?string,
* lock_as: int * lock_as: int
* } $data Source data * } $data Source data

View File

@ -11,6 +11,150 @@ use Symfony\Component\Yaml\Yaml;
class ConfigValidator 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 * Validate source.json
* *
@ -22,33 +166,20 @@ class ConfigValidator
// Validate basic source type configuration // Validate basic source type configuration
self::validateSourceTypeConfig($src, $name, 'source'); 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 // check if alt is valid
if (isset($src['alt'])) { if (isset($src['alt']) && is_assoc_array($src['alt'])) {
if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) { // validate alt source recursively
throw new ValidationException("source {$name} alt must be object or boolean"); self::validateSource([$name . '_alt' => $src['alt']]);
}
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");
} }
// check if license is valid // check if license is valid
if (isset($src['license'])) { 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'])) { if (is_assoc_array($src['license'])) {
self::checkSingleLicense($src['license'], $name); self::checkSingleLicense($src['license'], $name);
} elseif (is_list_array($src['license'])) { } elseif (is_list_array($src['license'])) {
@ -58,8 +189,6 @@ class ConfigValidator
} }
self::checkSingleLicense($license, $name); 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)) { if (!is_array($data)) {
throw new ValidationException('lib.json is broken'); throw new ValidationException('lib.json is broken');
} }
// check each lib
foreach ($data as $name => $lib) { foreach ($data as $name => $lib) {
// check if lib is an assoc array
if (!is_assoc_array($lib)) { if (!is_assoc_array($lib)) {
throw new ValidationException("lib {$name} is not an object"); 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']])) { if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) {
throw new ValidationException("lib {$name} assigns an invalid source: {$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'])) { // Validate basic fields using unified method
throw new ValidationException("lib {$name} source must be string"); self::validateConfigFields($lib, $name, 'lib', self::LIB_FIELDS);
}
// check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array // Validate list array fields with suffixes
$suffixes = ['', '-windows', '-unix', '-macos', '-linux']; $suffixes = ['', '-windows', '-unix', '-macos', '-linux'];
foreach ($suffixes as $suffix) { $fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin'];
if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) { self::validateListArrayFields($lib, $name, 'lib', $fields, $suffixes);
throw new ValidationException("lib {$name} lib-depends must be a list");
} // Validate frameworks (special case without suffix)
if (isset($lib['lib-suggests' . $suffix]) && !is_list_array($lib['lib-suggests' . $suffix])) { if (isset($lib['frameworks'])) {
throw new ValidationException("lib {$name} lib-suggests must be a list"); self::validateFieldType('frameworks', $lib['frameworks'], $name, 'lib');
}
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");
} }
// Check for unknown fields
self::validateAllowedFields($lib, $name, 'lib', self::LIB_FIELDS);
} }
} }
@ -127,61 +241,34 @@ class ConfigValidator
if (!is_array($data)) { if (!is_array($data)) {
throw new ValidationException('ext.json is broken'); throw new ValidationException('ext.json is broken');
} }
// check each extension
foreach ($data as $name => $ext) { foreach ($data as $name => $ext) {
// check if ext is an assoc array
if (!is_assoc_array($ext)) { if (!is_assoc_array($ext)) {
throw new ValidationException("ext {$name} is not an object"); throw new ValidationException("ext {$name} is not an object");
} }
// check if ext has valid type
if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) { if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) {
throw new ValidationException("ext {$name} type is invalid"); 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'])) { if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) {
throw new ValidationException("ext {$name} does not assign any source"); throw new ValidationException("ext {$name} does not assign any source");
} }
// check if source is string
if (isset($ext['source']) && !is_string($ext['source'])) { // Validate basic fields using unified method
throw new ValidationException("ext {$name} source must be string"); self::validateConfigFields($ext, $name, 'ext', self::EXT_FIELDS);
}
// check if support is valid // Validate list array fields with suffixes
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
$suffixes = ['', '-windows', '-unix', '-macos', '-linux']; $suffixes = ['', '-windows', '-unix', '-macos', '-linux'];
foreach ($suffixes as $suffix) { $fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests'];
if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) { self::validateListArrayFields($ext, $name, 'ext', $fields, $suffixes);
throw new ValidationException("ext {$name} lib-depends must be a list");
} // Validate arg-type fields
if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) { self::validateArgTypeFields($ext, $name, $suffixes);
throw new ValidationException("ext {$name} lib-suggests must be a list");
} // Check for unknown fields
if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) { self::validateAllowedFields($ext, $name, 'ext', self::EXT_FIELDS);
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");
}
}
}
} }
} }
@ -200,12 +287,11 @@ class ConfigValidator
// Validate basic source type configuration (reuse from source validation) // Validate basic source type configuration (reuse from source validation)
self::validateSourceTypeConfig($pkg, $name, 'pkg'); self::validateSourceTypeConfig($pkg, $name, 'pkg');
// Check pkg-specific fields // Validate all pkg-specific fields using unified method
// check if extract-files is valid self::validateConfigFields($pkg, $name, 'pkg', self::PKG_FIELDS);
// Validate extract-files content (object validation is done by validateFieldType)
if (isset($pkg['extract-files'])) { 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 // check each extract file mapping
foreach ($pkg['extract-files'] as $source => $target) { foreach ($pkg['extract-files'] as $source => $target) {
if (!is_string($source) || !is_string($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'); throw new ValidationException('pre-built.json is broken');
} }
// Check required fields // Validate all fields using unified method
if (!isset($data['repo'])) { self::validateConfigFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS);
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');
}
// Check optional prefer-stable field // Check for unknown fields
if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) { self::validateAllowedFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS);
throw new ValidationException('pre-built.json [prefer-stable] must be boolean');
}
// Check match pattern fields (at least one must exist) // Check match pattern fields (at least one must exist)
$pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows']; $pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows'];
@ -247,9 +329,6 @@ class ConfigValidator
foreach ($pattern_fields as $field) { foreach ($pattern_fields as $field) {
if (isset($data[$field])) { if (isset($data[$field])) {
$has_pattern = true; $has_pattern = true;
if (!is_string($data[$field])) {
throw new ValidationException("pre-built.json [{$field}] must be string");
}
// Validate pattern contains required placeholders // Validate pattern contains required placeholders
if (!str_contains($data[$field], '{name}')) { if (!str_contains($data[$field], '{name}')) {
throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder"); throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder");
@ -403,6 +482,52 @@ class ConfigValidator
return $craft; 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
};
}
private static function checkSingleLicense(array $license, string $name): void private static function checkSingleLicense(array $license, string $name): void
{ {
if (!is_assoc_array($license)) { if (!is_assoc_array($license)) {
@ -414,9 +539,6 @@ class ConfigValidator
if (!in_array($license['type'], ['file', 'text'])) { if (!in_array($license['type'], ['file', 'text'])) {
throw new ValidationException("source {$name} license type is invalid"); 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'])) { if ($license['type'] === 'file' && !isset($license['path'])) {
throw new ValidationException("source {$name} license file must have path"); throw new ValidationException("source {$name} license file must have path");
} }
@ -440,68 +562,127 @@ class ConfigValidator
if (!is_string($item['type'])) { if (!is_string($item['type'])) {
throw new ValidationException("{$config_type} {$name} type prop must be string"); 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"); throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid");
} }
// Validate type-specific requirements [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']];
switch ($item['type']) {
case 'filelist': // Check required fields exist
if (!isset($item['url'], $item['regex'])) { foreach ($required as $prop) {
throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props"); 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': foreach ($base_fields as $field) {
if (!isset($item['url'], $item['rev'])) { foreach ($suffixes as $suffix) {
throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props"); $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"); // frameworks is lib-only
} if ($type === 'lib') {
if (isset($item['path']) && !is_string($item['path'])) { $allowed_fields[] = 'frameworks';
throw new ValidationException("{$config_type} {$name} [path] must be string"); }
} }
break;
case 'ghtagtar': // Check each field in item
case 'ghtar': foreach (array_keys($item) as $field) {
if (!isset($item['repo'])) { if (!in_array($field, $allowed_fields)) {
throw new ValidationException("{$config_type} {$name} needs [repo] prop"); throw new ValidationException("{$type} {$name} has unknown field [{$field}]");
} }
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;
} }
} }
} }

View File

@ -300,3 +300,20 @@ function strip_ansi_colors(string $text): string
// Including color codes, cursor control, clear screen and other control sequences // Including color codes, cursor control, clear screen and other control sequences
return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text); return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text);
} }
/**
* Convert to a real path for display purposes, used in docker volumes.
*/
function get_display_path(string $path): string
{
$deploy_root = getenv('SPC_FIX_DEPLOY_ROOT');
if ($deploy_root === false) {
return $path;
}
$cwd = WORKING_DIR;
// replace build root with deploy root, only if path starts with build root
if (str_starts_with($path, $cwd)) {
return $deploy_root . substr($path, strlen($cwd));
}
throw new WrongUsageException("Cannot convert path: {$path}");
}

View File

@ -50,7 +50,6 @@ class ConfigValidatorTest extends TestCase
'filename' => 'test.tar.gz', 'filename' => 'test.tar.gz',
'path' => 'test/path', 'path' => 'test/path',
'provide-pre-built' => true, 'provide-pre-built' => true,
'prefer-stable' => false,
'license' => [ 'license' => [
'type' => 'file', 'type' => 'file',
'path' => 'LICENSE', 'path' => 'LICENSE',

View File

@ -44,6 +44,9 @@ class SPCConfigUtilTest extends TestCase
public function testConfig(): void public function testConfig(): void
{ {
if (PHP_OS_FAMILY !== 'Linux') {
$this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.');
}
// normal // normal
$result = (new SPCConfigUtil())->config(['bcmath']); $result = (new SPCConfigUtil())->config(['bcmath']);
$this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']); $this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']);