diff --git a/composer.json b/composer.json index aedd38d9..9bb4f3d6 100644 --- a/composer.json +++ b/composer.json @@ -34,20 +34,20 @@ "fidry/cpu-core-counter": "^1.3.0", "illuminate/container": "^10.49.0", "psr/container": "^2.0.2", - "symfony/console": "^6.4.27", - "symfony/event-dispatcher": "^6.4.25", - "symfony/process": "^6.4.26", + "symfony/console": "^6.4.32", + "symfony/event-dispatcher": "^6.4.32", + "symfony/process": "^6.4.33", "thephpf/attestation": "^0.0.5", "webmozart/assert": "^1.12.1" }, "require-dev": { "ext-openssl": "*", - "behat/behat": "^3.27.0", + "behat/behat": "^3.29.0", "bnf/phpstan-psr-container": "^1.1", "doctrine/coding-standard": "^14.0.0", - "phpstan/phpstan": "^2.1.32", + "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-webmozart-assert": "^2.0", - "phpunit/phpunit": "^10.5.58" + "phpunit/phpunit": "^10.5.63" }, "replace": { "symfony/polyfill-php81": "*", diff --git a/composer.lock b/composer.lock index 48278fed..654bc2d6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a0a5418f392e9a86fd3ad48a5be15549", + "content-hash": "6392877b0b53e6d18d3d156173dcbba3", "packages": [ { "name": "composer/ca-bundle", - "version": "1.5.9", + "version": "1.5.10", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "1905981ee626e6f852448b7aaa978f8666c5bc54" + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/1905981ee626e6f852448b7aaa978f8666c5bc54", - "reference": "1905981ee626e6f852448b7aaa978f8666c5bc54", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/961a5e4056dd2e4a2eedcac7576075947c28bf63", + "reference": "961a5e4056dd2e4a2eedcac7576075947c28bf63", "shasum": "" }, "require": { @@ -64,7 +64,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.9" + "source": "https://github.com/composer/ca-bundle/tree/1.5.10" }, "funding": [ { @@ -76,20 +76,20 @@ "type": "github" } ], - "time": "2025-11-06T11:46:17+00:00" + "time": "2025-12-08T15:06:51+00:00" }, { "name": "composer/class-map-generator", - "version": "1.7.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6" + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/2373419b7709815ed323ebf18c3c72d03ff4a8a6", - "reference": "2373419b7709815ed323ebf18c3c72d03ff4a8a6", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8f5fa3cc214230e71f54924bd0197a3bcc705eb1", + "reference": "8f5fa3cc214230e71f54924bd0197a3bcc705eb1", "shasum": "" }, "require": { @@ -133,7 +133,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.7.0" + "source": "https://github.com/composer/class-map-generator/tree/1.7.1" }, "funding": [ { @@ -145,7 +145,7 @@ "type": "github" } ], - "time": "2025-11-19T10:41:15+00:00" + "time": "2025-12-29T13:15:25+00:00" }, { "name": "composer/composer", @@ -793,21 +793,21 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.1", + "version": "6.6.4", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", "shasum": "" }, "require": { "ext-json": "*", - "marc-mabe/php-enum": "^4.0", + "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" }, "require-dev": { @@ -862,9 +862,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" }, - "time": "2025-11-07T18:30:29+00:00" + "time": "2025-12-19T15:01:32+00:00" }, { "name": "marc-mabe/php-enum", @@ -1391,16 +1391,16 @@ }, { "name": "symfony/console", - "version": "v6.4.27", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" + "reference": "0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3", + "reference": "0bc2199c6c1f05276b05956f1ddc63f6d7eb5fc3", "shasum": "" }, "require": { @@ -1465,7 +1465,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.27" + "source": "https://github.com/symfony/console/tree/v6.4.32" }, "funding": [ { @@ -1485,7 +1485,7 @@ "type": "tidelift" } ], - "time": "2025-10-06T10:25:16+00:00" + "time": "2026-01-13T08:45:59+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1556,16 +1556,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v6.4.25", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b0cf3162020603587363f0551cd3be43958611ff" + "reference": "99d7e101826e6610606b9433248f80c1997cd20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff", - "reference": "b0cf3162020603587363f0551cd3be43958611ff", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99d7e101826e6610606b9433248f80c1997cd20b", + "reference": "99d7e101826e6610606b9433248f80c1997cd20b", "shasum": "" }, "require": { @@ -1616,7 +1616,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.32" }, "funding": [ { @@ -1636,7 +1636,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2026-01-05T11:13:48+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -1716,16 +1716,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.24", + "version": "v6.4.30", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" + "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/441c6b69f7222aadae7cbf5df588496d5ee37789", + "reference": "441c6b69f7222aadae7cbf5df588496d5ee37789", "shasum": "" }, "require": { @@ -1762,7 +1762,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.24" + "source": "https://github.com/symfony/filesystem/tree/v6.4.30" }, "funding": [ { @@ -1782,20 +1782,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:14:14+00:00" + "time": "2025-11-26T14:43:45+00:00" }, { "name": "symfony/finder", - "version": "v6.4.27", + "version": "v6.4.33", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b" + "reference": "24965ca011dac87431729640feef8bcf7b5523e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/a1b6aa435d2fba50793b994a839c32b6064f063b", - "reference": "a1b6aa435d2fba50793b994a839c32b6064f063b", + "url": "https://api.github.com/repos/symfony/finder/zipball/24965ca011dac87431729640feef8bcf7b5523e0", + "reference": "24965ca011dac87431729640feef8bcf7b5523e0", "shasum": "" }, "require": { @@ -1830,7 +1830,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.27" + "source": "https://github.com/symfony/finder/tree/v6.4.33" }, "funding": [ { @@ -1850,7 +1850,7 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:32:00+00:00" + "time": "2026-01-26T13:03:48+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2269,16 +2269,16 @@ }, { "name": "symfony/process", - "version": "v6.4.26", + "version": "v6.4.33", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8" + "reference": "c46e854e79b52d07666e43924a20cb6dc546644e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/48bad913268c8cafabbf7034b39c8bb24fbc5ab8", - "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8", + "url": "https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e", + "reference": "c46e854e79b52d07666e43924a20cb6dc546644e", "shasum": "" }, "require": { @@ -2310,7 +2310,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.26" + "source": "https://github.com/symfony/process/tree/v6.4.33" }, "funding": [ { @@ -2330,7 +2330,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2026-01-23T16:02:12+00:00" }, { "name": "symfony/service-contracts", @@ -2421,16 +2421,16 @@ }, { "name": "symfony/string", - "version": "v6.4.26", + "version": "v6.4.30", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea" + "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5621f039a71a11c87c106c1c598bdcd04a19aeea", - "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea", + "url": "https://api.github.com/repos/symfony/string/zipball/50590a057841fa6bf69d12eceffce3465b9e32cb", + "reference": "50590a057841fa6bf69d12eceffce3465b9e32cb", "shasum": "" }, "require": { @@ -2486,7 +2486,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.26" + "source": "https://github.com/symfony/string/tree/v6.4.30" }, "funding": [ { @@ -2506,7 +2506,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:32:46+00:00" + "time": "2025-11-21T18:03:05+00:00" }, { "name": "thephpf/attestation", @@ -2632,16 +2632,16 @@ "packages-dev": [ { "name": "behat/behat", - "version": "v3.27.0", + "version": "v3.29.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "3282ad774358e4eaf533855e9a1f48559894d1b5" + "reference": "51bdf81639a14645c5d2c06926f4aa37d204921b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/3282ad774358e4eaf533855e9a1f48559894d1b5", - "reference": "3282ad774358e4eaf533855e9a1f48559894d1b5", + "url": "https://api.github.com/repos/Behat/Behat/zipball/51bdf81639a14645c5d2c06926f4aa37d204921b", + "reference": "51bdf81639a14645c5d2c06926f4aa37d204921b", "shasum": "" }, "require": { @@ -2660,8 +2660,8 @@ "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.68", "opis/json-schema": "^2.5", + "php-cs-fixer/shim": "^3.89", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.6", "rector/rector": "2.1.7", @@ -2677,11 +2677,6 @@ "bin/behat" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { "psr-4": { "Behat\\Hook\\": "src/Behat/Hook/", @@ -2721,22 +2716,36 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.27.0" + "source": "https://github.com/Behat/Behat/tree/v3.29.0" }, - "time": "2025-11-23T12:12:41+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-11T09:51:30+00:00" }, { "name": "behat/gherkin", - "version": "v4.15.0", + "version": "v4.16.1", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b" + "reference": "e26037937dfd48528746764dd870bc5d0836665f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/e26037937dfd48528746764dd870bc5d0836665f", + "reference": "e26037937dfd48528746764dd870bc5d0836665f", "shasum": "" }, "require": { @@ -2744,7 +2753,7 @@ "php": ">=8.1 <8.6" }, "require-dev": { - "cucumber/gherkin-monorepo": "dev-gherkin-v36.0.0", + "cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0", "friendsofphp/php-cs-fixer": "^3.77", "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", @@ -2790,9 +2799,23 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.15.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.16.1" }, - "time": "2025-11-05T15:34:04+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-08T16:12:58+00:00" }, { "name": "bnf/phpstan-psr-container", @@ -3065,16 +3088,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3117,9 +3140,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -3241,16 +3264,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.0", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", - "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -3282,17 +3305,17 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2025-08-30T15:50:23+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.32", + "version": "2.1.38", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", - "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", "shasum": "" }, "require": { @@ -3337,7 +3360,7 @@ "type": "github" } ], - "time": "2025-11-11T15:18:17+00:00" + "time": "2026-01-30T17:12:46+00:00" }, { "name": "phpstan/phpstan-webmozart-assert", @@ -3713,16 +3736,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.58", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", - "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -3743,7 +3766,7 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.4", + "sebastian/comparator": "^5.0.5", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.4", @@ -3794,7 +3817,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.58" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -3818,7 +3841,7 @@ "type": "tidelift" } ], - "time": "2025-09-28T12:04:46+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "sebastian/cli-parser", @@ -3990,16 +4013,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.4", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", - "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", "shasum": "" }, "require": { @@ -4055,7 +4078,7 @@ "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.4" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" }, "funding": [ { @@ -4075,7 +4098,7 @@ "type": "tidelift" } ], - "time": "2025-09-07T05:25:07+00:00" + "time": "2026-01-24T09:25:16+00:00" }, { "name": "sebastian/complexity", @@ -4775,32 +4798,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.25.1", + "version": "8.27.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "4caa5ec5a30b84b2305e80159c710d437f40cc40" + "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/4caa5ec5a30b84b2305e80159c710d437f40cc40", - "reference": "4caa5ec5a30b84b2305e80159c710d437f40cc40", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/29bdaee8b65e7ed2b8e702b01852edba8bae1769", + "reference": "29bdaee8b65e7ed2b8e702b01852edba8bae1769", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.2.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.2.0", "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.3.0", + "phpstan/phpdoc-parser": "^2.3.1", "squizlabs/php_codesniffer": "^4.0.1" }, "require-dev": { - "phing/phing": "3.0.1|3.1.0", + "phing/phing": "3.0.1|3.1.1", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.32", + "phpstan/phpstan": "2.1.37", "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.8", + "phpstan/phpstan-phpunit": "2.0.12", "phpstan/phpstan-strict-rules": "2.0.7", - "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.4.4" + "phpunit/phpunit": "9.6.31|10.5.60|11.4.4|11.5.49|12.5.7" }, "type": "phpcodesniffer-standard", "extra": { @@ -4824,7 +4847,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.25.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.27.1" }, "funding": [ { @@ -4836,7 +4859,7 @@ "type": "tidelift" } ], - "time": "2025-11-25T18:01:43+00:00" + "time": "2026-01-25T15:57:07+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -4919,16 +4942,16 @@ }, { "name": "symfony/config", - "version": "v6.4.28", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62" + "reference": "d445badf0ad2c2a492e38c0378c39997a56ef97b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/15947c18ef3ddb0b2f4ec936b9e90e2520979f62", - "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62", + "url": "https://api.github.com/repos/symfony/config/zipball/d445badf0ad2c2a492e38c0378c39997a56ef97b", + "reference": "d445badf0ad2c2a492e38c0378c39997a56ef97b", "shasum": "" }, "require": { @@ -4974,7 +4997,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.28" + "source": "https://github.com/symfony/config/tree/v6.4.32" }, "funding": [ { @@ -4994,20 +5017,20 @@ "type": "tidelift" } ], - "time": "2025-11-01T19:52:02+00:00" + "time": "2026-01-13T08:40:30+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.26", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898" + "reference": "b17882e933c4c606620247b6708ab53aa3b88753" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5f311eaf0b321f8ec640f6bae12da43a14026898", - "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b17882e933c4c606620247b6708ab53aa3b88753", + "reference": "b17882e933c4c606620247b6708ab53aa3b88753", "shasum": "" }, "require": { @@ -5059,7 +5082,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.26" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.32" }, "funding": [ { @@ -5079,20 +5102,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2026-01-23T10:54:33+00:00" }, { "name": "symfony/translation", - "version": "v6.4.26", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4" + "reference": "d6cc8e2fdd484f2f41d25938b0e8e3915de3cfbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/c8559fe25c7ee7aa9d28f228903a46db008156a4", - "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4", + "url": "https://api.github.com/repos/symfony/translation/zipball/d6cc8e2fdd484f2f41d25938b0e8e3915de3cfbc", + "reference": "d6cc8e2fdd484f2f41d25938b0e8e3915de3cfbc", "shasum": "" }, "require": { @@ -5158,7 +5181,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.26" + "source": "https://github.com/symfony/translation/tree/v6.4.32" }, "funding": [ { @@ -5178,7 +5201,7 @@ "type": "tidelift" } ], - "time": "2025-09-05T18:17:25+00:00" + "time": "2026-01-12T19:15:33+00:00" }, { "name": "symfony/translation-contracts", @@ -5345,16 +5368,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.26", + "version": "v6.4.30", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0" + "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", - "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8207ae83da19ee3748d6d4f567b4d9a7c656e331", + "reference": "8207ae83da19ee3748d6d4f567b4d9a7c656e331", "shasum": "" }, "require": { @@ -5397,7 +5420,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.26" + "source": "https://github.com/symfony/yaml/tree/v6.4.30" }, "funding": [ { @@ -5417,7 +5440,7 @@ "type": "tidelift" } ], - "time": "2025-09-26T15:07:38+00:00" + "time": "2025-12-02T11:50:18+00:00" }, { "name": "theseer/tokenizer", diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 8d279c63..f2e2e1af 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -228,18 +228,103 @@ The `build-path` may contain some templated values which are replaced: ##### `download-url-method` The `download-url-method` directive allows extension maintainers to -change the behaviour of downloading the source package. - - * Setting this to `composer-default`, which is the default value if not - specified, will use the default behaviour implemented by Composer, which is - to use the standard ZIP archive from the GitHub API (or other source control - system). - * Using `pre-packaged-source` will locate a source code package in the release - assets list based matching one of the following naming conventions: - * `php_{ExtensionName}-{Version}-src.tgz` (e.g. `php_myext-1.20.1-src.tgz`) - * `php_{ExtensionName}-{Version}-src.zip` (e.g. `php_myext-1.20.1-src.zip`) - * `{ExtensionName}-{Version}.tgz` (this is intended for backwards - compatibility with PECL packages) +change the behaviour of downloading the source package. This should be defined +as a list of supported methods, but for backwards compatibility a single +string may be used. + +The possible values are: + + - `composer-default` + - `pre-packaged-source` + - `pre-packaged-binary` + +The default value, if nothing is specified is `["composer-default"]`. + +###### composer-default + +Setting this to `composer-default`, which is the default value if nothing is +specified, will use the default behaviour implemented by Composer, which is +to use the standard ZIP archive from the GitHub API (or other source control +system). PIE will then build and install the extension from source. + +###### pre-packaged-source + +Using `pre-packaged-source` will locate a source code package in the release +assets list based matching one of the following naming conventions: + +* `php_{ExtensionName}-{Version}-src.tgz` (e.g. `php_myext-1.20.1-src.tgz`) +* `php_{ExtensionName}-{Version}-src.zip` (e.g. `php_myext-1.20.1-src.zip`) +* `{ExtensionName}-{Version}.tgz` (this is intended for backwards + compatibility with PECL packages) + +This is useful for scenarios where you might need additional dependencies +pulled into the source build, which would not be available if you downloaded +the ZIP archive from your repository. For example, if your extension uses Git +Submodules to include third party libraries statically in the build. + +###### pre-packaged-binary + +> [!CAUTION] +> If your extension depends on dynamically linked libraries, it is **not +> recommended** to use `pre-packaged-binary` option, as the correct version, +> or at least compatible linked libraries may not be available on the end +> user's system. Use with caution! + +Using `pre-packaged-binary` will attempt to locate a Zip (or TGZ) archive in +the release assets list based on matching one of the following naming +conventions: + + * `php_{ExtensionName}-{Version}_php{PhpVersion}-{Arch}-{OS}-{Libc}-{Debug}-{TSMode}.{Format}` + +The replacements are: + + * `{ExtensionName}` the name of your extension, e.g. `xdebug` (hint: this + is not your Composer package name!) + * `{PhpVersion}` the major and minor version of PHP, e.g. `8.5` + * `{Version}` the version of your extension, e.g. `1.20.1` + * `{Arch}` the architecture of the binary, one of `x86`, `x86_64`, `arm64` + * `{OS}` the operating system, one of `windows`, `darwin`, `linux`, `bsd`, `solaris`, `unknown` + * `{Libc}` the libc flavour, one of `glibc`, `musl`, `bsdlibc` + * `{Debug}` the debug mode, one of `debug`, `nodebug` (or omitted) + * `{TSMode}` the thread safety mode, one of `zts`, `nts` (or omitted) + * `{Format}` the archive format, one of `zip`, `tgz` - note that ZIP is + preferred as it means there are fewer dependencies for the end user + +> [!TIP] +> In order to generate pre-built binaries for PIE, you could use the +> [php/pie-ext-binary-builder](https://github.com/php/pie-ext-binary-builder) +> GitHub Action. This will build and name the assets correctly for you. + +Some examples of valid asset names: + + * `php_xdebug-4.1_php8.4-x86_64-linux-glibc.zip` (or `php_xdebug-4.1_php8.4-x86_64-glibc-nts.zip`) + * `php_xdebug-4.1_php8.4-x86_64-linux-musl.zip` (or `php_xdebug-4.1_php8.4-x86_64-musl-nts.zip`) + * `php_xdebug-4.1_php8.4-arm64-linux-glibc.zip` (or `php_xdebug-4.1_php8.4-arm64-glibc-nts.zip`) + * `php_xdebug-4.1_php8.4-arm64-linux-musl.zip` (or `php_xdebug-4.1_php8.4-arm64-musl-nts.zip`) + * `php_xdebug-4.1_php8.4-x86_64-linux-glibc-zts.zip` + * `php_xdebug-4.1_php8.4-x86_64-linux-musl-zts.zip` + * `php_xdebug-4.1_php8.4-arm64-linux-glibc-zts.zip` + * `php_xdebug-4.1_php8.4-arm64-linux-musl-zts.zip` + * `php_xdebug-4.1_php8.4-x86_64-linux-glibc-debug.zip` + * `php_xdebug-4.1_php8.4-x86_64-linux-musl-debug.zip` + * `php_xdebug-4.1_php8.4-arm64-linux-glibc-debug.zip` + * `php_xdebug-4.1_php8.4-arm64-linux-musl-debug.zip` + +It is recommended that `pre-packaged-binary` is combined with `composer-default` +as a fallback mechanism, if a particular combination is supported, but not +pre-packaged on the release, e.g. `"download-url-method": ["pre-packaged-binary", "composer-default"]`. +PIE will try to find a pre-packaged binary asset first, but if it cannot +find an appropriate binary, it will download the source code and build it +in the traditional manner. + +```json +{ + "name": "myvendor/myext", + "php-ext": { + "download-url-method": ["pre-packaged-binary", "composer-default"] + } +} +``` ##### `os-families` restrictions diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 24d19231..73debb0b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -175,31 +175,31 @@ parameters: path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$downloadUrlMethod is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$incompatibleOsFamilies is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$incompatibleOsFamilies is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$priority is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$priority is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportNts is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportNts is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportZts is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php - - message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportZts is assigned outside of the constructor\.$#' + message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportedDownloadUrlMethods is assigned outside of the constructor\.$#' identifier: property.readOnlyByPhpDocAssignNotInConstructor count: 1 path: src/DependencyResolver/Package.php diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json index 55ac0820..9095708b 100644 --- a/resources/composer-json-php-ext-schema.json +++ b/resources/composer-json-php-ext-schema.json @@ -41,10 +41,28 @@ "default": null }, "download-url-method": { - "type": "string", - "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", - "enum": ["composer-default", "pre-packaged-source"], - "example": "composer-default" + "oneOf": [ + { + "type": "string", + "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", + "deprecated": true, + "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"], + "example": "composer-default", + "default": "composer-default" + }, + { + "type": "array", + "description": "Multiple techniques can be specified, in which case PIE will try each in turn until one succeeds. The first technique that succeeds will be used.", + "items": { + "type": "string", + "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", + "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"], + "example": ["pre-packaged-binary", "composer-default"] + }, + "minItems": 1, + "default": ["composer-default"] + } + ] }, "os-families": { "type": "array", diff --git a/src/Building/ExtensionBinaryNotFound.php b/src/Building/ExtensionBinaryNotFound.php index 1074361b..9974ee98 100644 --- a/src/Building/ExtensionBinaryNotFound.php +++ b/src/Building/ExtensionBinaryNotFound.php @@ -10,6 +10,14 @@ class ExtensionBinaryNotFound extends RuntimeException { + public static function fromPrePackagedBinary(string $expectedBinaryName): self + { + return new self(sprintf( + 'Expected pre-packaged binary does not exist: %s', + $expectedBinaryName, + )); + } + public static function fromExpectedBinary(string $expectedBinaryName): self { return new self(sprintf( diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index ef05661e..26699e98 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -5,8 +5,10 @@ namespace Php\Pie\Building; use Composer\IO\IOInterface; +use LogicException; use Php\Pie\ComposerIntegration\BundledPhpExtensionsRepository; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; @@ -33,6 +35,46 @@ public function __invoke( array $configureOptions, IOInterface $io, PhpizePath|null $phpizePath, + ): BinaryFile { + $selectedDownloadMethod = DownloadUrlMethod::fromDownloadedPackage($downloadedPackage); + switch ($selectedDownloadMethod) { + case DownloadUrlMethod::PrePackagedBinary: + return $this->prePackagedBinary($downloadedPackage, $io); + + case DownloadUrlMethod::ComposerDefaultDownload: + case DownloadUrlMethod::PrePackagedSourceDownload: + return $this->buildFromSource($downloadedPackage, $targetPlatform, $configureOptions, $io, $phpizePath); + + default: + throw new LogicException('Unsupported download method: ' . $selectedDownloadMethod->value); + } + } + + private function prePackagedBinary( + DownloadedPackage $downloadedPackage, + IOInterface $io, + ): BinaryFile { + $expectedSoFile = $downloadedPackage->extractedSourcePath . '/' . $downloadedPackage->package->extensionName()->name() . '.so'; + + if (! file_exists($expectedSoFile)) { + throw ExtensionBinaryNotFound::fromPrePackagedBinary($expectedSoFile); + } + + $io->write(sprintf( + 'Pre-packaged binary found: %s', + $expectedSoFile, + )); + + return BinaryFile::fromFileWithSha256Checksum($expectedSoFile); + } + + /** @param list $configureOptions */ + private function buildFromSource( + DownloadedPackage $downloadedPackage, + TargetPlatform $targetPlatform, + array $configureOptions, + IOInterface $io, + PhpizePath|null $phpizePath, ): BinaryFile { $outputCallback = null; if ($io->isVerbose()) { diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index b036b24c..d64a4d79 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -48,6 +48,7 @@ public function __invoke( $composerPackage, ); + $builtBinaryFile = null; if ($composerRequest->operation->shouldBuild()) { $builtBinaryFile = ($this->pieBuild)( $downloadedPackage, @@ -75,6 +76,7 @@ public function __invoke( ($this->pieInstall)( $downloadedPackage, $composerRequest->targetPlatform, + $builtBinaryFile, $io, $composerRequest->attemptToSetupIniFile, ), diff --git a/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php b/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php new file mode 100644 index 00000000..a5782c56 --- /dev/null +++ b/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php @@ -0,0 +1,42 @@ + $downloadMethods + * @param array $failureReasons + */ + public static function fromDownloadUrlMethods(Package $piePackage, array $downloadMethods, array $failureReasons): self + { + $message = sprintf('Could not download %s', $piePackage->name()); + + if (count($downloadMethods) === 1) { + $first = array_key_first($downloadMethods); + $message .= sprintf(' using %s method: %s', $downloadMethods[$first]->value, $failureReasons[$downloadMethods[$first]->value] ?? '(unknown failure)'); + + return new self($message); + } + + $message .= ' using the following methods:' . PHP_EOL; + + foreach ($downloadMethods as $downloadMethod) { + $message .= sprintf(' - %s: %s%s', $downloadMethod->value, $failureReasons[$downloadMethod->value] ?? '(unknown failure)', PHP_EOL); + } + + return new self($message); + } +} diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 12118fb9..cf7b6e5a 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -17,6 +17,7 @@ use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\PackageReleaseAssets; use Psr\Container\ContainerInterface; +use Throwable; use function array_walk; use function pathinfo; @@ -69,38 +70,70 @@ function (OperationInterface $operation): void { return; } - $piePackage = Package::fromComposerCompletePackage($composerPackage); - $targetPlatform = $this->composerRequest->targetPlatform; - $downloadUrlMethod = DownloadUrlMethod::fromPackage($piePackage, $targetPlatform); - - // Exit early if we should just use Composer's normal download - if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { - return; - } - - $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); - if ($possibleAssetNames === null) { - return; + $piePackage = Package::fromComposerCompletePackage($composerPackage); + $targetPlatform = $this->composerRequest->targetPlatform; + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($piePackage, $targetPlatform); + + $selectedDownloadUrlMethod = null; + $downloadMethodFailures = []; + + foreach ($downloadUrlMethods as $downloadUrlMethod) { + $this->io->write('Trying to download using: ' . $downloadUrlMethod->value, verbosity: IOInterface::VERY_VERBOSE); + + // Exit early if we should just use Composer's normal download + if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { + $selectedDownloadUrlMethod = $downloadUrlMethod; + break; + } + + try { + $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); + } catch (Throwable $t) { + $downloadMethodFailures[$downloadUrlMethod->value] = $t->getMessage(); + $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: ' . $t->getMessage(), verbosity: IOInterface::VERBOSE); + continue; + } + + if ($possibleAssetNames === null) { + $downloadMethodFailures[$downloadUrlMethod->value] = 'No asset names'; + $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: No asset names', verbosity: IOInterface::VERBOSE); + continue; + } + + // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.) + $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); + + try { + $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $piePackage, + new HttpDownloader($this->io, $this->composer->getConfig()), + $downloadUrlMethod, + $possibleAssetNames, + ); + } catch (Throwable $t) { + $downloadMethodFailures[$downloadUrlMethod->value] = $t->getMessage(); + $this->io->write('Failed locating asset [' . $downloadUrlMethod->value . ']: ' . $t->getMessage(), verbosity: IOInterface::VERBOSE); + continue; + } + + $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); + $composerPackage->setDistUrl($url); + + if (pathinfo($url, PATHINFO_EXTENSION) === 'tgz') { + $composerPackage->setDistType('tar'); + } + + $selectedDownloadUrlMethod = $downloadUrlMethod; + break; } - // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.) - $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); - - $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( - $targetPlatform, - $piePackage, - new HttpDownloader($this->io, $this->composer->getConfig()), - $possibleAssetNames, - ); - - $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url); - $composerPackage->setDistUrl($url); - - if (pathinfo($url, PATHINFO_EXTENSION) !== 'tgz') { - return; + if ($selectedDownloadUrlMethod === null) { + throw CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods($piePackage, $downloadUrlMethods, $downloadMethodFailures); } - $composerPackage->setDistType('tar'); + $selectedDownloadUrlMethod->writeToComposerPackage($composerPackage); + $this->io->write('Selected download URL method: ' . $selectedDownloadUrlMethod->value . '', verbosity: IOInterface::VERBOSE); }, ); } diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index eb54d5d8..154c01ec 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -39,10 +39,11 @@ final class Package /** @var non-empty-list|null */ private array|null $compatibleOsFamilies = null; /** @var non-empty-list|null */ - private array|null $incompatibleOsFamilies = null; - private bool $supportZts = true; - private bool $supportNts = true; - private DownloadUrlMethod|null $downloadUrlMethod = null; + private array|null $incompatibleOsFamilies = null; + private bool $supportZts = true; + private bool $supportNts = true; + /** @var non-empty-list|null */ + private array|null $supportedDownloadUrlMethods = null; public function __construct( private readonly CompletePackageInterface $composerPackage, @@ -91,17 +92,15 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $package->priority = $phpExtOptions['priority'] ?? 80; if ($phpExtOptions !== null && array_key_exists('download-url-method', $phpExtOptions)) { - /** @var string|list $method */ - $method = $phpExtOptions['download-url-method']; - if (is_array($method)) { - if (count($method) === 1) { - $method = $method[0]; - } else { - $method = DownloadUrlMethod::ComposerDefaultDownload->value; - } + /** @var string|list $extOptionValue */ + $extOptionValue = $phpExtOptions['download-url-method']; + $methods = is_array($extOptionValue) ? $extOptionValue : [$extOptionValue]; + if (count($methods) > 0) { + $package->supportedDownloadUrlMethods = array_map( + static fn (string $method): DownloadUrlMethod => DownloadUrlMethod::from($method), + $methods, + ); } - - $package->downloadUrlMethod = DownloadUrlMethod::tryFrom($method); } return $package; @@ -231,8 +230,9 @@ public function supportNts(): bool return $this->supportNts; } - public function downloadUrlMethod(): DownloadUrlMethod|null + /** @return non-empty-list|null */ + public function supportedDownloadUrlMethods(): array|null { - return $this->downloadUrlMethod; + return $this->supportedDownloadUrlMethods; } } diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index 6a5a95d8..4b1d99d8 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -4,18 +4,29 @@ namespace Php\Pie\Downloading; +use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\PrePackagedBinaryAssetName; use Php\Pie\Platform\PrePackagedSourceAssetName; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; +use function array_key_exists; +use function array_merge; +use function assert; +use function is_string; +use function method_exists; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ enum DownloadUrlMethod: string { + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + case ComposerDefaultDownload = 'composer-default'; case WindowsBinaryDownload = 'windows-binary'; case PrePackagedSourceDownload = 'pre-packaged-source'; + case PrePackagedBinary = 'pre-packaged-binary'; /** @return non-empty-list|null */ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatform): array|null @@ -24,28 +35,45 @@ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatf self::WindowsBinaryDownload => WindowsExtensionAssetName::zipNames($targetPlatform, $package), self::PrePackagedSourceDownload => PrePackagedSourceAssetName::packageNames($package), self::ComposerDefaultDownload => null, + self::PrePackagedBinary => PrePackagedBinaryAssetName::packageNames($targetPlatform, $package), }; } - public static function fromPackage(Package $package, TargetPlatform $targetPlatform): self + public static function fromDownloadedPackage(DownloadedPackage $downloadedPackage): self + { + return self::fromComposerPackage($downloadedPackage->package->composerPackage()); + } + + public static function fromComposerPackage(CompletePackageInterface $completePackage): self + { + $extra = $completePackage->getExtra(); + + return self::from(array_key_exists(self::COMPOSER_PACKAGE_EXTRA_KEY, $extra) && is_string($extra[self::COMPOSER_PACKAGE_EXTRA_KEY]) ? $extra[self::COMPOSER_PACKAGE_EXTRA_KEY] : ''); + } + + public function writeToComposerPackage(CompletePackageInterface $composerPackage): void + { + assert(method_exists($composerPackage, 'setExtra')); + + $composerPackage->setExtra(array_merge($composerPackage->getExtra(), [self::COMPOSER_PACKAGE_EXTRA_KEY => $this->value])); + } + + /** @return non-empty-list */ + public static function possibleDownloadUrlMethodsForPackage(Package $package, TargetPlatform $targetPlatform): array { /** * PIE does not support building on Windows (yet, at least). Maintainers * should provide pre-built Windows binaries. */ if ($targetPlatform->operatingSystem === OperatingSystem::Windows) { - return self::WindowsBinaryDownload; + return [self::WindowsBinaryDownload]; } - /** - * Some packages pre-package source code (e.g. mongodb) as there are - * external dependencies in Git submodules that otherwise aren't - * included in GitHub/Gitlab/etc "dist" downloads - */ - if ($package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) { - return self::PrePackagedSourceDownload; + $configuredSupportedMethods = $package->supportedDownloadUrlMethods(); + if ($configuredSupportedMethods === null) { + return [self::ComposerDefaultDownload]; } - return self::ComposerDefaultDownload; + return $configuredSupportedMethods; } } diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php index 815086bd..e52d0101 100644 --- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -17,10 +17,8 @@ class CouldNotFindReleaseAsset extends RuntimeException { /** @param non-empty-list $expectedAssetNames */ - public static function forPackage(TargetPlatform $targetPlatform, Package $package, array $expectedAssetNames): self + public static function forPackage(TargetPlatform $targetPlatform, Package $package, DownloadUrlMethod $downloadUrlMethod, array $expectedAssetNames): self { - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); - if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) { return new self(sprintf( 'Windows archive with prebuilt extension for %s was not attached on release %s - looked for one of "%s"', @@ -37,10 +35,10 @@ public static function forPackage(TargetPlatform $targetPlatform, Package $packa )); } - public static function forPackageWithMissingTag(Package $package): self + public static function forPackageWithMissingTag(Package $package, DownloadUrlMethod $downloadUrlMethod): self { if ( - $package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload + $downloadUrlMethod === DownloadUrlMethod::PrePackagedSourceDownload && $package->composerPackage()->isDev() ) { return new self(sprintf( diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 04c7aed9..ad1bd13a 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -31,12 +31,14 @@ public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, HttpDownloader $httpDownloader, + DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): string { $releaseAsset = $this->selectMatchingReleaseAsset( $targetPlatform, $package, - $this->getReleaseAssetsForPackage($package, $httpDownloader), + $this->getReleaseAssetsForPackage($package, $httpDownloader, $downloadUrlMethod), + $downloadUrlMethod, $possibleReleaseAssetNames, ); @@ -56,6 +58,7 @@ private function selectMatchingReleaseAsset( TargetPlatform $targetPlatform, Package $package, array $releaseAssets, + DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): array { foreach ($releaseAssets as $releaseAsset) { @@ -64,13 +67,14 @@ private function selectMatchingReleaseAsset( } } - throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $possibleReleaseAssetNames); + throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $downloadUrlMethod, $possibleReleaseAssetNames); } /** @return list */ private function getReleaseAssetsForPackage( Package $package, HttpDownloader $httpDownloader, + DownloadUrlMethod $downloadUrlMethod, ): array { Assert::notNull($package->downloadUrl()); @@ -88,7 +92,7 @@ private function getReleaseAssetsForPackage( } catch (TransportException $t) { /** @link https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name */ if ($t->getStatusCode() === 404) { - throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package); + throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package, $downloadUrlMethod); } throw $t; diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php index 87a903f2..622bebdb 100644 --- a/src/Downloading/PackageReleaseAssets.php +++ b/src/Downloading/PackageReleaseAssets.php @@ -20,6 +20,7 @@ public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, HttpDownloader $httpDownloader, + DownloadUrlMethod $downloadUrlMethod, array $possibleReleaseAssetNames, ): string; } diff --git a/src/Installing/Install.php b/src/Installing/Install.php index ebdda216..81724d0a 100644 --- a/src/Installing/Install.php +++ b/src/Installing/Install.php @@ -19,6 +19,7 @@ interface Install public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, + BinaryFile|null $builtBinaryFile, IOInterface $io, bool $attemptToSetupIniFile, ): BinaryFile; diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index 02e32489..a58c14db 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -6,14 +6,18 @@ use Composer\IO\IOInterface; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\File\BinaryFile; use Php\Pie\File\Sudo; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use RuntimeException; +use Webmozart\Assert\Assert; -use function array_unshift; +use function array_map; +use function array_merge; use function file_exists; +use function implode; use function is_writable; use function sprintf; @@ -29,6 +33,7 @@ public function __construct(private readonly SetupIniFile $setupIniFile) public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, + BinaryFile|null $builtBinaryFile, IOInterface $io, bool $attemptToSetupIniFile, ): BinaryFile { @@ -41,7 +46,30 @@ public function __invoke( $sharedObjectName, ); - $makeInstallCommand = ['make', 'install']; + $installCommands = []; + switch (DownloadUrlMethod::fromDownloadedPackage($downloadedPackage)) { + case DownloadUrlMethod::PrePackagedBinary: + Assert::notNull($builtBinaryFile); + + if (file_exists($expectedSharedObjectLocation)) { + $installCommands[] = [ + 'rm', + '-v', + $expectedSharedObjectLocation, + ]; + } + + $installCommands[] = [ + 'cp', + '-v', + $builtBinaryFile->filePath, + $targetExtensionPath, + ]; + break; + + default: + $installCommands[] = ['make', 'install']; + } // If the target directory isn't writable, or a .so file already exists and isn't writable, try to use sudo if ( @@ -55,16 +83,20 @@ public function __invoke( 'Cannot write to %s, so using sudo to elevate privileges.', $targetExtensionPath, )); - array_unshift($makeInstallCommand, Sudo::find()); + $installCommands = array_map(static fn (array $command) => array_merge(['sudo'], $command), $installCommands); } - $makeInstallOutput = Process::run( - $makeInstallCommand, - $downloadedPackage->extractedSourcePath, - self::MAKE_INSTALL_TIMEOUT_SECS, - ); + $io->write(sprintf('Install commands are: %s', implode(', ', array_map(static fn (array $command) => implode(' ', $command), $installCommands))), verbosity: IOInterface::VERY_VERBOSE); - $io->write($makeInstallOutput, verbosity: IOInterface::VERY_VERBOSE); + foreach ($installCommands as $installCommand) { + $makeInstallOutput = Process::run( + $installCommand, + $downloadedPackage->extractedSourcePath, + self::MAKE_INSTALL_TIMEOUT_SECS, + ); + + $io->write($makeInstallOutput, verbosity: IOInterface::VERY_VERBOSE); + } if (! file_exists($expectedSharedObjectLocation)) { throw new RuntimeException('Install failed, ' . $expectedSharedObjectLocation . ' was not installed.'); diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index 97eae97e..e674a561 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -37,6 +37,7 @@ public function __construct(private readonly SetupIniFile $setupIniFile) public function __invoke( DownloadedPackage $downloadedPackage, TargetPlatform $targetPlatform, + BinaryFile|null $builtBinaryFile, IOInterface $io, bool $attemptToSetupIniFile, ): BinaryFile { diff --git a/src/Platform/DebugBuild.php b/src/Platform/DebugBuild.php new file mode 100644 index 00000000..691dd50d --- /dev/null +++ b/src/Platform/DebugBuild.php @@ -0,0 +1,12 @@ +find('otool'); + if ($otool !== null) { + return self::Bsd; + } + + $lddPath = $executableFinder->find('ldd'); + $lsPath = $executableFinder->find('ls'); + + if ($lddPath === null || $lsPath === null) { + return self::Gnu; + } + + try { + $linkResult = Process::run([$lddPath, $lsPath]); + } catch (Throwable) { + return self::Gnu; + } + + return str_contains($linkResult, 'musl') ? self::Musl : self::Gnu; + } +} diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php new file mode 100644 index 00000000..eda3f947 --- /dev/null +++ b/src/Platform/PrePackagedBinaryAssetName.php @@ -0,0 +1,71 @@ + */ + public static function packageNames(TargetPlatform $targetPlatform, Package $package): array + { + return array_values(array_unique([ + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s-%s%s%s.zip', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', + )), + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '', + )), + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s-%s%s%s.zip', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', + )), + strtolower(sprintf( + 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz', + $package->extensionName()->name(), + $package->version(), + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->architecture->name, + $targetPlatform->operatingSystemFamily->value, + $targetPlatform->libcFlavour()->value, + $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '', + $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts', + )), + ])); + } +} diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index c6abc55f..32108921 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -9,6 +9,7 @@ use Composer\Util\Platform; use Php\Pie\ExtensionName; use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\DebugBuild; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Util\Process; @@ -89,6 +90,18 @@ public function phpApiVersion(): string throw new RuntimeException('Failed to find PHP API version...'); } + public function debugMode(): DebugBuild + { + if ( + preg_match('/Debug Build([ =>\t]*)(.*)/', $this->phpinfo(), $m) + && $m[2] !== '' + ) { + return $m[2] === 'yes' ? DebugBuild::Debug : DebugBuild::NoDebug; + } + + throw new RuntimeException('Failed to find PHP API version...'); + } + /** @return non-empty-string */ public function extensionPath(): string { diff --git a/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php index c6cf5f62..9a50542c 100644 --- a/src/Platform/TargetPlatform.php +++ b/src/Platform/TargetPlatform.php @@ -21,6 +21,8 @@ */ class TargetPlatform { + private static LibcFlavour $libcFlavour; + public function __construct( public readonly OperatingSystem $operatingSystem, public readonly OperatingSystemFamily $operatingSystemFamily, @@ -32,6 +34,15 @@ public function __construct( ) { } + public function libcFlavour(): LibcFlavour + { + if (! isset(self::$libcFlavour)) { + self::$libcFlavour = LibcFlavour::detect(); + } + + return self::$libcFlavour; + } + public static function isRunningAsRoot(): bool { return function_exists('posix_getuid') && posix_getuid() === 0; diff --git a/test/assets/fake-ldd/bsdlibc/otool b/test/assets/fake-ldd/bsdlibc/otool new file mode 100755 index 00000000..69ef7881 --- /dev/null +++ b/test/assets/fake-ldd/bsdlibc/otool @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +echo "/bin/ls: + /usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0) + /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0) + /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)" diff --git a/test/assets/fake-ldd/glibc/ldd b/test/assets/fake-ldd/glibc/ldd new file mode 100755 index 00000000..0a6f9719 --- /dev/null +++ b/test/assets/fake-ldd/glibc/ldd @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +echo " linux-vdso.so.1 (0x000078fecccd6000) + libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x000078feccc57000) + libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x000078feccc49000) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000078fecca00000) + libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x000078fecc941000) + /lib64/ld-linux-x86-64.so.2 (0x000078fecccd8000)" diff --git a/test/assets/fake-ldd/musl/ldd b/test/assets/fake-ldd/musl/ldd new file mode 100755 index 00000000..1824d33d --- /dev/null +++ b/test/assets/fake-ldd/musl/ldd @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo " /lib/ld-musl-x86_64.so.1 (0x7146f93b1000) + libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7146f93b1000)" diff --git a/test/assets/pre-packaged-binary-examples/invalid/wrong-name.so b/test/assets/pre-packaged-binary-examples/invalid/wrong-name.so new file mode 100644 index 00000000..e69de29b diff --git a/test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so b/test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so new file mode 100644 index 00000000..e69de29b diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 05f95df4..c39b00bf 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -17,6 +17,7 @@ use function copy; use function realpath; use function sprintf; +use function str_contains; class CliContext implements Context { @@ -119,6 +120,13 @@ public function theExtensionShouldHaveBeenBuilt(): void return; } + if (str_contains($this->output, 'Found prebuilt archive')) { + Assert::contains($this->output, 'Found prebuilt archive'); + Assert::contains($this->output, 'Pre-packaged binary found'); + + return; + } + Assert::contains($this->output, 'phpize complete.'); Assert::contains($this->output, 'Configure complete'); Assert::contains($this->output, 'Build complete:'); @@ -141,6 +149,13 @@ public function theExtensionShouldHaveBeenBuiltWithOptions(): void return; } + if (str_contains($this->output, 'Found prebuilt archive')) { + Assert::contains($this->output, 'Found prebuilt archive'); + Assert::contains($this->output, 'Pre-packaged binary found'); + + return; + } + Assert::contains($this->output, 'phpize complete.'); Assert::contains($this->output, 'Configure complete with options: --with-hello-name=sup'); Assert::contains($this->output, 'Build complete:'); diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 73d1c80f..bcf221e0 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -11,6 +11,7 @@ use Php\Pie\Building\UnixBuild; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -25,9 +26,12 @@ #[CoversClass(UnixBuild::class)] final class UnixBuildTest extends TestCase { - private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const TEST_PREBUILT_PATH_VALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/valid'; + private const TEST_PREBUILT_PATH_INVALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/invalid'; - public function testUnixBuildCanBuildExtension(): void + public function testUnixSourceBuildCanBuildExtension(): void { if (Platform::isWindows()) { self::markTestSkipped('Unix build test cannot be run on Windows'); @@ -35,9 +39,14 @@ public function testUnixBuildCanBuildExtension(): void $output = new BufferIO(); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', @@ -76,7 +85,7 @@ public function testUnixBuildCanBuildExtension(): void (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } - public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(): void + public function testUnixSourceBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(): void { if (Platform::isWindows()) { self::markTestSkipped('Unix build test cannot be run on Windows'); @@ -84,9 +93,14 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( $output = new BufferIO(); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('mismatched_name'), 'pie_test_ext', @@ -113,7 +127,7 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( } } - public function testUnixBuildCanBuildExtensionWithBuildPath(): void + public function testUnixSourceBuildCanBuildExtensionWithBuildPath(): void { if (Platform::isWindows()) { self::markTestSkipped('Unix build test cannot be run on Windows'); @@ -126,6 +140,9 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); $composerPackage->method('getType')->willReturn('php-ext'); $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'pie_test_ext']); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( Package::fromComposerCompletePackage($composerPackage), @@ -161,6 +178,77 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } + public function testUnixBinaryBuildThrowsErrorWhenBinaryFileNotFound(): void + { + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $output = new BufferIO(); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext'); + $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + Package::fromComposerCompletePackage($composerPackage), + self::TEST_PREBUILT_PATH_INVALID, + ); + + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null); + $unixBuilder = new UnixBuild(); + + $this->expectException(ExtensionBinaryNotFound::class); + $this->expectExceptionMessage('Expected pre-packaged binary does not exist'); + $unixBuilder->__invoke( + $downloadedPackage, + $targetPlatform, + ['--enable-pie_test_ext'], + $output, + null, + ); + } + + public function testUnixBinaryBuildReturnsBinaryFile(): void + { + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $output = new BufferIO(); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext'); + $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + Package::fromComposerCompletePackage($composerPackage), + self::TEST_PREBUILT_PATH_VALID, + ); + + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null); + $unixBuilder = new UnixBuild(); + + $binaryFile = $unixBuilder->__invoke( + $downloadedPackage, + $targetPlatform, + ['--enable-pie_test_ext'], + $output, + null, + ); + + self::assertSame('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', $binaryFile->checksum); + self::assertStringEndsWith('pre-packaged-binary-examples/valid/pie_test_ext.so', $binaryFile->filePath); + } + public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void { if (Platform::isWindows()) { @@ -172,9 +260,14 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void $output = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', @@ -209,9 +302,14 @@ public function testVerboseOutputShowsCleanupMessages(): void $output = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', diff --git a/test/integration/Command/BuildCommandTest.php b/test/integration/Command/BuildCommandTest.php index 07123e58..342e81ee 100644 --- a/test/integration/Command/BuildCommandTest.php +++ b/test/integration/Command/BuildCommandTest.php @@ -10,6 +10,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use function str_contains; + #[CoversClass(BuildCommand::class)] class BuildCommandTest extends TestCase { @@ -36,6 +38,13 @@ public function testBuildCommandWillBuildTheExtension(): void return; } + if (str_contains($outputString, 'Found prebuilt archive')) { + self::assertStringContainsString('Found prebuilt archive', $outputString); + self::assertStringContainsString('Pre-packaged binary found', $outputString); + + return; + } + self::assertStringContainsString('phpize complete.', $outputString); self::assertStringContainsString('Configure complete', $outputString); self::assertStringContainsString('Build complete:', $outputString); diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index cd9cb4b3..f26b0ef7 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -9,6 +9,7 @@ use Composer\Package\CompletePackageInterface; use Composer\Util\HttpDownloader; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -65,6 +66,7 @@ public function testDeterminingReleaseAssetUrlForWindows(): void $targetPlatform, $package, new HttpDownloader($io, $config), + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index cae63683..f80e21d8 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -10,8 +10,10 @@ use Php\Pie\Building\UnixBuild; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\PickBestSetupIniApproach; use Php\Pie\Installing\SetupIniFile; use Php\Pie\Installing\UnixInstall; @@ -30,11 +32,18 @@ use function file_exists; use function is_executable; use function is_writable; +use function mkdir; +use function rename; +use function unlink; + +use const DIRECTORY_SEPARATOR; #[CoversClass(UnixInstall::class)] final class UnixInstallTest extends TestCase { - private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const TEST_PREBUILT_PATH = __DIR__ . '/../../assets/pre-packaged-binary-examples/install'; /** @return array */ public static function phpPathProvider(): array @@ -68,7 +77,7 @@ public static function phpPathProvider(): array } #[DataProvider('phpPathProvider')] - public function testUnixInstallCanInstallExtension(string $phpConfig): void + public function testUnixInstallCanInstallExtensionBuiltFromSource(string $phpConfig): void { assert($phpConfig !== ''); if (Platform::isWindows()) { @@ -79,9 +88,14 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable($phpConfig), null); $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( new Package( - $this->createMock(CompletePackageInterface::class), + $composerPackage, ExtensionType::PhpModule, ExtensionName::normaliseFromString('pie_test_ext'), 'pie_test_ext', @@ -91,7 +105,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void self::TEST_EXTENSION_PATH, ); - (new UnixBuild())->__invoke( + $built = (new UnixBuild())->__invoke( $downloadedPackage, $targetPlatform, ['--enable-pie_test_ext'], @@ -102,6 +116,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke( $downloadedPackage, $targetPlatform, + $built, $output, true, ); @@ -122,4 +137,89 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void (new Process(['make', 'clean'], $downloadedPackage->extractedSourcePath))->mustRun(); (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); } + + #[DataProvider('phpPathProvider')] + public function testUnixInstallCanInstallPrePackagedBinary(string $phpConfig): void + { + assert($phpConfig !== ''); + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $output = new BufferIO(); + $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable($phpConfig), null); + $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + + // First build it (otherwise the test assets would need to have a binary for every test platform...) + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + + $built = (new UnixBuild())->__invoke( + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('pie_test_ext'), + 'pie_test_ext', + '0.1.0', + null, + ), + self::TEST_EXTENSION_PATH, + ), + $targetPlatform, + ['--enable-pie_test_ext'], + $output, + null, + ); + + /** + * Move the built .so into a new path; this simulates a pre-packaged binary, which would not have Makefile etc + * so this ensures we're not accidentally relying on any build mechanism (`make install` or otherwise) + */ + mkdir(self::TEST_PREBUILT_PATH, 0777, true); + $prebuiltBinaryFilePath = self::TEST_PREBUILT_PATH . DIRECTORY_SEPARATOR . 'pie_test_ext.so'; + rename($built->filePath, $prebuiltBinaryFilePath); + + $prebuiltBinaryFile = BinaryFile::fromFileWithSha256Checksum($prebuiltBinaryFilePath); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]); + + $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke( + DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('pie_test_ext'), + 'pie_test_ext', + '0.1.0', + null, + ), + self::TEST_PREBUILT_PATH, + ), + $targetPlatform, + $prebuiltBinaryFile, + $output, + true, + ); + $outputString = $output->getOutput(); + + self::assertStringContainsString('Install complete: ' . $extensionPath . '/pie_test_ext.so', $outputString); + self::assertStringContainsString('You must now add "extension=pie_test_ext" to your php.ini', $outputString); + + self::assertSame($extensionPath . '/pie_test_ext.so', $installedSharedObject->filePath); + self::assertFileExists($installedSharedObject->filePath); + + $rmCommand = ['rm', $installedSharedObject->filePath]; + if (! is_writable($installedSharedObject->filePath)) { + array_unshift($rmCommand, 'sudo'); + } + + (new Process($rmCommand))->mustRun(); + unlink($prebuiltBinaryFile->filePath); + } } diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php index 086fe026..a4142da6 100644 --- a/test/integration/Installing/WindowsInstallTest.php +++ b/test/integration/Installing/WindowsInstallTest.php @@ -71,7 +71,7 @@ public function testWindowsInstallCanInstallExtension(): void $installer = new WindowsInstall(new SetupIniFile(new PickBestSetupIniApproach([]))); - $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output, true); + $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, null, $output, true); self::assertSame($extensionPath . '\php_pie_test_ext.dll', $installedDll->filePath); $outputString = $output->getOutput(); diff --git a/test/unit/Building/ExtensionBinaryNotFoundTest.php b/test/unit/Building/ExtensionBinaryNotFoundTest.php new file mode 100644 index 00000000..f2996ae9 --- /dev/null +++ b/test/unit/Building/ExtensionBinaryNotFoundTest.php @@ -0,0 +1,29 @@ +getMessage(), + ); + } + + public function testFromExpectedBinary(): void + { + self::assertSame( + 'Build complete, but expected /foo/bar does not exist.', + ExtensionBinaryNotFound::fromExpectedBinary('/foo/bar')->getMessage(), + ); + } +} diff --git a/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php b/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php new file mode 100644 index 00000000..7b0b0fce --- /dev/null +++ b/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php @@ -0,0 +1,73 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $e = CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods( + $package, + [DownloadUrlMethod::PrePackagedBinary], + [DownloadUrlMethod::PrePackagedBinary->value => 'A bad thing happened downloading the binary'], + ); + + self::assertSame( + 'Could not download foo/foo using pre-packaged-binary method: A bad thing happened downloading the binary', + $e->getMessage(), + ); + } + + public function testMultipleDownloadUrlMethods(): void + { + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $e = CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods( + $package, + [ + DownloadUrlMethod::PrePackagedBinary, + DownloadUrlMethod::PrePackagedSourceDownload, + ], + [ + DownloadUrlMethod::PrePackagedBinary->value => 'A bad thing happened downloading the binary', + DownloadUrlMethod::PrePackagedSourceDownload->value => 'Another bad thing happened downloading the source', + ], + ); + + self::assertSame( + 'Could not download foo/foo using the following methods: + - pre-packaged-binary: A bad thing happened downloading the binary + - pre-packaged-source: Another bad thing happened downloading the source +', + $e->getMessage(), + ); + } +} diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 5a356b8e..657a2aab 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -12,10 +12,13 @@ use Composer\IO\IOInterface; use Composer\Package\CompletePackage; use Composer\Package\Package; +use Php\Pie\ComposerIntegration\Listeners\CouldNotDetermineDownloadUrlMethod; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\Downloading\DownloadUrlMethod; +use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -256,6 +259,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void 'https://example.com/git-archive-zip-url', $composerPackage->getDistUrl(), ); + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); } public function testDistUrlIsUpdatedForWindowsInstallers(): void @@ -310,6 +314,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void 'https://example.com/windows-download-url', $composerPackage->getDistUrl(), ); + self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); } public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void @@ -369,6 +374,187 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void 'https://example.com/pre-packaged-source-download-url.tgz', $composerPackage->getDistUrl(), ); + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); self::assertSame('tar', $composerPackage->getDistType()); } + + public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'download-url-method' => ['pre-packaged-binary', 'composer-default'], + ]); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willReturn('https://example.com/pre-packaged-binary-download-url.tgz'); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(IOInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/pre-packaged-binary-download-url.tgz', + $composerPackage->getDistUrl(), + ); + self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage)); + self::assertSame('tar', $composerPackage->getDistType()); + } + + public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'download-url-method' => ['pre-packaged-binary', 'composer-default'], + ]); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willThrowException(new CouldNotFindReleaseAsset('nope not found')); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(IOInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/git-archive-zip-url', + $composerPackage->getDistUrl(), + ); + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage)); + self::assertSame('zip', $composerPackage->getDistType()); + } + + public function testNoSelectedDownloadUrlMethodWillThrowException(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'download-url-method' => ['pre-packaged-binary'], + ]); + + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willThrowException(new CouldNotFindReleaseAsset('nope not found')); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + $listener = new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(IOInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ); + + $this->expectException(CouldNotDetermineDownloadUrlMethod::class); + $this->expectExceptionMessage('Could not download foo/bar using pre-packaged-binary method: nope not found'); + $listener($installerEvent); + } } diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index d048da9c..14576c3b 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -148,33 +148,32 @@ public function testFromComposerCompletePackageWithBuildPath(): void self::assertSame('some/subdirectory/path/', $package->buildPath()); } - public function testDownloadUrlMethodWithStringHasValidDownloadUrlMethod(): void + public function testFromComposerCompletePackageWithStringDownloadUrlMethod(): void { $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); - $composerCompletePackage->setPhpExt(['download-url-method' => 'composer-default']); + $composerCompletePackage->setPhpExt(['download-url-method' => 'pre-packaged-binary']); - $package = Package::fromComposerCompletePackage($composerCompletePackage); - - self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $package->downloadUrlMethod()); + self::assertSame( + [DownloadUrlMethod::PrePackagedBinary], + Package::fromComposerCompletePackage($composerCompletePackage)->supportedDownloadUrlMethods(), + ); } - public function testDownloadUrlMethodWithSingleItemListHasValidDownloadUrlMethod(): void + public function testFromComposerCompletePackageWithListDownloadUrlMethods(): void { $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); - $composerCompletePackage->setPhpExt(['download-url-method' => ['composer-default']]); + $composerCompletePackage->setPhpExt(['download-url-method' => ['pre-packaged-binary', 'composer-default']]); - $package = Package::fromComposerCompletePackage($composerCompletePackage); - - self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $package->downloadUrlMethod()); + self::assertSame( + [DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::ComposerDefaultDownload], + Package::fromComposerCompletePackage($composerCompletePackage)->supportedDownloadUrlMethods(), + ); } - public function testDownloadUrlMethodWithMultiItemListIsNotYetSupported(): void + public function testFromComposerCompletePackageWithOmittedDownloadUrlMethod(): void { - $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); - $composerCompletePackage->setPhpExt(['download-url-method' => ['pre-packaged-source', 'composer-default']]); - - $package = Package::fromComposerCompletePackage($composerCompletePackage); - - self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $package->downloadUrlMethod()); + self::assertNull(Package::fromComposerCompletePackage( + new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'), + )->supportedDownloadUrlMethods()); } } diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index 26712bec..b1113b9d 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -4,12 +4,15 @@ namespace Php\PieUnitTest\Downloading; +use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\DebugBuild; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; @@ -18,6 +21,9 @@ use Php\Pie\Platform\WindowsCompiler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use ValueError; + +use function array_key_first; #[CoversClass(DownloadUrlMethod::class)] final class DownloadUrlMethodTest extends TestCase @@ -48,7 +54,10 @@ public function testWindowsPackages(): void WindowsCompiler::VC15, ); - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, $downloadUrlMethod); @@ -81,7 +90,10 @@ public function testPrePackagedSourceDownloads(): void null, ); - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $downloadUrlMethod); @@ -95,6 +107,50 @@ public function testPrePackagedSourceDownloads(): void ); } + public function testPrePackagedBinaryDownloads(): void + { + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => ['pre-packaged-binary']]); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath + ->method('majorMinorVersion') + ->willReturn('8.3'); + $phpBinaryPath + ->method('debugMode') + ->willReturn(DebugBuild::Debug); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; + + self::assertSame(DownloadUrlMethod::PrePackagedBinary, $downloadUrlMethod); + + self::assertSame( + [ + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-zts.zip', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-zts.tgz', + ], + $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), + ); + } + public function testComposerDefaultDownload(): void { $package = new Package( @@ -116,10 +172,108 @@ public function testComposerDefaultDownload(): void null, ); - $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(1, $downloadUrlMethods); + $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)]; self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $downloadUrlMethod); self::assertNull($downloadUrlMethod->possibleAssetNames($package, $targetPlatform)); } + + public function testMultipleDownloadUrlMethods(): void + { + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => ['pre-packaged-binary', 'pre-packaged-source', 'composer-default']]); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath + ->method('majorMinorVersion') + ->willReturn('8.3'); + $phpBinaryPath + ->method('debugMode') + ->willReturn(DebugBuild::Debug); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform); + + self::assertCount(3, $downloadUrlMethods); + + $firstMethod = $downloadUrlMethods[0]; + self::assertSame(DownloadUrlMethod::PrePackagedBinary, $firstMethod); + self::assertSame( + [ + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug.zip', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug.tgz', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-nts.zip', + 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-nts.tgz', + ], + $firstMethod->possibleAssetNames($package, $targetPlatform), + ); + + $secondMethod = $downloadUrlMethods[1]; + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $secondMethod); + self::assertSame( + [ + 'php_bar-1.2.3-src.tgz', + 'php_bar-1.2.3-src.zip', + 'bar-1.2.3.tgz', + ], + $secondMethod->possibleAssetNames($package, $targetPlatform), + ); + + $thirdMethod = $downloadUrlMethods[2]; + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $thirdMethod); + self::assertNull($thirdMethod->possibleAssetNames($package, $targetPlatform)); + } + + public function testFromComposerPackageWhenPackageKeyWasDefined(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + DownloadUrlMethod::PrePackagedBinary->writeToComposerPackage($composerPackage); + self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage)); + } + + public function testFromComposerPackageWhenPackageKeyWasNotDefined(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + + $this->expectException(ValueError::class); + DownloadUrlMethod::fromComposerPackage($composerPackage); + } + + public function testFromDownloadedPackage(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + DownloadUrlMethod::PrePackagedSourceDownload->writeToComposerPackage($composerPackage); + + $downloaded = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/bar', + '1.2.3', + null, + ), + '/path/to/extracted/source', + ); + + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromDownloadedPackage($downloaded)); + } } diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index d7b153d3..32bd161c 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -6,6 +6,7 @@ use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -44,6 +45,7 @@ public function testForPackageWithRegularPackage(): void null, ), $package, + DownloadUrlMethod::PrePackagedSourceDownload, ['something.zip', 'something2.zip'], ); @@ -72,6 +74,7 @@ public function testForPackageWithWindowsPackage(): void WindowsCompiler::VS17, ), $package, + DownloadUrlMethod::WindowsBinaryDownload, ['something.zip', 'something2.zip'], ); @@ -89,7 +92,7 @@ public function testForPackageWithMissingTag(): void null, ); - $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); + $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package, DownloadUrlMethod::PrePackagedSourceDownload); self::assertSame('Could not find release by tag name for foo/bar:1.2.3', $exception->getMessage()); } diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index eb2985fd..4002a366 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -9,6 +9,7 @@ use Composer\Util\Http\Response; use Composer\Util\HttpDownloader; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\ExtensionName; @@ -86,6 +87,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $targetPlatform, $package, $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, @@ -151,6 +153,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr $targetPlatform, $package, $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, @@ -196,6 +199,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $targetPlatform, $package, $httpDownloader, + DownloadUrlMethod::WindowsBinaryDownload, WindowsExtensionAssetName::zipNames( $targetPlatform, $package, diff --git a/test/unit/Platform/LibcFlavourTest.php b/test/unit/Platform/LibcFlavourTest.php new file mode 100644 index 00000000..231462e8 --- /dev/null +++ b/test/unit/Platform/LibcFlavourTest.php @@ -0,0 +1,63 @@ +createMock(PhpBinaryPath::class); + $php->method('debugMode')->willReturn(DebugBuild::NoDebug); + $php->method('majorMinorVersion')->willReturn('8.2'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $php, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $libc = $targetPlatform->libcFlavour(); + self::assertSame( + [ + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '.zip', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '.tgz', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '-nts.zip', + 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '-nts.tgz', + ], + PrePackagedBinaryAssetName::packageNames( + $targetPlatform, + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } + + public function testPackageNamesZts(): void + { + $php = $this->createMock(PhpBinaryPath::class); + $php->method('debugMode')->willReturn(DebugBuild::NoDebug); + $php->method('majorMinorVersion')->willReturn('8.3'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $php, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $libc = $targetPlatform->libcFlavour(); + self::assertSame( + [ + 'php_foobar-1.2.3_php8.3-x86_64-linux-' . $libc->value . '-zts.zip', + 'php_foobar-1.2.3_php8.3-x86_64-linux-' . $libc->value . '-zts.tgz', + ], + PrePackagedBinaryAssetName::packageNames( + $targetPlatform, + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } + + public function testPackageNamesDebug(): void + { + $php = $this->createMock(PhpBinaryPath::class); + $php->method('debugMode')->willReturn(DebugBuild::Debug); + $php->method('majorMinorVersion')->willReturn('8.4'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Darwin, + $php, + Architecture::arm64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $libc = $targetPlatform->libcFlavour(); + self::assertSame( + [ + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug.zip', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug.tgz', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug-nts.zip', + 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug-nts.tgz', + ], + PrePackagedBinaryAssetName::packageNames( + $targetPlatform, + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); + } +} diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 3bf6b74e..321ac7d8 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -8,6 +8,7 @@ use Composer\Util\Platform; use Php\Pie\ExtensionName; use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\DebugBuild; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; @@ -445,4 +446,26 @@ public function testBuildProviderNullWhenNotConfigured(): void self::assertNull($phpBinary->buildProvider()); } + + public function testDebugBuildModeReturnsDebugWhenYes(): void + { + $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']); + + $phpBinary->expects(self::once()) + ->method('phpinfo') + ->willReturn('Debug Build => no'); + + self::assertSame(DebugBuild::NoDebug, $phpBinary->debugMode()); + } + + public function testDebugBuildModeReturnsNoDebugWhenNo(): void + { + $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']); + + $phpBinary->expects(self::once()) + ->method('phpinfo') + ->willReturn('Debug Build => yes'); + + self::assertSame(DebugBuild::Debug, $phpBinary->debugMode()); + } } diff --git a/test/unit/Platform/TargetPlatformTest.php b/test/unit/Platform/TargetPlatformTest.php index 3753181a..48c79df3 100644 --- a/test/unit/Platform/TargetPlatformTest.php +++ b/test/unit/Platform/TargetPlatformTest.php @@ -107,4 +107,12 @@ public function testLinuxPlatform(): void self::assertSame(ThreadSafetyMode::NonThreadSafe, $platform->threadSafety); self::assertSame(Architecture::x86_64, $platform->architecture); } + + public function testLibcFlavourIsMemoized(): void + { + self::assertSame( + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null)->libcFlavour(), + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null)->libcFlavour(), + ); + } }