From 86192a422882de6f525c16cb1279a9bccfc5ec85 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 29 Feb 2024 11:49:31 +0000 Subject: [PATCH 01/43] Added happy and sad tests for resolving a requested package and version --- .gitignore | 1 + composer.json | 4 +- composer.lock | 2513 +++++++++++++++------ test/unit/Command/DownloadCommandTest.php | 43 +- 4 files changed, 1840 insertions(+), 721 deletions(-) diff --git a/.gitignore b/.gitignore index 1d5a365..21c8f73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /vendor/ /psalm.xml /.phpunit.cache/ +/.phpunit.result.cache /.phpcs-cache diff --git a/composer.json b/composer.json index 9603cee..36abffa 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,9 @@ ], "require": { "php": "8.1.*||8.2.*||8.3.*", - "symfony/console": "^6.4" + "composer/composer": "^2.7", + "symfony/console": "^6.4", + "webmozart/assert": "^1.11" }, "require-dev": { "doctrine/coding-standard": "^12.0", diff --git a/composer.lock b/composer.lock index 3d9e038..93540cd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,34 +4,42 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "84a9164241781879ce2a3156ecb3fd35", + "content-hash": "3d5d527866522e29c6ec8f315db493fd", "packages": [ { - "name": "psr/container", - "version": "2.0.2", + "name": "composer/ca-bundle", + "version": "1.4.1", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/composer/ca-bundle.git", + "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", "shasum": "" }, "require": { - "php": ">=7.4.0" + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Composer\\CaBundle\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -40,77 +48,77 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.4.1" }, - "time": "2021-11-05T16:47:00+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-02-23T10:16:52+00:00" }, { - "name": "symfony/console", - "version": "v6.4.4", + "name": "composer/class-map-generator", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78" + "url": "https://github.com/composer/class-map-generator.git", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0d9e4eb5ad413075624378f474c4167ea202de78", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" - }, - "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "phpstan/phpstan": "^1.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/filesystem": "^5.4 || ^6", + "symfony/phpunit-bridge": "^5" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, "autoload": { "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Composer\\ClassMapGenerator\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -118,72 +126,103 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", + "description": "Utilities to scan PHP code and generate class maps.", "keywords": [ - "cli", - "command-line", - "console", - "terminal" + "classmap" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.4" + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.1.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2024-02-22T20:27:10+00:00" + "time": "2023-06-30T13:58:57+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v3.4.0", + "name": "composer/composer", + "version": "2.7.1", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + "url": "https://github.com/composer/composer.git", + "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", - "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", + "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", "shasum": "" }, "require": { - "php": ">=8.1" + "composer/ca-bundle": "^1.0", + "composer/class-map-generator": "^1.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.1 || ^3.1", + "composer/semver": "^3.2.5", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "justinrainbow/json-schema": "^5.2.11", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^2.8 || ^3", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.11 || ^6.0.11 || ^7", + "symfony/filesystem": "^5.4 || ^6.0 || ^7", + "symfony/finder": "^5.4 || ^6.0 || ^7", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/process": "^5.4 || ^6.0 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.9.3", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1", + "phpstan/phpstan-symfony": "^1.2.10", + "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" }, + "bin": [ + "bin/composer" + ], "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "2.7-dev" }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] } }, "autoload": { - "files": [ - "function.php" - ] + "psr-4": { + "Composer\\": "src/Composer/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -191,71 +230,76 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" } ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", + "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.7.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2023-05-23T14:45:45+00:00" + "time": "2024-02-09T14:26:28+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "name": "composer/metadata-minifier", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", "shasum": "" }, "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" + "php": "^5.3.2 || ^7.0 || ^8.0" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "branch-alias": { + "dev-main": "1.x-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" + "Composer\\MetadataMinifier\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -264,74 +308,67 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", + "description": "Small utility library that handles metadata minification and expansion.", "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" + "composer", + "compression" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2021-04-07T13:37:33+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "name": "composer/pcre", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "url": "https://github.com/composer/pcre.git", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^7.4 || ^8.0" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + "Composer\\Pcre\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -340,80 +377,69 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" + "PCRE", + "preg", + "regex", + "regular expression" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2023-10-11T07:11:09+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "name": "composer/semver", + "version": "3.4.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^5.3.2 || ^7.0 || ^8.0" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "branch-alias": { + "dev-main": "3.x-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Composer\\Semver\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -421,79 +447,79 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", + "description": "Semver library that offers utilities, version constraint parsing and validation.", "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" + "semantic", + "semver", + "validation", + "versioning" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2023-08-31T09:50:34+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "name": "composer/spdx-licenses", + "version": "1.5.8", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", "shasum": "" }, "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" + "php": "^5.3.2 || ^7.0 || ^8.0" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "branch-alias": { + "dev-main": "1.x-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Composer\\Spdx\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -502,80 +528,77 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" } ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", + "description": "SPDX licenses list and validation library.", "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" + "license", + "spdx", + "validator" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2023-11-20T07:44:33+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.4.1", + "name": "composer/xdebug-handler", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, - "conflict": { - "ext-psr": "<1.1|>=2" + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^6.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.4-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, "autoload": { "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + "Composer\\XdebugHandler\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -583,85 +606,71 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" } ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", + "description": "Restarts a process without Xdebug.", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "Xdebug", + "performance" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/fabpot", + "url": "https://github.com/composer", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/composer/composer", "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { - "name": "symfony/string", - "version": "v6.4.4", + "name": "justinrainbow/json-schema", + "version": "v5.2.13", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", - "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" + "php": ">=5.3.3" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" }, + "bin": [ + "bin/validate-json" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "JsonSchema\\": "src/JsonSchema/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -669,84 +678,60 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" + "json", + "schema" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.4" + "issues": "https://github.com/justinrainbow/json-schema/issues", + "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-02-01T13:16:41+00:00" - } - ], - "packages-dev": [ + "time": "2023-09-26T02:20:38+00:00" + }, { - "name": "amphp/amp", - "version": "v2.6.2", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/amphp/amp.git", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=7.1" - }, - "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1", - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^7 | ^8 | ^9", - "psalm/phar": "^3.11@dev", - "react/promise": "^2" + "php": ">=7.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "files": [ - "lib/functions.php", - "lib/Internal/functions.php" - ], "psr-4": { - "Amp\\": "lib" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -755,86 +740,103 @@ ], "authors": [ { - "name": "Daniel Lowrey", - "email": "rdlowrey@php.net" - }, - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Bob Weinand", - "email": "bobwei9@hotmail.com" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A non-blocking concurrency framework for PHP applications.", - "homepage": "https://amphp.org/amp", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "async", - "asynchronous", - "awaitable", - "concurrency", - "event", - "event-loop", - "future", - "non-blocking", - "promise" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.2" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "funding": [ + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://github.com/amphp", - "type": "github" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "time": "2022-02-20T17:52:18+00:00" + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" }, { - "name": "amphp/byte-stream", - "version": "v1.8.1", + "name": "react/promise", + "version": "v3.1.0", "source": { "type": "git", - "url": "https://github.com/amphp/byte-stream.git", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + "url": "https://github.com/reactphp/promise.git", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", "shasum": "" }, "require": { - "amphp/amp": "^2", - "php": ">=7.1" + "php": ">=7.1.0" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "amphp/phpunit-util": "^1.4", - "friendsofphp/php-cs-fixer": "^2.3", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^6 || ^7 || ^8", - "psalm/phar": "^3.11.4" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { "files": [ - "lib/functions.php" + "src/functions_include.php" ], "psr-4": { - "Amp\\ByteStream\\": "lib" + "React\\Promise\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -843,68 +845,71 @@ ], "authors": [ { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" }, { - "name": "Niklas Keller", - "email": "me@kelunik.com" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "http://amphp.org/byte-stream", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "amp", - "amphp", - "async", - "io", - "non-blocking", - "stream" + "promise", + "promises" ], "support": { - "irc": "irc://irc.freenode.org/amphp", - "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.1.0" }, "funding": [ { - "url": "https://github.com/amphp", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2021-03-30T17:13:30+00:00" + "time": "2023-11-16T16:21:57+00:00" }, { - "name": "composer/pcre", - "version": "3.1.1", + "name": "seld/jsonlint", + "version": "1.10.2", "source": { "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" }, + "bin": [ + "bin/jsonlint" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, "autoload": { "psr-4": { - "Composer\\Pcre\\": "src" + "Seld\\JsonLint\\": "src/Seld/JsonLint/" } }, "notification-url": "https://packagist.org/downloads/", @@ -915,66 +920,58 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "homepage": "https://seld.be" } ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "description": "JSON Linter", "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" + "json", + "linter", + "parser", + "validator" ], "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.1" + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/Seldaek", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", "type": "tidelift" } ], - "time": "2023-10-11T07:11:09+00:00" + "time": "2024-02-07T12:57:50+00:00" }, { - "name": "composer/semver", - "version": "3.4.0", + "name": "seld/phar-utils", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "php": ">=5.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "Composer\\Semver\\": "src" + "Seld\\PharUtils\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -982,78 +979,1336 @@ "MIT" ], "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, { "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" + "email": "j.boggiano@seld.be" } ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", + "description": "PHAR file format utilities, for when PHP phars you up", "keywords": [ - "semantic", - "semver", - "validation", - "versioning" + "phar" ], "support": { - "irc": "ircs://irc.libera.chat:6697/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" + }, + { + "name": "seld/signal-handler", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\Signal\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", + "keywords": [ + "posix", + "sigint", + "signal", + "sigterm", + "unix" + ], + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" + }, + "time": "2023-09-03T09:24:00+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0d9e4eb5ad413075624378f474c4167ea202de78" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0d9e4eb5ad413075624378f474c4167ea202de78", + "reference": "0d9e4eb5ad413075624378f474c4167ea202de78", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-22T20:27:10+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce", + "reference": "11d736e97f116ac375a81f96e662911a34cd50ce", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-10-31T17:30:12+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", + "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "710e27879e9be3395de2b98da3f52a946039f297" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297", + "reference": "710e27879e9be3395de2b98da3f52a946039f297", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-20T12:31:00+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-26T14:02:43+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-01T13:16:41+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v2.6.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "psalm/phar": "^3.11@dev", + "react/promise": "^2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.2" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2022-02-20T17:52:18+00:00" }, { - "name": "composer/xdebug-handler", - "version": "3.0.3", + "name": "amphp/byte-stream", + "version": "v1.8.1", "source": { "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", + "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", "shasum": "" }, "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", - "psr/log": "^1 || ^2 || ^3" + "amphp/amp": "^2", + "php": ">=7.1" }, "require-dev": { - "phpstan/phpstan": "^1.0", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, "autoload": { + "files": [ + "lib/functions.php" + ], "psr-4": { - "Composer\\XdebugHandler\\": "src" + "Amp\\ByteStream\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -1062,35 +2317,36 @@ ], "authors": [ { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Restarts a process without Xdebug.", + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", "keywords": [ - "Xdebug", - "performance" + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" ], "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" }, "funding": [ { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", + "url": "https://github.com/amphp", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2021-03-30T17:13:30+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -2386,56 +3642,6 @@ ], "time": "2024-02-25T14:05:00+00:00" }, - { - "name": "psr/log", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", - "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.0" - }, - "time": "2021-07-14T16:46:02+00:00" - }, { "name": "sebastian/cli-parser", "version": "2.0.0", @@ -3559,69 +4765,6 @@ ], "time": "2024-02-16T15:06:51+00:00" }, - { - "name": "symfony/filesystem", - "version": "v6.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-23T14:51:35+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.2", @@ -3781,64 +4924,6 @@ "source": "https://github.com/vimeo/psalm" }, "time": "2024-02-22T23:39:07+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" - }, - "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], diff --git a/test/unit/Command/DownloadCommandTest.php b/test/unit/Command/DownloadCommandTest.php index 8b68344..088ecc6 100644 --- a/test/unit/Command/DownloadCommandTest.php +++ b/test/unit/Command/DownloadCommandTest.php @@ -8,21 +8,52 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\BufferedOutput; +use Throwable; + +use const PHP_VERSION; +use const PHP_VERSION_ID; #[CoversClass(DownloadCommand::class)] class DownloadCommandTest extends TestCase { public function testDownloadCommand(): void { - $input = $this->createMock(InputInterface::class); - $output = $this->createMock(OutputInterface::class); + $input = $this->createMock(InputInterface::class); + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn('ramsey/uuid'); - $output->expects(self::once()) - ->method('writeln') - ->with('to do'); + $output = new BufferedOutput(); $command = new DownloadCommand(); self::assertSame(0, $command->execute($input, $output)); + + $outputString = $output->fetch(); + self::assertStringContainsString('Found package: ramsey/uuid (version: ', $outputString); + self::assertStringContainsString('Dist download URL: https://api.github.com/repos/ramsey/uuid/zipball/', $outputString); + } + + public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void + { + if (PHP_VERSION_ID >= 80200) { + self::markTestSkipped('This test can only run on older than PHP 8.2 - you are running ' . PHP_VERSION); + } + + $input = $this->createMock(InputInterface::class); + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn('phpunit/phpunit:^11.0'); + + $output = new BufferedOutput(); + + $command = new DownloadCommand(); + + // @todo narrow this down to our true expected failure; + // i.e. phpunit/phpunit:^11.0 should NOT be installable on PHP 8.1 + $this->expectException(Throwable::class); + $command->execute($input, $output); } } From 93071b81b2111370d66b71266c7c84e0dfebd586 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 29 Feb 2024 12:11:05 +0000 Subject: [PATCH 02/43] Added a basic VO to be expanded upon to represent an installable package --- src/DependencyResolver/Package.php | 27 ++++++++++++++++++++ test/unit/DependencyResolver/PackageTest.php | 25 ++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/DependencyResolver/Package.php create mode 100644 test/unit/DependencyResolver/PackageTest.php diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php new file mode 100644 index 0000000..9f338b5 --- /dev/null +++ b/src/DependencyResolver/Package.php @@ -0,0 +1,27 @@ +getPrettyName(), + $completePackage->getPrettyVersion(), + $completePackage->getDistUrl(), + ); + } +} diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php new file mode 100644 index 0000000..b052cff --- /dev/null +++ b/test/unit/DependencyResolver/PackageTest.php @@ -0,0 +1,25 @@ +name); + self::assertSame('1.2.3', $package->version); + self::assertNull($package->downloadUrl); + } +} From 3c771d1727fa0f1b5410cfce87b9520194ea338d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 29 Feb 2024 12:17:31 +0000 Subject: [PATCH 03/43] Added DependencyResolver interface to be used by DownloadCommand --- bin/pie | 3 +- src/Command/DownloadCommand.php | 69 ++++++++++++++++++- src/DependencyResolver/DependencyResolver.php | 11 +++ test/unit/Command/DownloadCommandTest.php | 33 +++++---- 4 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 src/DependencyResolver/DependencyResolver.php diff --git a/bin/pie b/bin/pie index 6d3eb3e..4f8a3e2 100755 --- a/bin/pie +++ b/bin/pie @@ -11,6 +11,7 @@ include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; $application = new Application('pie', 'dev-main'); $application->addCommands([ - new Command\DownloadCommand(), + // @todo we may want to use some kind of service locator eventually + new Command\DownloadCommand(new \Php\Pie\DependencyResolver\ResolveDependencyWithComposer()), ]); $application->run(); diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 97876df..c3744dd 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -4,10 +4,23 @@ namespace Php\Pie\Command; +use Composer\Package\Version\VersionParser; +use InvalidArgumentException; +use Php\Pie\DependencyResolver\DependencyResolver; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Webmozart\Assert\Assert; + +use function array_key_exists; +use function is_array; +use function is_string; +use function reset; +use function sprintf; + +use const PHP_VERSION; #[AsCommand( name: 'download', @@ -15,10 +28,64 @@ )] final class DownloadCommand extends Command { + private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; + + public function __construct(private readonly DependencyResolver $dependencyResolver) + { + parent::__construct(); + } + + public function configure(): void + { + parent::configure(); + + $this->addArgument( + self::ARG_REQUESTED_PACKAGE_AND_VERSION, + InputArgument::REQUIRED, + 'The extension name and version constraint to use, in the format {ext-name}{?:version-constraint}{?@dev-branch-name}, for example `ext-debug:^1.0`', + ); + } + public function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('to do'); // @todo + $requestedNameAndVersionPair = $this->requestedNameAndVersionPair($input); + + $package = ($this->dependencyResolver)( + $requestedNameAndVersionPair['name'], + $requestedNameAndVersionPair['version'], + ); + + $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); + $output->writeln(sprintf('Found package: %s (version: %s)', $package->name, $package->version)); + $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); return Command::SUCCESS; } + + /** @return array{name: non-empty-string, version: non-empty-string|null} */ + private function requestedNameAndVersionPair(InputInterface $input): array + { + $requestedPackageString = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); + + if (! is_string($requestedPackageString) || $requestedPackageString === '') { + throw new InvalidArgumentException('No package was requested for installation'); + } + + $nameAndVersionPairs = (new VersionParser()) + ->parseNameVersionPairs([$requestedPackageString]); + $requestedNameAndVersionPair = reset($nameAndVersionPairs); + + if (! is_array($requestedNameAndVersionPair)) { + throw new InvalidArgumentException('Failed to parse the name/version pair'); + } + + if (! array_key_exists('version', $requestedNameAndVersionPair)) { + $requestedNameAndVersionPair['version'] = null; + } + + Assert::stringNotEmpty($requestedNameAndVersionPair['name']); + Assert::nullOrStringNotEmpty($requestedNameAndVersionPair['version']); + + return $requestedNameAndVersionPair; + } } diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php new file mode 100644 index 0000000..4dacb6d --- /dev/null +++ b/src/DependencyResolver/DependencyResolver.php @@ -0,0 +1,11 @@ +input = $this->createMock(InputInterface::class); + $this->output = new BufferedOutput(); + + $this->command = new DownloadCommand(new ResolveDependencyWithComposer()); + } + public function testDownloadCommand(): void { - $input = $this->createMock(InputInterface::class); - $input->expects(self::once()) + $this->input->expects(self::once()) ->method('getArgument') ->with('requested-package-and-version') ->willReturn('ramsey/uuid'); - $output = new BufferedOutput(); - - $command = new DownloadCommand(); - self::assertSame(0, $command->execute($input, $output)); + self::assertSame(0, $this->command->execute($this->input, $this->output)); - $outputString = $output->fetch(); + $outputString = $this->output->fetch(); self::assertStringContainsString('Found package: ramsey/uuid (version: ', $outputString); self::assertStringContainsString('Dist download URL: https://api.github.com/repos/ramsey/uuid/zipball/', $outputString); } @@ -41,19 +51,14 @@ public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void self::markTestSkipped('This test can only run on older than PHP 8.2 - you are running ' . PHP_VERSION); } - $input = $this->createMock(InputInterface::class); - $input->expects(self::once()) + $this->input->expects(self::once()) ->method('getArgument') ->with('requested-package-and-version') ->willReturn('phpunit/phpunit:^11.0'); - $output = new BufferedOutput(); - - $command = new DownloadCommand(); - // @todo narrow this down to our true expected failure; // i.e. phpunit/phpunit:^11.0 should NOT be installable on PHP 8.1 $this->expectException(Throwable::class); - $command->execute($input, $output); + $this->command->execute($this->input, $this->output); } } From 723fe5b39e617f6b4b10ab803b4bb54bcc696ecd Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 1 Mar 2024 21:07:03 +0000 Subject: [PATCH 04/43] Use Composer to resolve dependencies --- bin/pie | 6 +- src/DependencyResolver/DependencyResolver.php | 1 + .../ResolveDependencyWithComposer.php | 46 +++++++++++++ .../UnableToResolveRequirement.php | 21 ++++++ test/unit/Command/DownloadCommandTest.php | 2 +- .../ResolveDependencyWithComposerTest.php | 64 +++++++++++++++++++ 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/DependencyResolver/ResolveDependencyWithComposer.php create mode 100644 src/DependencyResolver/UnableToResolveRequirement.php create mode 100644 test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php diff --git a/bin/pie b/bin/pie index 4f8a3e2..776adcb 100755 --- a/bin/pie +++ b/bin/pie @@ -3,7 +3,9 @@ declare(strict_types=1); -use Php\Pie\Command; +namespace Php\Pie; + +use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Symfony\Component\Console\Application; /** @psalm-suppress UnresolvableInclude */ @@ -12,6 +14,6 @@ include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; $application = new Application('pie', 'dev-main'); $application->addCommands([ // @todo we may want to use some kind of service locator eventually - new Command\DownloadCommand(new \Php\Pie\DependencyResolver\ResolveDependencyWithComposer()), + new Command\DownloadCommand(ResolveDependencyWithComposer::factory()), ]); $application->run(); diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index 4dacb6d..908888a 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -7,5 +7,6 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DependencyResolver { + /** @throws UnableToResolveRequirement */ public function __invoke(string $packageName, string|null $requestedVersion): Package; } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php new file mode 100644 index 0000000..4e5b5ed --- /dev/null +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -0,0 +1,46 @@ +addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + + return new self( + new PlatformRepository(), + $repositorySet, + ); + } + + public function __invoke(string $packageName, string|null $requestedVersion): Package + { + $package = (new VersionSelector($this->repositorySet, $this->platformRepository)) + ->findBestCandidate($packageName, $requestedVersion); + + if (! $package instanceof CompletePackageInterface) { + throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); + } + + return Package::fromComposerCompletePackage($package); + } +} diff --git a/src/DependencyResolver/UnableToResolveRequirement.php b/src/DependencyResolver/UnableToResolveRequirement.php new file mode 100644 index 0000000..b284948 --- /dev/null +++ b/src/DependencyResolver/UnableToResolveRequirement.php @@ -0,0 +1,21 @@ +input = $this->createMock(InputInterface::class); $this->output = new BufferedOutput(); - $this->command = new DownloadCommand(new ResolveDependencyWithComposer()); + $this->command = new DownloadCommand(ResolveDependencyWithComposer::factory()); } public function testDownloadCommand(): void diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php new file mode 100644 index 0000000..584f9af --- /dev/null +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -0,0 +1,64 @@ +repositorySet = new RepositorySet(); + $this->repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + } + + public function testPackageThatCanBeResolved(): void + { + $package = (new ResolveDependencyWithComposer( + new PlatformRepository([], ['php' => '8.2.0']), + $this->repositorySet, + ))('phpunit/phpunit', '^11.0'); + + self::assertSame('phpunit/phpunit', $package->name); + } + + /** @return list, 1: string, 2: string}> */ + public static function unresolvableDependencies(): array + { + return [ + 'phpVersionTooOld' => [['php' => '8.1.0'], 'phpunit/phpunit', '^11.0'], + 'phpVersionTooNew' => [['php' => '8.3.0'], 'roave/signature', '1.4.*'], + ]; + } + + /** @param array $platformOverrides */ + #[DataProvider('unresolvableDependencies')] + public function testPackageThatCannotBeResolvedThrowsException(array $platformOverrides, string $package, string $version): void + { + $this->expectException(UnableToResolveRequirement::class); + + (new ResolveDependencyWithComposer( + new PlatformRepository([], $platformOverrides), + $this->repositorySet, + ))( + $package, + $version, + ); + } +} From a5e7c5c9d4a86fbe804eb8298b818d632c0cd20b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 1 Mar 2024 21:23:47 +0000 Subject: [PATCH 05/43] Updated DownloadCommand to be an integration test --- composer.json | 1 + phpcs.xml.dist | 1 + phpunit.xml.dist | 3 + .../Command/DownloadCommandTest.php | 49 ++++++++++++++ test/unit/Command/DownloadCommandTest.php | 64 ------------------- 5 files changed, 54 insertions(+), 64 deletions(-) create mode 100644 test/integration/Command/DownloadCommandTest.php delete mode 100644 test/unit/Command/DownloadCommandTest.php diff --git a/composer.json b/composer.json index 36abffa..127006e 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ }, "autoload-dev": { "psr-4": { + "Php\\PieIntegrationTest\\": "test/integration/", "Php\\PieUnitTest\\": "test/unit/" } }, diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 3ee6c03..b9e5857 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -9,6 +9,7 @@ bin/pie src test/unit + test/integration diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 675d305..a6f2dc9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,9 @@ test/unit + + test/integration + diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php new file mode 100644 index 0000000..b23dcd8 --- /dev/null +++ b/test/integration/Command/DownloadCommandTest.php @@ -0,0 +1,49 @@ +commandTester = new CommandTester( + new DownloadCommand(ResolveDependencyWithComposer::factory()), + ); + } + + public function testDownloadCommand(): void + { + $this->commandTester->execute(['requested-package-and-version' => 'ramsey/uuid']); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Found package: ramsey/uuid (version: ', $outputString); + self::assertStringContainsString('Dist download URL: https://api.github.com/repos/ramsey/uuid/zipball/', $outputString); + } + + public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void + { + if (PHP_VERSION_ID >= 80200) { + self::markTestSkipped('This test can only run on older than PHP 8.2 - you are running ' . PHP_VERSION); + } + + $this->expectException(UnableToResolveRequirement::class); + $this->commandTester->execute(['requested-package-and-version' => 'phpunit/phpunit:^11.0']); + } +} diff --git a/test/unit/Command/DownloadCommandTest.php b/test/unit/Command/DownloadCommandTest.php deleted file mode 100644 index 7f6a962..0000000 --- a/test/unit/Command/DownloadCommandTest.php +++ /dev/null @@ -1,64 +0,0 @@ -input = $this->createMock(InputInterface::class); - $this->output = new BufferedOutput(); - - $this->command = new DownloadCommand(ResolveDependencyWithComposer::factory()); - } - - public function testDownloadCommand(): void - { - $this->input->expects(self::once()) - ->method('getArgument') - ->with('requested-package-and-version') - ->willReturn('ramsey/uuid'); - - self::assertSame(0, $this->command->execute($this->input, $this->output)); - - $outputString = $this->output->fetch(); - self::assertStringContainsString('Found package: ramsey/uuid (version: ', $outputString); - self::assertStringContainsString('Dist download URL: https://api.github.com/repos/ramsey/uuid/zipball/', $outputString); - } - - public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void - { - if (PHP_VERSION_ID >= 80200) { - self::markTestSkipped('This test can only run on older than PHP 8.2 - you are running ' . PHP_VERSION); - } - - $this->input->expects(self::once()) - ->method('getArgument') - ->with('requested-package-and-version') - ->willReturn('phpunit/phpunit:^11.0'); - - // @todo narrow this down to our true expected failure; - // i.e. phpunit/phpunit:^11.0 should NOT be installable on PHP 8.1 - $this->expectException(Throwable::class); - $this->command->execute($this->input, $this->output); - } -} From 18f6778f0caf0ab0c07adbb558a33574cf50cfae Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 1 Mar 2024 21:31:12 +0000 Subject: [PATCH 06/43] Enable Psalm testing on test suite too --- composer.json | 1 + composer.lock | 135 +++++++++++++++++- psalm.xml.dist | 11 +- .../ResolveDependencyWithComposerTest.php | 8 +- 4 files changed, 149 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 127006e..8e457e6 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "require-dev": { "doctrine/coding-standard": "^12.0", "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.4", "vimeo/psalm": "^5.22" }, "config": { diff --git a/composer.lock b/composer.lock index 93540cd..8a2419d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d5d527866522e29c6ec8f315db493fd", + "content-hash": "cafeea4526f45230a3b676f13f2f0c0c", "packages": [ { "name": "composer/ca-bundle", @@ -2348,6 +2348,79 @@ ], "time": "2021-03-30T17:13:30+00:00" }, + { + "name": "composer/package-versions-deprecated", + "version": "1.11.99.5", + "source": { + "type": "git", + "url": "https://github.com/composer/package-versions-deprecated.git", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0", + "php": "^7 || ^8" + }, + "replace": { + "ocramius/package-versions": "1.11.99" + }, + "require-dev": { + "composer/composer": "^1.9.3 || ^2.0@dev", + "ext-zip": "^1.13", + "phpunit/phpunit": "^6.5 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2022-01-17T14:14:24+00:00" + }, { "name": "dealerdirect/phpcodesniffer-composer-installer", "version": "v1.0.0", @@ -3642,6 +3715,66 @@ ], "time": "2024-02-25T14:05:00+00:00" }, + { + "name": "psalm/plugin-phpunit", + "version": "0.18.4", + "source": { + "type": "git", + "url": "https://github.com/psalm/psalm-plugin-phpunit.git", + "reference": "e4ab3096653d9eb6f6d0ea5f4461898d59ae4dbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/psalm/psalm-plugin-phpunit/zipball/e4ab3096653d9eb6f6d0ea5f4461898d59ae4dbc", + "reference": "e4ab3096653d9eb6f6d0ea5f4461898d59ae4dbc", + "shasum": "" + }, + "require": { + "composer/package-versions-deprecated": "^1.10", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0", + "vimeo/psalm": "dev-master || dev-4.x || ^4.7.1 || ^5@beta || ^5.0" + }, + "conflict": { + "phpunit/phpunit": "<7.5" + }, + "require-dev": { + "codeception/codeception": "^4.0.3", + "php": "^7.3 || ^8.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.3.1", + "weirdan/codeception-psalm-module": "^0.11.0", + "weirdan/prophecy-shim": "^1.0 || ^2.0" + }, + "type": "psalm-plugin", + "extra": { + "psalm": { + "pluginClass": "Psalm\\PhpUnitPlugin\\Plugin" + } + }, + "autoload": { + "psr-4": { + "Psalm\\PhpUnitPlugin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Brown", + "email": "github@muglug.com" + } + ], + "description": "Psalm plugin for PHPUnit", + "support": { + "issues": "https://github.com/psalm/psalm-plugin-phpunit/issues", + "source": "https://github.com/psalm/psalm-plugin-phpunit/tree/0.18.4" + }, + "time": "2022-12-03T07:47:07+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.0", diff --git a/psalm.xml.dist b/psalm.xml.dist index aa2ce96..d78d0a9 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -9,10 +9,15 @@ findUnusedCode="true" > - - + + + + - + + + + diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 584f9af..1aca5a1 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -38,7 +38,11 @@ public function testPackageThatCanBeResolved(): void self::assertSame('phpunit/phpunit', $package->name); } - /** @return list, 1: string, 2: string}> */ + /** + * @return array, 1: string, 2: string}> + * + * @psalm-suppress PossiblyUnusedMethod + */ public static function unresolvableDependencies(): array { return [ @@ -47,7 +51,7 @@ public static function unresolvableDependencies(): array ]; } - /** @param array $platformOverrides */ + /** @param array $platformOverrides */ #[DataProvider('unresolvableDependencies')] public function testPackageThatCannotBeResolvedThrowsException(array $platformOverrides, string $package, string $version): void { From 2c35b2c97336c03adf0f0c48c06088711cf8b716 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 8 Mar 2024 07:50:33 +0000 Subject: [PATCH 07/43] Basic implementation of downloading and extracting the source archive for Unixy platforms --- bin/pie | 8 +- composer.json | 4 + composer.lock | 534 +++++++++++++++++- src/Command/DownloadCommand.php | 18 +- src/Downloading/DownloadAndExtract.php | 12 + src/Downloading/DownloadZip.php | 69 +++ src/Downloading/DownloadedPackage.php | 22 + src/Downloading/ExtractZip.php | 46 ++ src/Downloading/UnixDownloadAndExtract.php | 54 ++ .../Command/DownloadCommandTest.php | 6 +- 10 files changed, 766 insertions(+), 7 deletions(-) create mode 100644 src/Downloading/DownloadAndExtract.php create mode 100644 src/Downloading/DownloadZip.php create mode 100644 src/Downloading/DownloadedPackage.php create mode 100644 src/Downloading/ExtractZip.php create mode 100644 src/Downloading/UnixDownloadAndExtract.php diff --git a/bin/pie b/bin/pie index 776adcb..b68dbb1 100755 --- a/bin/pie +++ b/bin/pie @@ -6,14 +6,20 @@ declare(strict_types=1); namespace Php\Pie; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; +use Php\Pie\Downloading\UnixDownloadAndExtract; use Symfony\Component\Console\Application; /** @psalm-suppress UnresolvableInclude */ include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; $application = new Application('pie', 'dev-main'); +// @todo make these lazy eventually https://symfony.com/doc/current/console/lazy_commands.html $application->addCommands([ // @todo we may want to use some kind of service locator eventually - new Command\DownloadCommand(ResolveDependencyWithComposer::factory()), + new Command\DownloadCommand( + ResolveDependencyWithComposer::factory(), + // @todo detect platform + UnixDownloadAndExtract::factory(), + ), ]); $application->run(); diff --git a/composer.json b/composer.json index 8e457e6..978cc53 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,11 @@ ], "require": { "php": "8.1.*||8.2.*||8.3.*", + "ext-zip": "*", "composer/composer": "^2.7", + "guzzlehttp/guzzle": "^7.8", + "guzzlehttp/psr7": "^2.6", + "psr/http-message": "^2.0", "symfony/console": "^6.4", "webmozart/assert": "^1.11" }, diff --git a/composer.lock b/composer.lock index 8a2419d..6d966f0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cafeea4526f45230a3b676f13f2f0c0c", + "content-hash": "37d41a51728ec587b1d61db4f11eb903", "packages": [ { "name": "composer/ca-bundle", @@ -636,6 +636,331 @@ ], "time": "2022-02-25T21:32:43+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, { "name": "justinrainbow/json-schema", "version": "v5.2.13", @@ -759,6 +1084,166 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, { "name": "psr/log", "version": "3.0.0", @@ -809,6 +1294,50 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, { "name": "react/promise", "version": "v3.1.0", @@ -5065,7 +5594,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "8.1.*||8.2.*||8.3.*" + "php": "8.1.*||8.2.*||8.3.*", + "ext-zip": "*" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index c3744dd..5f8cef2 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -7,6 +7,7 @@ use Composer\Package\Version\VersionParser; use InvalidArgumentException; use Php\Pie\DependencyResolver\DependencyResolver; +use Php\Pie\Downloading\DownloadAndExtract; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -30,8 +31,10 @@ final class DownloadCommand extends Command { private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; - public function __construct(private readonly DependencyResolver $dependencyResolver) - { + public function __construct( + private readonly DependencyResolver $dependencyResolver, + private readonly DownloadAndExtract $downloadAndExtract, + ) { parent::__construct(); } @@ -57,7 +60,16 @@ public function execute(InputInterface $input, OutputInterface $output): int $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); $output->writeln(sprintf('Found package: %s (version: %s)', $package->name, $package->version)); - $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); + $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); + + $downloadedPackage = ($this->downloadAndExtract)($package); + + $output->writeln(sprintf( + 'Extracted %s:%s source to: %s', + $downloadedPackage->package->name, + $downloadedPackage->package->version, + $downloadedPackage->extractedSourcePath, + )); return Command::SUCCESS; } diff --git a/src/Downloading/DownloadAndExtract.php b/src/Downloading/DownloadAndExtract.php new file mode 100644 index 0000000..b210221 --- /dev/null +++ b/src/Downloading/DownloadAndExtract.php @@ -0,0 +1,12 @@ +downloadUrl === null) { + throw new RuntimeException('Could not download a package without a download URL'); + } + + $request = new Request('GET', $package->downloadUrl); + + $authHeaders = $this->authHelper->addAuthenticationHeader([], 'github.com', $package->downloadUrl); + array_walk( + $authHeaders, + static function (string $v) use (&$request): void { + // @todo probably process this better + $headerParts = explode(':', $v); + $request = $request->withHeader(trim($headerParts[0]), trim($headerParts[1])); + }, + ); + + assert($request instanceof RequestInterface); + $response = $this->client + ->sendAsync( + $request, + [ + RequestOptions::ALLOW_REDIRECTS => true, + RequestOptions::HTTP_ERRORS => false, + RequestOptions::SYNCHRONOUS => true, + ], + ) + ->wait(); + assert($response instanceof ResponseInterface); + + // @todo check response was successful + + // @todo handle this writing better + $tmpZipFile = $localPath . '/downloaded.zip'; + file_put_contents($tmpZipFile, $response->getBody()->__toString()); + + return $tmpZipFile; + } +} diff --git a/src/Downloading/DownloadedPackage.php b/src/Downloading/DownloadedPackage.php new file mode 100644 index 0000000..ce6dab6 --- /dev/null +++ b/src/Downloading/DownloadedPackage.php @@ -0,0 +1,22 @@ +performExtractionUsingZipArchive($zipFile, $destination); + } + + private function performExtractionUsingZipArchive(string $zipFile, string $destination): string + { + $zip = new ZipArchive(); + + $openError = $zip->open($zipFile); + if ($openError !== true) { + throw new RuntimeException(sprintf( + 'Could not open ZIP [%s]: %s', + $openError === false ? '(false)' : $openError, + $zipFile, + )); + } + + if (! $zip->extractTo($destination)) { + throw new RuntimeException(sprintf('Could not extract ZIP "%s" to path: %s', $zipFile, $destination)); + } + + // @todo maybe improve this; GH wraps archives in a top level directory based on the repo name + // and commit, but does anyone else? :s + $extractedPath = explode('/', $zip->getNameIndex(0))[0]; + + $zip->close(); + + return $destination . '/' . $extractedPath; + } +} diff --git a/src/Downloading/UnixDownloadAndExtract.php b/src/Downloading/UnixDownloadAndExtract.php new file mode 100644 index 0000000..7f1920f --- /dev/null +++ b/src/Downloading/UnixDownloadAndExtract.php @@ -0,0 +1,54 @@ +loadConfiguration($config); + + return new self( + new DownloadZip( + new Client(), + new AuthHelper($io, $config), + ), + new ExtractZip(), + ); + } + + public function __invoke(Package $package): DownloadedPackage + { + $localTempPath = sys_get_temp_dir() . '/' . uniqid('pie_downloader_', true); + if (! file_exists($localTempPath)) { + mkdir($localTempPath, recursive: true); + } + + $tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath($package, $localTempPath); + + $extractedPath = $this->extractZip->to($tmpZipFile, $localTempPath); + + return DownloadedPackage::fromPackageAndExtractedPath($package, $extractedPath); + } +} diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index b23dcd8..e50c238 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -7,6 +7,7 @@ use Php\Pie\Command\DownloadCommand; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\DependencyResolver\UnableToResolveRequirement; +use Php\Pie\Downloading\UnixDownloadAndExtract; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -22,7 +23,10 @@ class DownloadCommandTest extends TestCase public function setUp(): void { $this->commandTester = new CommandTester( - new DownloadCommand(ResolveDependencyWithComposer::factory()), + new DownloadCommand( + ResolveDependencyWithComposer::factory(), + UnixDownloadAndExtract::factory(), + ), ); } From 33d65d401724b47780c7f41897dc39178a6bbf3f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 8 Mar 2024 20:45:47 +0000 Subject: [PATCH 08/43] Introduce Illuminate Container for service location --- .php.stub | 19 +++ bin/pie | 17 +- composer.json | 1 + composer.lock | 152 +++++++++++++++++- psalm.xml.dist | 10 ++ src/Container.php | 74 +++++++++ .../ResolveDependencyWithComposer.php | 14 -- src/Downloading/DownloadAndExtract.php | 1 + src/Downloading/DownloadZip.php | 1 + src/Downloading/ExtractZip.php | 1 + src/Downloading/UnixDownloadAndExtract.php | 20 +-- .../Command/DownloadCommandTest.php | 10 +- 12 files changed, 269 insertions(+), 51 deletions(-) create mode 100644 .php.stub create mode 100644 src/Container.php diff --git a/.php.stub b/.php.stub new file mode 100644 index 0000000..be1e3f0 --- /dev/null +++ b/.php.stub @@ -0,0 +1,19 @@ + $name + * @psalm-return ($name is class-string ? T : mixed) + */ + public function get(string $name): object; + } +} diff --git a/bin/pie b/bin/pie index b68dbb1..4928bff 100755 --- a/bin/pie +++ b/bin/pie @@ -5,21 +5,20 @@ declare(strict_types=1); namespace Php\Pie; +use Php\Pie\Command\DownloadCommand; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\Downloading\UnixDownloadAndExtract; use Symfony\Component\Console\Application; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; /** @psalm-suppress UnresolvableInclude */ include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; $application = new Application('pie', 'dev-main'); -// @todo make these lazy eventually https://symfony.com/doc/current/console/lazy_commands.html -$application->addCommands([ - // @todo we may want to use some kind of service locator eventually - new Command\DownloadCommand( - ResolveDependencyWithComposer::factory(), - // @todo detect platform - UnixDownloadAndExtract::factory(), - ), -]); +$application->setCommandLoader(new ContainerCommandLoader( + Container::factory(), + [ + 'download' => DownloadCommand::class, + ] +)); $application->run(); diff --git a/composer.json b/composer.json index 978cc53..8de5973 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "composer/composer": "^2.7", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/psr7": "^2.6", + "illuminate/container": "^10.47", "psr/http-message": "^2.0", "symfony/console": "^6.4", "webmozart/assert": "^1.11" diff --git a/composer.lock b/composer.lock index 6d966f0..b4e1178 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "37d41a51728ec587b1d61db4f11eb903", + "content-hash": "e06f8eec17d3884eb9d1bec39a8991bf", "packages": [ { "name": "composer/ca-bundle", @@ -961,6 +961,105 @@ ], "time": "2023-12-03T20:05:35+00:00" }, + { + "name": "illuminate/container", + "version": "v10.47.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "ddc26273085fad3c471b2602ad820e0097ff7939" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/ddc26273085fad3c471b2602ad820e0097ff7939", + "reference": "ddc26273085fad3c471b2602ad820e0097ff7939", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0", + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-06-18T09:12:03+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v10.47.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac", + "reference": "8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-01-15T18:52:32+00:00" + }, { "name": "justinrainbow/json-schema", "version": "v5.2.13", @@ -1294,6 +1393,57 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", diff --git a/psalm.xml.dist b/psalm.xml.dist index d78d0a9..f8fc9cf 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -20,4 +20,14 @@ + + + + + + + + + + diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..8ce2af0 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,74 @@ +singleton(DownloadCommand::class); + $container->singleton( + DependencyResolver::class, + static function (): DependencyResolver { + $repositorySet = new RepositorySet(); + $repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + + return new ResolveDependencyWithComposer( + new PlatformRepository(), + $repositorySet, + ); + }, + ); + $container->singleton( + UnixDownloadAndExtract::class, + static function (): UnixDownloadAndExtract { + $config = Factory::createConfig(); + $io = new NullIO(); + $io->loadConfiguration($config); + + return new UnixDownloadAndExtract( + new DownloadZip( + new Client(), + new AuthHelper($io, $config), + ), + new ExtractZip(), + ); + }, + ); + $container->singleton( + DownloadAndExtract::class, + static function (ContainerInterface $container): DownloadAndExtract { + if (Platform::isWindows()) { + // @todo add windows downloader + throw new RuntimeException('Windows support not yet'); + } + + return $container->get(UnixDownloadAndExtract::class); + }, + ); + + return $container; + } +} diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 4e5b5ed..995b67e 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -4,12 +4,9 @@ namespace Php\Pie\DependencyResolver; -use Composer\IO\NullIO; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; -use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -21,17 +18,6 @@ public function __construct( ) { } - public static function factory(): self - { - $repositorySet = new RepositorySet(); - $repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); - - return new self( - new PlatformRepository(), - $repositorySet, - ); - } - public function __invoke(string $packageName, string|null $requestedVersion): Package { $package = (new VersionSelector($this->repositorySet, $this->platformRepository)) diff --git a/src/Downloading/DownloadAndExtract.php b/src/Downloading/DownloadAndExtract.php index b210221..413a246 100644 --- a/src/Downloading/DownloadAndExtract.php +++ b/src/Downloading/DownloadAndExtract.php @@ -6,6 +6,7 @@ use Php\Pie\DependencyResolver\Package; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DownloadAndExtract { public function __invoke(Package $package): DownloadedPackage; diff --git a/src/Downloading/DownloadZip.php b/src/Downloading/DownloadZip.php index 77eea86..756a35c 100644 --- a/src/Downloading/DownloadZip.php +++ b/src/Downloading/DownloadZip.php @@ -19,6 +19,7 @@ use function file_put_contents; use function trim; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class DownloadZip { public function __construct( diff --git a/src/Downloading/ExtractZip.php b/src/Downloading/ExtractZip.php index bc326c3..e5f0cec 100644 --- a/src/Downloading/ExtractZip.php +++ b/src/Downloading/ExtractZip.php @@ -10,6 +10,7 @@ use function explode; use function sprintf; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class ExtractZip { public function to(string $zipFile, string $destination): string diff --git a/src/Downloading/UnixDownloadAndExtract.php b/src/Downloading/UnixDownloadAndExtract.php index 7f1920f..60d702d 100644 --- a/src/Downloading/UnixDownloadAndExtract.php +++ b/src/Downloading/UnixDownloadAndExtract.php @@ -4,10 +4,6 @@ namespace Php\Pie\Downloading; -use Composer\Factory; -use Composer\IO\NullIO; -use Composer\Util\AuthHelper; -use GuzzleHttp\Client; use Php\Pie\DependencyResolver\Package; use function file_exists; @@ -15,6 +11,7 @@ use function sys_get_temp_dir; use function uniqid; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class UnixDownloadAndExtract implements DownloadAndExtract { public function __construct( @@ -23,21 +20,6 @@ public function __construct( ) { } - public static function factory(): self - { - $config = Factory::createConfig(); - $io = new NullIO(); - $io->loadConfiguration($config); - - return new self( - new DownloadZip( - new Client(), - new AuthHelper($io, $config), - ), - new ExtractZip(), - ); - } - public function __invoke(Package $package): DownloadedPackage { $localTempPath = sys_get_temp_dir() . '/' . uniqid('pie_downloader_', true); diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index e50c238..1261236 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -5,9 +5,8 @@ namespace Php\PieIntegrationTest\Command; use Php\Pie\Command\DownloadCommand; -use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; +use Php\Pie\Container; use Php\Pie\DependencyResolver\UnableToResolveRequirement; -use Php\Pie\Downloading\UnixDownloadAndExtract; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -22,12 +21,7 @@ class DownloadCommandTest extends TestCase public function setUp(): void { - $this->commandTester = new CommandTester( - new DownloadCommand( - ResolveDependencyWithComposer::factory(), - UnixDownloadAndExtract::factory(), - ), - ); + $this->commandTester = new CommandTester(Container::factory()->get(DownloadCommand::class)); } public function testDownloadCommand(): void From 3323693a182ad460d9a028755b38837a2ee52b6a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 8 Mar 2024 21:19:55 +0000 Subject: [PATCH 09/43] Rewire some Composer dependencies and console IO --- .php.stub | 19 ------------------- .phpstorm.meta.php | 9 +++++++++ bin/pie | 10 ++++++---- psalm.xml.dist | 3 --- src/Container.php | 43 +++++++++++++++++++++++++++++++++++-------- 5 files changed, 50 insertions(+), 34 deletions(-) delete mode 100644 .php.stub create mode 100644 .phpstorm.meta.php diff --git a/.php.stub b/.php.stub deleted file mode 100644 index be1e3f0..0000000 --- a/.php.stub +++ /dev/null @@ -1,19 +0,0 @@ - $name - * @psalm-return ($name is class-string ? T : mixed) - */ - public function get(string $name): object; - } -} diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php new file mode 100644 index 0000000..05552cf --- /dev/null +++ b/.phpstorm.meta.php @@ -0,0 +1,9 @@ + '@', + ]) + ); +} diff --git a/bin/pie b/bin/pie index 4928bff..cbff1ac 100755 --- a/bin/pie +++ b/bin/pie @@ -6,19 +6,21 @@ declare(strict_types=1); namespace Php\Pie; use Php\Pie\Command\DownloadCommand; -use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; -use Php\Pie\Downloading\UnixDownloadAndExtract; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; /** @psalm-suppress UnresolvableInclude */ include $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; +$container = Container::factory(); + $application = new Application('pie', 'dev-main'); $application->setCommandLoader(new ContainerCommandLoader( - Container::factory(), + $container, [ 'download' => DownloadCommand::class, ] )); -$application->run(); +$application->run($container->get(InputInterface::class), $container->get(OutputInterface::class)); diff --git a/psalm.xml.dist b/psalm.xml.dist index f8fc9cf..5fd0a8e 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -20,9 +20,6 @@ - - - diff --git a/src/Container.php b/src/Container.php index 8ce2af0..73c6238 100644 --- a/src/Container.php +++ b/src/Container.php @@ -4,7 +4,10 @@ namespace Php\Pie; -use Composer\Factory; +use Composer\Composer; +use Composer\Factory as ComposerFactory; +use Composer\IO\ConsoleIO; +use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; @@ -13,6 +16,7 @@ use Composer\Util\AuthHelper; use Composer\Util\Platform; use GuzzleHttp\Client; +use Illuminate\Container\Container as IlluminateContainer; use Php\Pie\Command\DownloadCommand; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; @@ -22,13 +26,37 @@ use Php\Pie\Downloading\UnixDownloadAndExtract; use Psr\Container\ContainerInterface; use RuntimeException; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; final class Container { public static function factory(): ContainerInterface { - $container = new \Illuminate\Container\Container(); + $container = new IlluminateContainer(); + $container->instance(InputInterface::class, new ArgvInput()); + $container->instance(OutputInterface::class, new ConsoleOutput()); + $container->singleton(DownloadCommand::class); + + $container->singleton(IOInterface::class, static function (ContainerInterface $container): IOInterface { + return new ConsoleIO( + $container->get(InputInterface::class), + $container->get(OutputInterface::class), + new HelperSet([]), + ); + }); + $container->singleton(Composer::class, static function (ContainerInterface $container): Composer { + $io = $container->get(IOInterface::class); + $composer = ComposerFactory::create($io); + $io->loadConfiguration($composer->getConfig()); + + return $composer; + }); + $container->singleton( DependencyResolver::class, static function (): DependencyResolver { @@ -43,15 +71,14 @@ static function (): DependencyResolver { ); $container->singleton( UnixDownloadAndExtract::class, - static function (): UnixDownloadAndExtract { - $config = Factory::createConfig(); - $io = new NullIO(); - $io->loadConfiguration($config); - + static function (ContainerInterface $container): UnixDownloadAndExtract { return new UnixDownloadAndExtract( new DownloadZip( new Client(), - new AuthHelper($io, $config), + new AuthHelper( + $container->get(IOInterface::class), + $container->get(Composer::class)->getConfig(), + ), ), new ExtractZip(), ); From 52d6b65397b940009426f5ea37d964615ac3b5de Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 1 Apr 2024 10:00:17 +0100 Subject: [PATCH 10/43] Added helper to find the PHP binary --- composer.json | 1 + composer.lock | 2 +- src/TargetPhp/PhpBinaryPath.php | 44 +++++++++++++++++++++++ test/unit/TargetPhp/PhpBinaryPathTest.php | 18 ++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/TargetPhp/PhpBinaryPath.php create mode 100644 test/unit/TargetPhp/PhpBinaryPathTest.php diff --git a/composer.json b/composer.json index 8de5973..3542a07 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "illuminate/container": "^10.47", "psr/http-message": "^2.0", "symfony/console": "^6.4", + "symfony/process": "^6.4", "webmozart/assert": "^1.11" }, "require-dev": { diff --git a/composer.lock b/composer.lock index b4e1178..e8d4935 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e06f8eec17d3884eb9d1bec39a8991bf", + "content-hash": "694d5bf1b96fcceae6af19c03eb55095", "packages": [ { "name": "composer/ca-bundle", diff --git a/src/TargetPhp/PhpBinaryPath.php b/src/TargetPhp/PhpBinaryPath.php new file mode 100644 index 0000000..d57aa5a --- /dev/null +++ b/src/TargetPhp/PhpBinaryPath.php @@ -0,0 +1,44 @@ +phpBinaryPath, '-r', 'echo phpversion();'])) + ->mustRun() + ->getOutput()); + Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + return $phpVersion; + } + + public static function fromPhpConfigExecutable(string $phpConfig): self + { + // @todo filter input/sanitize output + $phpExecutable = trim((new Process([$phpConfig, '--php-binary'])) + ->mustRun() + ->getOutput()); + Assert::stringNotEmpty($phpExecutable, 'Could not find path to PHP executable.'); + return new self($phpExecutable); + } + + public static function fromCurrentProcess(): self + { + $phpExecutable = trim((new PhpExecutableFinder())->find()); + Assert::stringNotEmpty($phpExecutable, 'Could not find path to PHP executable.'); + return new self($phpExecutable); + } +} diff --git a/test/unit/TargetPhp/PhpBinaryPathTest.php b/test/unit/TargetPhp/PhpBinaryPathTest.php new file mode 100644 index 0000000..b59fead --- /dev/null +++ b/test/unit/TargetPhp/PhpBinaryPathTest.php @@ -0,0 +1,18 @@ +version()); + } +} From 892c5360d5e9211c3b5b6c585d90f08b129cf95c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 1 Apr 2024 11:40:09 +0100 Subject: [PATCH 11/43] Ensure target PHP platform is used to resolve package version --- composer.json | 2 +- composer.lock | 19 ++++++++------- src/Command/DownloadCommand.php | 22 +++++++++++++++++- src/Container.php | 13 +++++------ src/DependencyResolver/DependencyResolver.php | 4 +++- .../ResolveDependencyWithComposer.php | 14 +++++++---- src/TargetPhp/PhpBinaryPath.php | 9 ++++++-- .../ResolveTargetPhpToPlatformRepository.php | 16 +++++++++++++ .../Command/DownloadCommandTest.php | 14 +++++++---- .../ResolveDependencyWithComposerTest.php | 23 +++++++++++++++---- test/unit/TargetPhp/PhpBinaryPathTest.php | 2 ++ 11 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 src/TargetPhp/ResolveTargetPhpToPlatformRepository.php diff --git a/composer.json b/composer.json index 3542a07..43e1b56 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require": { "php": "8.1.*||8.2.*||8.3.*", "ext-zip": "*", - "composer/composer": "^2.7", + "composer/composer": "dev-main@dev", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/psr7": "^2.6", "illuminate/container": "^10.47", diff --git a/composer.lock b/composer.lock index e8d4935..1189dc7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "694d5bf1b96fcceae6af19c03eb55095", + "content-hash": "fb31ac50e2d4c78585e5dc67fbef9477", "packages": [ { "name": "composer/ca-bundle", @@ -157,16 +157,16 @@ }, { "name": "composer/composer", - "version": "2.7.1", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc" + "reference": "b12a88b7f3313e9dbae5b58085323b8328d10296" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", - "reference": "aaf6ed5ccd27c23f79a545e351b4d7842a99d0bc", + "url": "https://api.github.com/repos/composer/composer/zipball/b12a88b7f3313e9dbae5b58085323b8328d10296", + "reference": "b12a88b7f3313e9dbae5b58085323b8328d10296", "shasum": "" }, "require": { @@ -205,6 +205,7 @@ "ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zlib": "Allow gzip compression of HTTP requests" }, + "default-branch": true, "bin": [ "bin/composer" ], @@ -251,7 +252,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.1" + "source": "https://github.com/composer/composer/tree/main" }, "funding": [ { @@ -267,7 +268,7 @@ "type": "tidelift" } ], - "time": "2024-02-09T14:26:28+00:00" + "time": "2024-03-22T08:29:43+00:00" }, { "name": "composer/metadata-minifier", @@ -5740,7 +5741,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "composer/composer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 5f8cef2..9fe973a 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -8,10 +8,12 @@ use InvalidArgumentException; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\Downloading\DownloadAndExtract; +use Php\Pie\TargetPhp\PhpBinaryPath; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Webmozart\Assert\Assert; @@ -30,6 +32,7 @@ final class DownloadCommand extends Command { private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; + private const OPTION_WITH_PHP_CONFIG = 'with-php-config'; public function __construct( private readonly DependencyResolver $dependencyResolver, @@ -47,18 +50,35 @@ public function configure(): void InputArgument::REQUIRED, 'The extension name and version constraint to use, in the format {ext-name}{?:version-constraint}{?@dev-branch-name}, for example `ext-debug:^1.0`', ); + $this->addOption( + self::OPTION_WITH_PHP_CONFIG, + null, + InputOption::VALUE_OPTIONAL, + 'The path to `php-config` to use', + ); } public function execute(InputInterface $input, OutputInterface $output): int { + $phpBinaryPath = PhpBinaryPath::fromCurrentProcess(); + + /** @var mixed $withPhpConfig */ + $withPhpConfig = $input->getOption(self::OPTION_WITH_PHP_CONFIG); + if (is_string($withPhpConfig) && $withPhpConfig !== '') { + $phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($withPhpConfig); + } + + $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); + $output->writeln(sprintf('Target PHP installation: %s (from %s)', $phpBinaryPath->version(), $phpBinaryPath->phpBinaryPath)); + $requestedNameAndVersionPair = $this->requestedNameAndVersionPair($input); $package = ($this->dependencyResolver)( + $phpBinaryPath, $requestedNameAndVersionPair['name'], $requestedNameAndVersionPair['version'], ); - $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); $output->writeln(sprintf('Found package: %s (version: %s)', $package->name, $package->version)); $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); diff --git a/src/Container.php b/src/Container.php index 73c6238..a84ce4c 100644 --- a/src/Container.php +++ b/src/Container.php @@ -8,10 +8,7 @@ use Composer\Factory as ComposerFactory; use Composer\IO\ConsoleIO; use Composer\IO\IOInterface; -use Composer\IO\NullIO; use Composer\Repository\CompositeRepository; -use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; use Composer\Util\AuthHelper; use Composer\Util\Platform; @@ -24,6 +21,7 @@ use Php\Pie\Downloading\DownloadZip; use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\UnixDownloadAndExtract; +use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; use Psr\Container\ContainerInterface; use RuntimeException; use Symfony\Component\Console\Helper\HelperSet; @@ -51,7 +49,7 @@ public static function factory(): ContainerInterface }); $container->singleton(Composer::class, static function (ContainerInterface $container): Composer { $io = $container->get(IOInterface::class); - $composer = ComposerFactory::create($io); + $composer = (new ComposerFactory())->createComposer($io, [], true); $io->loadConfiguration($composer->getConfig()); return $composer; @@ -59,13 +57,14 @@ public static function factory(): ContainerInterface $container->singleton( DependencyResolver::class, - static function (): DependencyResolver { + static function (ContainerInterface $container): DependencyResolver { + $composer = $container->get(Composer::class); $repositorySet = new RepositorySet(); - $repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + $repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); return new ResolveDependencyWithComposer( - new PlatformRepository(), $repositorySet, + new ResolveTargetPhpToPlatformRepository(), ); }, ); diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index 908888a..57a4316 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -4,9 +4,11 @@ namespace Php\Pie\DependencyResolver; +use Php\Pie\TargetPhp\PhpBinaryPath; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DependencyResolver { /** @throws UnableToResolveRequirement */ - public function __invoke(string $packageName, string|null $requestedVersion): Package; + public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, string|null $requestedVersion): Package; } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 995b67e..f70c186 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -6,23 +6,29 @@ use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; -use Composer\Repository\PlatformRepository; use Composer\Repository\RepositorySet; +use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class ResolveDependencyWithComposer implements DependencyResolver { public function __construct( - private readonly PlatformRepository $platformRepository, private readonly RepositorySet $repositorySet, + private readonly ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository, ) { } - public function __invoke(string $packageName, string|null $requestedVersion): Package + public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, string|null $requestedVersion): Package { - $package = (new VersionSelector($this->repositorySet, $this->platformRepository)) + $package = (new VersionSelector( + $this->repositorySet, + ($this->resolveTargetPhpToPlatformRepository)($phpBinaryPath), + )) ->findBestCandidate($packageName, $requestedVersion); + // @todo check it is a `php-ext` or `php-ext-zend` + if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); } diff --git a/src/TargetPhp/PhpBinaryPath.php b/src/TargetPhp/PhpBinaryPath.php index d57aa5a..9d364c0 100644 --- a/src/TargetPhp/PhpBinaryPath.php +++ b/src/TargetPhp/PhpBinaryPath.php @@ -8,8 +8,10 @@ use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; +use function trim; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ -final class PhpBinaryPath +class PhpBinaryPath { /** @param non-empty-string $phpBinaryPath */ private function __construct(readonly string $phpBinaryPath) @@ -22,6 +24,7 @@ public function version(): string ->mustRun() ->getOutput()); Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + return $phpVersion; } @@ -32,13 +35,15 @@ public static function fromPhpConfigExecutable(string $phpConfig): self ->mustRun() ->getOutput()); Assert::stringNotEmpty($phpExecutable, 'Could not find path to PHP executable.'); + return new self($phpExecutable); } public static function fromCurrentProcess(): self { - $phpExecutable = trim((new PhpExecutableFinder())->find()); + $phpExecutable = trim((string) (new PhpExecutableFinder())->find()); Assert::stringNotEmpty($phpExecutable, 'Could not find path to PHP executable.'); + return new self($phpExecutable); } } diff --git a/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php b/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php new file mode 100644 index 0000000..9e3eaac --- /dev/null +++ b/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php @@ -0,0 +1,16 @@ + $phpBinaryPath->version()]); + } +} diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 1261236..e58aa7e 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -26,13 +26,18 @@ public function setUp(): void public function testDownloadCommand(): void { - $this->commandTester->execute(['requested-package-and-version' => 'ramsey/uuid']); + if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { + self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); + } + + // 1.0.0 is only compatible with PHP 8.3.0 + $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:1.0.0']); $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Found package: ramsey/uuid (version: ', $outputString); - self::assertStringContainsString('Dist download URL: https://api.github.com/repos/ramsey/uuid/zipball/', $outputString); + self::assertStringContainsString('Found package: asgrim/example-pie-extension (version: 1.0.0)', $outputString); + self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString); } public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void @@ -42,6 +47,7 @@ public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void } $this->expectException(UnableToResolveRequirement::class); - $this->commandTester->execute(['requested-package-and-version' => 'phpunit/phpunit:^11.0']); + // 1.0.0 is only compatible with PHP 8.3.0 + $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:1.0.0']); } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 1aca5a1..0e24daa 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -6,11 +6,12 @@ use Composer\IO\NullIO; use Composer\Repository\CompositeRepository; -use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\DependencyResolver\UnableToResolveRequirement; +use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -19,6 +20,7 @@ final class ResolveDependencyWithComposerTest extends TestCase { private RepositorySet $repositorySet; + private ResolveTargetPhpToPlatformRepository $resolveTargetPhpToPlatformRepository; public function setUp(): void { @@ -26,14 +28,21 @@ public function setUp(): void $this->repositorySet = new RepositorySet(); $this->repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultReposWithDefaultManager(new NullIO()))); + + $this->resolveTargetPhpToPlatformRepository = new ResolveTargetPhpToPlatformRepository(); } public function testPackageThatCanBeResolved(): void { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::once()) + ->method('version') + ->willReturn('8.2.0'); + $package = (new ResolveDependencyWithComposer( - new PlatformRepository([], ['php' => '8.2.0']), $this->repositorySet, - ))('phpunit/phpunit', '^11.0'); + $this->resolveTargetPhpToPlatformRepository, + ))($phpBinaryPath, 'phpunit/phpunit', '^11.0'); self::assertSame('phpunit/phpunit', $package->name); } @@ -55,12 +64,18 @@ public static function unresolvableDependencies(): array #[DataProvider('unresolvableDependencies')] public function testPackageThatCannotBeResolvedThrowsException(array $platformOverrides, string $package, string $version): void { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::once()) + ->method('version') + ->willReturn($platformOverrides['php']); + $this->expectException(UnableToResolveRequirement::class); (new ResolveDependencyWithComposer( - new PlatformRepository([], $platformOverrides), $this->repositorySet, + $this->resolveTargetPhpToPlatformRepository, ))( + $phpBinaryPath, $package, $version, ); diff --git a/test/unit/TargetPhp/PhpBinaryPathTest.php b/test/unit/TargetPhp/PhpBinaryPathTest.php index b59fead..05c7551 100644 --- a/test/unit/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/TargetPhp/PhpBinaryPathTest.php @@ -8,6 +8,8 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use const PHP_VERSION; + #[CoversClass(PhpBinaryPath::class)] final class PhpBinaryPathTest extends TestCase { From 2b9009504d72ea6a6ed310014cdb1348d1219dc6 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 1 Apr 2024 12:06:51 +0100 Subject: [PATCH 12/43] Ensure package resolved is a php-ext or php-ext-zend --- src/DependencyResolver/Package.php | 3 +++ .../ResolveDependencyWithComposer.php | 9 +++++++-- .../UnableToResolveRequirement.php | 11 +++++++++++ .../ResolveDependencyWithComposerTest.php | 14 ++++++++------ 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 9f338b5..f3f9314 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -9,6 +9,9 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class Package { + public const TYPE_PHP_MODULE = 'php-ext'; + public const TYPE_ZEND_EXTENSION = 'php-ext-zend'; + private function __construct( public readonly string $name, public readonly string $version, diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index f70c186..4f251ca 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -10,6 +10,8 @@ use Php\Pie\TargetPhp\PhpBinaryPath; use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; +use function in_array; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class ResolveDependencyWithComposer implements DependencyResolver { @@ -27,12 +29,15 @@ public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, stri )) ->findBestCandidate($packageName, $requestedVersion); - // @todo check it is a `php-ext` or `php-ext-zend` - if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); } + $type = $package->getType(); + if (! in_array($type, [Package::TYPE_PHP_MODULE, Package::TYPE_ZEND_EXTENSION])) { + throw UnableToResolveRequirement::toPhpOrZendExtension($package, $packageName, $requestedVersion); + } + return Package::fromComposerCompletePackage($package); } } diff --git a/src/DependencyResolver/UnableToResolveRequirement.php b/src/DependencyResolver/UnableToResolveRequirement.php index b284948..3ab76ad 100644 --- a/src/DependencyResolver/UnableToResolveRequirement.php +++ b/src/DependencyResolver/UnableToResolveRequirement.php @@ -4,6 +4,7 @@ namespace Php\Pie\DependencyResolver; +use Composer\Package\PackageInterface; use RuntimeException; use function sprintf; @@ -18,4 +19,14 @@ public static function fromRequirement(string $requiredPackageName, string|null $requiredVersion !== null ? sprintf(' for version %s.', $requiredVersion) : '.', )); } + + public static function toPhpOrZendExtension(PackageInterface $locatedComposerPackage, string $requiredPackageName, string|null $requiredVersion): self + { + return new self(sprintf( + 'Package %s was not of type php-ext or php-ext-zend (requested %s%s)', + $locatedComposerPackage->getName(), + $requiredPackageName, + $requiredVersion !== null ? sprintf(' for version %s.', $requiredVersion) : '.', + )); + } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 0e24daa..ce163a1 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -35,16 +35,17 @@ public function setUp(): void public function testPackageThatCanBeResolved(): void { $phpBinaryPath = $this->createMock(PhpBinaryPath::class); - $phpBinaryPath->expects(self::once()) + $phpBinaryPath->expects(self::any()) ->method('version') - ->willReturn('8.2.0'); + ->willReturn('8.3.0'); $package = (new ResolveDependencyWithComposer( $this->repositorySet, $this->resolveTargetPhpToPlatformRepository, - ))($phpBinaryPath, 'phpunit/phpunit', '^11.0'); + ))($phpBinaryPath, 'asgrim/example-pie-extension', '1.0.0'); - self::assertSame('phpunit/phpunit', $package->name); + self::assertSame('asgrim/example-pie-extension', $package->name); + self::assertSame('1.0.0', $package->version); } /** @@ -55,8 +56,9 @@ public function testPackageThatCanBeResolved(): void public static function unresolvableDependencies(): array { return [ - 'phpVersionTooOld' => [['php' => '8.1.0'], 'phpunit/phpunit', '^11.0'], - 'phpVersionTooNew' => [['php' => '8.3.0'], 'roave/signature', '1.4.*'], + 'phpVersionTooOld' => [['php' => '8.1.0'], 'asgrim/example-pie-extension', '1.0.0'], + 'phpVersionTooNew' => [['php' => '8.4.0'], 'asgrim/example-pie-extension', '1.0.0'], + 'notAPhpExtension' => [['php' => '8.3.0'], 'ramsey/uuid', '^4.7'], ]; } From f757ee978cf0712a30ad4b9a8b3824b885c33ce8 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 1 Apr 2024 12:36:12 +0100 Subject: [PATCH 13/43] When fetching PHP version, do not include the 'extra' information in the version --- src/TargetPhp/PhpBinaryPath.php | 10 +++++++++- test/unit/TargetPhp/PhpBinaryPathTest.php | 11 +++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/TargetPhp/PhpBinaryPath.php b/src/TargetPhp/PhpBinaryPath.php index 9d364c0..cadb8b2 100644 --- a/src/TargetPhp/PhpBinaryPath.php +++ b/src/TargetPhp/PhpBinaryPath.php @@ -4,6 +4,7 @@ namespace Php\Pie\TargetPhp; +use Composer\Semver\VersionParser; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; @@ -20,11 +21,18 @@ private function __construct(readonly string $phpBinaryPath) public function version(): string { - $phpVersion = trim((new Process([$this->phpBinaryPath, '-r', 'echo phpversion();'])) + $phpVersion = trim((new Process([ + $this->phpBinaryPath, + '-r', + 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "." . PHP_RELEASE_VERSION;', + ])) ->mustRun() ->getOutput()); Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + // normalizing the version will throw an exception if it is not a valid version + (new VersionParser())->normalize($phpVersion); + return $phpVersion; } diff --git a/test/unit/TargetPhp/PhpBinaryPathTest.php b/test/unit/TargetPhp/PhpBinaryPathTest.php index 05c7551..a18bdb5 100644 --- a/test/unit/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/TargetPhp/PhpBinaryPathTest.php @@ -8,13 +8,20 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use const PHP_VERSION; +use function sprintf; + +use const PHP_MAJOR_VERSION; +use const PHP_MINOR_VERSION; +use const PHP_RELEASE_VERSION; #[CoversClass(PhpBinaryPath::class)] final class PhpBinaryPathTest extends TestCase { public function testVersionFromCurrentProcess(): void { - self::assertSame(PHP_VERSION, PhpBinaryPath::fromCurrentProcess()->version()); + self::assertSame( + sprintf('%s.%s.%s', PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION), + PhpBinaryPath::fromCurrentProcess()->version(), + ); } } From c7fb06cdcd7bfc8a28019a5274584fb617388289 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 12 Apr 2024 12:45:55 +0100 Subject: [PATCH 14/43] Added Windows downloading support --- composer.json | 2 + composer.lock | 543 ++++++++++++------ psalm.xml.dist | 3 +- src/Container.php | 39 +- .../ResolveDependencyWithComposer.php | 2 +- src/Downloading/AddAuthenticationHeader.php | 37 ++ src/Downloading/DownloadZip.php | 27 +- src/Downloading/UnixDownloadAndExtract.php | 23 +- src/Downloading/WindowsDownloadAndExtract.php | 141 +++++ .../Command/DownloadCommandTest.php | 1 + 10 files changed, 617 insertions(+), 201 deletions(-) create mode 100644 src/Downloading/AddAuthenticationHeader.php create mode 100644 src/Downloading/WindowsDownloadAndExtract.php diff --git a/composer.json b/composer.json index 43e1b56..7f50029 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "require": { "php": "8.1.*||8.2.*||8.3.*", "ext-zip": "*", + "azjezz/psl": "^2.9", "composer/composer": "dev-main@dev", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/psr7": "^2.6", @@ -39,6 +40,7 @@ }, "require-dev": { "doctrine/coding-standard": "^12.0", + "php-standard-library/psalm-plugin": "^2.3", "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.4", "vimeo/psalm": "^5.22" diff --git a/composer.lock b/composer.lock index 1189dc7..952be8e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,32 +4,106 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fb31ac50e2d4c78585e5dc67fbef9477", + "content-hash": "527f1f5b3881b9c5f1def2bc223a385d", "packages": [ + { + "name": "azjezz/psl", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/azjezz/psl.git", + "reference": "1ade4f1a99fe07a8e06f8dee596609aa07585422" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/azjezz/psl/zipball/1ade4f1a99fe07a8e06f8dee596609aa07585422", + "reference": "1ade4f1a99fe07a8e06f8dee596609aa07585422", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "revolt/event-loop": "^1.0.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.22.0", + "php-coveralls/php-coveralls": "^2.6.0", + "php-standard-library/psalm-plugin": "^2.2.1", + "phpbench/phpbench": "^1.2.14", + "phpunit/phpunit": "^9.6.10", + "roave/infection-static-analysis-plugin": "^1.32.0", + "squizlabs/php_codesniffer": "^3.7.2", + "vimeo/psalm": "^5.13.1" + }, + "suggest": { + "php-standard-library/psalm-plugin": "Psalm integration" + }, + "type": "library", + "extra": { + "thanks": { + "name": "hhvm/hsl", + "url": "https://github.com/hhvm/hsl" + } + }, + "autoload": { + "files": [ + "src/bootstrap.php" + ], + "psr-4": { + "Psl\\": "src/Psl" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "azjezz", + "email": "azjezz@protonmail.com" + } + ], + "description": "PHP Standard Library", + "support": { + "issues": "https://github.com/azjezz/psl/issues", + "source": "https://github.com/azjezz/psl/tree/2.9.1" + }, + "funding": [ + { + "url": "https://github.com/azjezz", + "type": "github" + } + ], + "time": "2024-04-05T05:18:37+00:00" + }, { "name": "composer/ca-bundle", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd" + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", - "reference": "3ce240142f6d59b808dd65c1f52f7a1c252e6cfd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan": "^1.10", "psr/log": "^1.0", "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -64,7 +138,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.4.1" + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" }, "funding": [ { @@ -80,20 +154,20 @@ "type": "tidelift" } ], - "time": "2024-02-23T10:16:52+00:00" + "time": "2024-03-15T14:00:32+00:00" }, { "name": "composer/class-map-generator", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + "reference": "8286a62d243312ed99b3eee20d5005c961adb311" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311", "shasum": "" }, "require": { @@ -137,7 +211,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.0" + "source": "https://github.com/composer/class-map-generator/tree/1.1.1" }, "funding": [ { @@ -153,7 +227,7 @@ "type": "tidelift" } ], - "time": "2023-06-30T13:58:57+00:00" + "time": "2024-03-15T12:53:41+00:00" }, { "name": "composer/composer", @@ -161,12 +235,12 @@ "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "b12a88b7f3313e9dbae5b58085323b8328d10296" + "reference": "c5ff69ed584449de3dbd01ecc0c145d80fa51fd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/b12a88b7f3313e9dbae5b58085323b8328d10296", - "reference": "b12a88b7f3313e9dbae5b58085323b8328d10296", + "url": "https://api.github.com/repos/composer/composer/zipball/c5ff69ed584449de3dbd01ecc0c145d80fa51fd2", + "reference": "c5ff69ed584449de3dbd01ecc0c145d80fa51fd2", "shasum": "" }, "require": { @@ -268,7 +342,7 @@ "type": "tidelift" } ], - "time": "2024-03-22T08:29:43+00:00" + "time": "2024-04-03T09:05:07+00:00" }, { "name": "composer/metadata-minifier", @@ -341,16 +415,16 @@ }, { "name": "composer/pcre", - "version": "3.1.1", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9" + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/00104306927c7a0919b4ced2aaa6782c1e61a3c9", - "reference": "00104306927c7a0919b4ced2aaa6782c1e61a3c9", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", "shasum": "" }, "require": { @@ -392,7 +466,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.1" + "source": "https://github.com/composer/pcre/tree/3.1.3" }, "funding": [ { @@ -408,7 +482,7 @@ "type": "tidelift" } ], - "time": "2023-10-11T07:11:09+00:00" + "time": "2024-03-19T10:26:25+00:00" }, { "name": "composer/semver", @@ -573,16 +647,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", "shasum": "" }, "require": { @@ -593,7 +667,7 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -617,9 +691,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" }, "funding": [ { @@ -635,7 +709,7 @@ "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2024-03-26T18:29:49+00:00" }, { "name": "guzzlehttp/guzzle", @@ -964,7 +1038,7 @@ }, { "name": "illuminate/container", - "version": "v10.47.0", + "version": "v10.48.7", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", @@ -1015,7 +1089,7 @@ }, { "name": "illuminate/contracts", - "version": "v10.47.0", + "version": "v10.48.7", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -1562,6 +1636,78 @@ ], "time": "2023-11-16T16:21:57+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/25de49af7223ba039f64da4ae9a28ec2d10d0254", + "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.6" + }, + "time": "2023-11-30T05:34:44+00:00" + }, { "name": "seld/jsonlint", "version": "1.10.2", @@ -1737,16 +1883,16 @@ }, { "name": "symfony/console", - "version": "v6.4.4", + "version": "v6.4.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78" + "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0d9e4eb5ad413075624378f474c4167ea202de78", - "reference": "0d9e4eb5ad413075624378f474c4167ea202de78", + "url": "https://api.github.com/repos/symfony/console/zipball/a2708a5da5c87d1d0d52937bdeac625df659e11f", + "reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f", "shasum": "" }, "require": { @@ -1811,7 +1957,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.4" + "source": "https://github.com/symfony/console/tree/v6.4.6" }, "funding": [ { @@ -1827,7 +1973,7 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:10+00:00" + "time": "2024-03-29T19:07:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1898,16 +2044,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.3", + "version": "v6.4.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb" + "reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", - "reference": "7f3b1755eb49297a0827a7575d5d2b2fd11cc9fb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/9919b5509ada52cc7f66f9a35c86a4a29955c9d3", + "reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3", "shasum": "" }, "require": { @@ -1941,7 +2087,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.3" + "source": "https://github.com/symfony/filesystem/tree/v6.4.6" }, "funding": [ { @@ -1957,7 +2103,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:51:35+00:00" + "time": "2024-03-21T19:36:20+00:00" }, { "name": "symfony/finder", @@ -2636,16 +2782,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.4.1", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", "shasum": "" }, "require": { @@ -2698,7 +2844,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" }, "funding": [ { @@ -2714,7 +2860,7 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2023-12-19T21:51:00+00:00" }, { "name": "symfony/string", @@ -2864,16 +3010,16 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v2.6.2", + "version": "v2.6.4", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb" + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", - "reference": "9d5100cebffa729aaffecd3ad25dc5aeea4f13bb", + "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", "shasum": "" }, "require": { @@ -2885,8 +3031,8 @@ "ext-json": "*", "jetbrains/phpstorm-stubs": "^2019.3", "phpunit/phpunit": "^7 | ^8 | ^9", - "psalm/phar": "^3.11@dev", - "react/promise": "^2" + "react/promise": "^2", + "vimeo/psalm": "^3.12" }, "type": "library", "extra": { @@ -2941,7 +3087,7 @@ "support": { "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v2.6.2" + "source": "https://github.com/amphp/amp/tree/v2.6.4" }, "funding": [ { @@ -2949,7 +3095,7 @@ "type": "github" } ], - "time": "2022-02-20T17:52:18+00:00" + "time": "2024-03-21T18:52:26+00:00" }, { "name": "amphp/byte-stream", @@ -3593,21 +3739,21 @@ }, { "name": "nikic/php-parser", - "version": "v4.18.0", + "version": "v4.19.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=7.0" + "php": ">=7.1" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", @@ -3643,26 +3789,27 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2023-12-10T21:03:43+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", @@ -3703,9 +3850,15 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" + "source": "https://github.com/phar-io/manifest/tree/2.0.4" }, - "time": "2021-07-20T11:28:43+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", @@ -3758,6 +3911,60 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-standard-library/psalm-plugin", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-standard-library/psalm-plugin.git", + "reference": "bf6d560ae498966150bc66a42e02744b0ee242c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-standard-library/psalm-plugin/zipball/bf6d560ae498966150bc66a42e02744b0ee242c5", + "reference": "bf6d560ae498966150bc66a42e02744b0ee242c5", + "shasum": "" + }, + "require": { + "php": "^8.1", + "vimeo/psalm": ">=5.16" + }, + "conflict": { + "azjezz/psl": "<2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18", + "roave/security-advisories": "dev-master", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "psalm-plugin", + "extra": { + "psalm": { + "pluginClass": "Psl\\Psalm\\Plugin" + } + }, + "autoload": { + "psr-4": { + "Psl\\Psalm\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "azjezz", + "email": "azjezz@protonmail.com" + } + ], + "description": "Psalm plugin for the PHP Standard Library", + "support": { + "issues": "https://github.com/php-standard-library/psalm-plugin/issues", + "source": "https://github.com/php-standard-library/psalm-plugin/tree/2.3.0" + }, + "time": "2023-11-28T12:22:48+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -3813,28 +4020,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", + "version": "5.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a", + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a", "shasum": "" }, "require": { + "doctrine/deprecations": "^1.1", "ext-filter": "*", - "php": "^7.2 || ^8.0", + "php": "^7.4 || ^8.0", "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" }, "type": "library", "extra": { @@ -3858,15 +4072,15 @@ }, { "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "email": "opensource@ijaap.nl" } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0" }, - "time": "2021-10-19T17:43:47+00:00" + "time": "2024-04-09T21:13:58+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -3928,16 +4142,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.26.0", + "version": "1.28.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "231e3186624c03d7e7c890ec662b81e6b0405227" + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/231e3186624c03d7e7c890ec662b81e6b0405227", - "reference": "231e3186624c03d7e7c890ec662b81e6b0405227", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", "shasum": "" }, "require": { @@ -3969,22 +4183,22 @@ "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/1.26.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.28.0" }, - "time": "2024-02-23T16:05:55+00:00" + "time": "2024-04-03T18:51:33+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.11", + "version": "10.1.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "78c3b7625965c2513ee96569a4dbb62601784145" + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/78c3b7625965c2513ee96569a4dbb62601784145", - "reference": "78c3b7625965c2513ee96569a4dbb62601784145", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", "shasum": "" }, "require": { @@ -4041,7 +4255,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" }, "funding": [ { @@ -4049,7 +4263,7 @@ "type": "github" } ], - "time": "2023-12-21T15:38:30+00:00" + "time": "2024-03-12T15:33:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4296,16 +4510,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.11", + "version": "10.5.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4" + "reference": "c1f736a473d21957ead7e94fcc029f571895abf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4", - "reference": "0d968f6323deb3dbfeba5bfd4929b9415eb7a9a4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1f736a473d21957ead7e94fcc029f571895abf5", + "reference": "c1f736a473d21957ead7e94fcc029f571895abf5", "shasum": "" }, "require": { @@ -4377,7 +4591,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.11" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.17" }, "funding": [ { @@ -4393,7 +4607,7 @@ "type": "tidelift" } ], - "time": "2024-02-25T14:05:00+00:00" + "time": "2024-04-05T04:39:01+00:00" }, { "name": "psalm/plugin-phpunit", @@ -4457,16 +4671,16 @@ }, { "name": "sebastian/cli-parser", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", - "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { @@ -4501,7 +4715,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.0" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" }, "funding": [ { @@ -4509,7 +4724,7 @@ "type": "github" } ], - "time": "2023-02-03T06:58:15+00:00" + "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", @@ -4759,16 +4974,16 @@ }, { "name": "sebastian/diff", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f" + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/fbf413a49e54f6b9b17e12d900ac7f6101591b7f", - "reference": "fbf413a49e54f6b9b17e12d900ac7f6101591b7f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { @@ -4776,7 +4991,7 @@ }, "require-dev": { "phpunit/phpunit": "^10.0", - "symfony/process": "^4.2 || ^5" + "symfony/process": "^6.4" }, "type": "library", "extra": { @@ -4814,7 +5029,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" }, "funding": [ { @@ -4822,20 +5037,20 @@ "type": "github" } ], - "time": "2023-12-22T10:55:06+00:00" + "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { @@ -4850,7 +5065,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -4878,7 +5093,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -4886,20 +5101,20 @@ "type": "github" } ], - "time": "2023-04-11T05:39:26+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", - "version": "5.1.1", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", - "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { @@ -4956,7 +5171,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" }, "funding": [ { @@ -4964,20 +5179,20 @@ "type": "github" } ], - "time": "2023-09-24T13:22:09+00:00" + "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4" + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4", - "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { @@ -5011,14 +5226,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" }, "funding": [ { @@ -5026,7 +5241,7 @@ "type": "github" } ], - "time": "2023-07-19T07:19:23+00:00" + "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", @@ -5372,32 +5587,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.14.1", + "version": "8.15.0", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926" + "reference": "7d1d957421618a3803b593ec31ace470177d7817" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/fea1fd6f137cc84f9cba0ae30d549615dbc6a926", - "reference": "fea1fd6f137cc84f9cba0ae30d549615dbc6a926", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/7d1d957421618a3803b593ec31ace470177d7817", + "reference": "7d1d957421618a3803b593ec31ace470177d7817", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", "phpstan/phpdoc-parser": "^1.23.1", - "squizlabs/php_codesniffer": "^3.7.1" + "squizlabs/php_codesniffer": "^3.9.0" }, "require-dev": { "phing/phing": "2.17.4", "php-parallel-lint/php-parallel-lint": "1.3.2", - "phpstan/phpstan": "1.10.37", + "phpstan/phpstan": "1.10.60", "phpstan/phpstan-deprecation-rules": "1.1.4", - "phpstan/phpstan-phpunit": "1.3.14", - "phpstan/phpstan-strict-rules": "1.5.1", - "phpunit/phpunit": "8.5.21|9.6.8|10.3.5" + "phpstan/phpstan-phpunit": "1.3.16", + "phpstan/phpstan-strict-rules": "1.5.2", + "phpunit/phpunit": "8.5.21|9.6.8|10.5.11" }, "type": "phpcodesniffer-standard", "extra": { @@ -5421,7 +5636,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.14.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.15.0" }, "funding": [ { @@ -5433,7 +5648,7 @@ "type": "tidelift" } ], - "time": "2023-10-08T07:28:08+00:00" + "time": "2024-03-09T15:20:58+00:00" }, { "name": "spatie/array-to-xml", @@ -5500,16 +5715,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909", + "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909", "shasum": "" }, "require": { @@ -5576,20 +5791,20 @@ "type": "open_collective" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2024-03-31T21:03:09+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.2", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", - "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { @@ -5618,7 +5833,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.2" + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" }, "funding": [ { @@ -5626,20 +5841,20 @@ "type": "github" } ], - "time": "2023-11-20T00:12:19+00:00" + "time": "2024-03-03T12:36:25+00:00" }, { "name": "vimeo/psalm", - "version": "5.22.2", + "version": "5.23.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d768d914152dbbf3486c36398802f74e80cfde48" + "reference": "8471a896ccea3526b26d082f4461eeea467f10a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d768d914152dbbf3486c36398802f74e80cfde48", - "reference": "d768d914152dbbf3486c36398802f74e80cfde48", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/8471a896ccea3526b26d082f4461eeea467f10a4", + "reference": "8471a896ccea3526b26d082f4461eeea467f10a4", "shasum": "" }, "require": { @@ -5736,7 +5951,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2024-02-22T23:39:07+00:00" + "time": "2024-03-11T20:33:46+00:00" } ], "aliases": [], diff --git a/psalm.xml.dist b/psalm.xml.dist index 5fd0a8e..cc9b3b2 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -19,11 +19,12 @@ + - + diff --git a/src/Container.php b/src/Container.php index a84ce4c..b3a954f 100644 --- a/src/Container.php +++ b/src/Container.php @@ -21,9 +21,9 @@ use Php\Pie\Downloading\DownloadZip; use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\UnixDownloadAndExtract; +use Php\Pie\Downloading\WindowsDownloadAndExtract; use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; use Psr\Container\ContainerInterface; -use RuntimeException; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; @@ -49,7 +49,13 @@ public static function factory(): ContainerInterface }); $container->singleton(Composer::class, static function (ContainerInterface $container): Composer { $io = $container->get(IOInterface::class); - $composer = (new ComposerFactory())->createComposer($io, [], true); + $composer = (new ComposerFactory())->createComposer( + $io, + [ + 'config' => ['lock' => false], + ], + true, + ); $io->loadConfiguration($composer->getConfig()); return $composer; @@ -74,12 +80,30 @@ static function (ContainerInterface $container): UnixDownloadAndExtract { return new UnixDownloadAndExtract( new DownloadZip( new Client(), - new AuthHelper( - $container->get(IOInterface::class), - $container->get(Composer::class)->getConfig(), - ), ), new ExtractZip(), + new AuthHelper( + $container->get(IOInterface::class), + $container->get(Composer::class)->getConfig(), + ), + ); + }, + ); + $container->singleton( + WindowsDownloadAndExtract::class, + static function (ContainerInterface $container): WindowsDownloadAndExtract { + $guzzleClient = new Client(); + + return new WindowsDownloadAndExtract( + new DownloadZip( + $guzzleClient, + ), + new ExtractZip(), + new AuthHelper( + $container->get(IOInterface::class), + $container->get(Composer::class)->getConfig(), + ), + $guzzleClient, ); }, ); @@ -87,8 +111,7 @@ static function (ContainerInterface $container): UnixDownloadAndExtract { DownloadAndExtract::class, static function (ContainerInterface $container): DownloadAndExtract { if (Platform::isWindows()) { - // @todo add windows downloader - throw new RuntimeException('Windows support not yet'); + return $container->get(WindowsDownloadAndExtract::class); } return $container->get(UnixDownloadAndExtract::class); diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 4f251ca..21f487b 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -27,7 +27,7 @@ public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, stri $this->repositorySet, ($this->resolveTargetPhpToPlatformRepository)($phpBinaryPath), )) - ->findBestCandidate($packageName, $requestedVersion); + ->findBestCandidate($packageName, $requestedVersion, 'dev', null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); diff --git a/src/Downloading/AddAuthenticationHeader.php b/src/Downloading/AddAuthenticationHeader.php new file mode 100644 index 0000000..a0b4c17 --- /dev/null +++ b/src/Downloading/AddAuthenticationHeader.php @@ -0,0 +1,37 @@ +downloadUrl === null) { + throw new RuntimeException(sprintf('The package %s does not have a download URL', $package->name)); + } + + $authHeaders = $authHelper->addAuthenticationHeader([], 'github.com', $package->downloadUrl); + array_walk( + $authHeaders, + static function (string $v) use (&$request): void { + // @todo probably process this better + $headerParts = explode(':', $v); + $request = $request->withHeader(trim($headerParts[0]), trim($headerParts[1])); + }, + ); + + return $request; + } +} diff --git a/src/Downloading/DownloadZip.php b/src/Downloading/DownloadZip.php index 756a35c..4b13acc 100644 --- a/src/Downloading/DownloadZip.php +++ b/src/Downloading/DownloadZip.php @@ -4,49 +4,24 @@ namespace Php\Pie\Downloading; -use Composer\Util\AuthHelper; use GuzzleHttp\ClientInterface; -use GuzzleHttp\Psr7\Request; use GuzzleHttp\RequestOptions; -use Php\Pie\DependencyResolver\Package; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use function array_walk; use function assert; -use function explode; use function file_put_contents; -use function trim; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class DownloadZip { public function __construct( private readonly ClientInterface $client, - private readonly AuthHelper $authHelper, ) { } - public function downloadZipAndReturnLocalPath(Package $package, string $localPath): string + public function downloadZipAndReturnLocalPath(RequestInterface $request, string $localPath): string { - if ($package->downloadUrl === null) { - throw new RuntimeException('Could not download a package without a download URL'); - } - - $request = new Request('GET', $package->downloadUrl); - - $authHeaders = $this->authHelper->addAuthenticationHeader([], 'github.com', $package->downloadUrl); - array_walk( - $authHeaders, - static function (string $v) use (&$request): void { - // @todo probably process this better - $headerParts = explode(':', $v); - $request = $request->withHeader(trim($headerParts[0]), trim($headerParts[1])); - }, - ); - - assert($request instanceof RequestInterface); $response = $this->client ->sendAsync( $request, diff --git a/src/Downloading/UnixDownloadAndExtract.php b/src/Downloading/UnixDownloadAndExtract.php index 60d702d..1a344e2 100644 --- a/src/Downloading/UnixDownloadAndExtract.php +++ b/src/Downloading/UnixDownloadAndExtract.php @@ -4,10 +4,15 @@ namespace Php\Pie\Downloading; +use Composer\Util\AuthHelper; +use GuzzleHttp\Psr7\Request; use Php\Pie\DependencyResolver\Package; +use Psr\Http\Message\RequestInterface; +use RuntimeException; use function file_exists; use function mkdir; +use function sprintf; use function sys_get_temp_dir; use function uniqid; @@ -17,20 +22,36 @@ final class UnixDownloadAndExtract implements DownloadAndExtract public function __construct( private readonly DownloadZip $downloadZip, private readonly ExtractZip $extractZip, + private readonly AuthHelper $authHelper, ) { } public function __invoke(Package $package): DownloadedPackage { + // @todo extract to a static util $localTempPath = sys_get_temp_dir() . '/' . uniqid('pie_downloader_', true); if (! file_exists($localTempPath)) { mkdir($localTempPath, recursive: true); } - $tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath($package, $localTempPath); + $tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath( + $this->createRequestForUnixDownloadUrl($package), + $localTempPath, + ); $extractedPath = $this->extractZip->to($tmpZipFile, $localTempPath); return DownloadedPackage::fromPackageAndExtractedPath($package, $extractedPath); } + + private function createRequestForUnixDownloadUrl(Package $package): RequestInterface + { + if ($package->downloadUrl === null) { + throw new RuntimeException(sprintf('The package %s does not have a download URL', $package->name)); + } + + $request = new Request('GET', $package->downloadUrl); + + return AddAuthenticationHeader::withAuthHeaderFromComposer($request, $package, $this->authHelper); + } } diff --git a/src/Downloading/WindowsDownloadAndExtract.php b/src/Downloading/WindowsDownloadAndExtract.php new file mode 100644 index 0000000..045cd5c --- /dev/null +++ b/src/Downloading/WindowsDownloadAndExtract.php @@ -0,0 +1,141 @@ +selectMatchingReleaseAsset( + $package, + $this->getReleaseAssetsForPackage($package), + ); + + // @todo extract to a static util + $localTempPath = sys_get_temp_dir() . '/' . uniqid('pie_downloader_', true); + if (! file_exists($localTempPath)) { + mkdir($localTempPath, recursive: true); + } + + $tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath( + AddAuthenticationHeader::withAuthHeaderFromComposer( + new Request('GET', $releaseAsset['browser_download_url']), + $package, + $this->authHelper, + ), + $localTempPath, + ); + + $this->extractZip->to($tmpZipFile, $localTempPath); + + return DownloadedPackage::fromPackageAndExtractedPath($package, $localTempPath); + } + + /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ + // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName + /** + * @param list $releaseAssets + * + * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} + */ + // phpcs:enable + private function selectMatchingReleaseAsset(Package $package, array $releaseAssets): array + { + // @todo source these from the right places... + $arch = 'x86'; + $ts = 'nts'; + $compiler = 'vs16'; + $phpVersion = '8.3'; + $extensionName = str_replace('-', '_', 'example-pie-extension'); + $expectedAssetName = sprintf( + 'php_%s-%s-%s-%s-%s-%s.zip', + $extensionName, + $package->version, + $phpVersion, + $compiler, + $ts, + $arch, + ); + + foreach ($releaseAssets as $releaseAsset) { + if ($releaseAsset['name'] === $expectedAssetName) { + return $releaseAsset; + } + } + + throw new RuntimeException('Could not find release asset for ' . $package->version . ' named: ' . $expectedAssetName); + } + + /** @return list */ + private function getReleaseAssetsForPackage(Package $package): array + { + // @todo dynamic URL, don't hard code it... + // @todo confirm prettyName will always match the repo name - it might not + $request = AddAuthenticationHeader::withAuthHeaderFromComposer( + new Request('GET', 'https://api.github.com/repos/' . $package->name . '/releases/tags/' . $package->version), + $package, + $this->authHelper, + ); + + $response = $this->client + ->sendAsync( + $request, + [ + RequestOptions::ALLOW_REDIRECTS => true, + RequestOptions::HTTP_ERRORS => false, + RequestOptions::SYNCHRONOUS => true, + ], + ) + ->wait(); + assert($response instanceof ResponseInterface); + + // @todo check response was successful + + $releaseAssets = Json\typed( + (string) $response->getBody(), + Type\shape( + [ + 'assets' => Type\vec(Type\shape( + [ + 'name' => Type\non_empty_string(), + 'browser_download_url' => Type\non_empty_string(), + ], + true, + )), + ], + true, + ), + ); + + return $releaseAssets['assets']; + } +} diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index e58aa7e..def32e0 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -38,6 +38,7 @@ public function testDownloadCommand(): void $outputString = $this->commandTester->getDisplay(); self::assertStringContainsString('Found package: asgrim/example-pie-extension (version: 1.0.0)', $outputString); self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString); + self::assertStringContainsString('Extracted asgrim/example-pie-extension:1.0.0 source', $outputString); } public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void From d7b298575974e52bfcada8bdcc299e1aae13f2c2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 15 Apr 2024 10:49:20 +0100 Subject: [PATCH 15/43] Adjusted annotations for immutable/internal --- src/DependencyResolver/Package.php | 6 +++++- src/Downloading/AddAuthenticationHeader.php | 1 + src/Downloading/DownloadedPackage.php | 6 +++++- src/TargetPhp/PhpBinaryPath.php | 6 +++++- src/TargetPhp/ResolveTargetPhpToPlatformRepository.php | 1 + 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index f3f9314..7fabaeb 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -6,7 +6,11 @@ use Composer\Package\CompletePackageInterface; -/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ +/** + * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks + * + * @immutable + */ final class Package { public const TYPE_PHP_MODULE = 'php-ext'; diff --git a/src/Downloading/AddAuthenticationHeader.php b/src/Downloading/AddAuthenticationHeader.php index a0b4c17..1e54478 100644 --- a/src/Downloading/AddAuthenticationHeader.php +++ b/src/Downloading/AddAuthenticationHeader.php @@ -14,6 +14,7 @@ use function sprintf; use function trim; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class AddAuthenticationHeader { public static function withAuthHeaderFromComposer(RequestInterface $request, Package $package, AuthHelper $authHelper): RequestInterface diff --git a/src/Downloading/DownloadedPackage.php b/src/Downloading/DownloadedPackage.php index ce6dab6..0e21461 100644 --- a/src/Downloading/DownloadedPackage.php +++ b/src/Downloading/DownloadedPackage.php @@ -6,7 +6,11 @@ use Php\Pie\DependencyResolver\Package; -/** @immutable */ +/** + * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks + * + * @immutable + */ final class DownloadedPackage { private function __construct( diff --git a/src/TargetPhp/PhpBinaryPath.php b/src/TargetPhp/PhpBinaryPath.php index cadb8b2..7c5c1e0 100644 --- a/src/TargetPhp/PhpBinaryPath.php +++ b/src/TargetPhp/PhpBinaryPath.php @@ -11,7 +11,11 @@ use function trim; -/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ +/** + * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks + * + * @immutable + */ class PhpBinaryPath { /** @param non-empty-string $phpBinaryPath */ diff --git a/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php b/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php index 9e3eaac..93c2ad0 100644 --- a/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php +++ b/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php @@ -6,6 +6,7 @@ use Composer\Repository\PlatformRepository; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class ResolveTargetPhpToPlatformRepository { public function __invoke(PhpBinaryPath $phpBinaryPath): PlatformRepository From f3bbf035d35f2a2a0f3067f9623970ca5cc81358 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 08:38:53 +0100 Subject: [PATCH 16/43] Use DIRECTORY_SEPARATOR constant instead of literal forward-slash Co-authored-by: Anton Vasiliev --- src/Downloading/UnixDownloadAndExtract.php | 4 +++- src/Downloading/WindowsDownloadAndExtract.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Downloading/UnixDownloadAndExtract.php b/src/Downloading/UnixDownloadAndExtract.php index 1a344e2..699340e 100644 --- a/src/Downloading/UnixDownloadAndExtract.php +++ b/src/Downloading/UnixDownloadAndExtract.php @@ -16,6 +16,8 @@ use function sys_get_temp_dir; use function uniqid; +use const DIRECTORY_SEPARATOR; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class UnixDownloadAndExtract implements DownloadAndExtract { @@ -29,7 +31,7 @@ public function __construct( public function __invoke(Package $package): DownloadedPackage { // @todo extract to a static util - $localTempPath = sys_get_temp_dir() . '/' . uniqid('pie_downloader_', true); + $localTempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_downloader_', true); if (! file_exists($localTempPath)) { mkdir($localTempPath, recursive: true); } diff --git a/src/Downloading/WindowsDownloadAndExtract.php b/src/Downloading/WindowsDownloadAndExtract.php index 045cd5c..84f8d74 100644 --- a/src/Downloading/WindowsDownloadAndExtract.php +++ b/src/Downloading/WindowsDownloadAndExtract.php @@ -22,6 +22,8 @@ use function sys_get_temp_dir; use function uniqid; +use const DIRECTORY_SEPARATOR; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class WindowsDownloadAndExtract implements DownloadAndExtract { @@ -41,7 +43,7 @@ public function __invoke(Package $package): DownloadedPackage ); // @todo extract to a static util - $localTempPath = sys_get_temp_dir() . '/' . uniqid('pie_downloader_', true); + $localTempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_downloader_', true); if (! file_exists($localTempPath)) { mkdir($localTempPath, recursive: true); } From c60458ffac4a47c286c42db5a52c50966d30baae Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 09:24:24 +0100 Subject: [PATCH 17/43] Added a bunch of unit tests for existing code --- phpunit.xml.dist | 1 - src/DependencyResolver/Package.php | 2 +- .../UnableToResolveRequirement.php | 4 +- test/assets/test-zip.zip | Bin 0 -> 427 bytes .../UnableToResolveRequirementTest.php | 48 +++++++++++++++ .../AddAuthenticationHeaderTest.php | 56 ++++++++++++++++++ test/unit/Downloading/DownloadZipTest.php | 54 +++++++++++++++++ .../Downloading/DownloadedPackageTest.php | 28 +++++++++ test/unit/Downloading/ExtractZipTest.php | 48 +++++++++++++++ test/unit/TargetPhp/PhpBinaryPathTest.php | 26 ++++++++ ...solveTargetPhpToPlatformRepositoryTest.php | 41 +++++++++++++ 11 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 test/assets/test-zip.zip create mode 100644 test/unit/DependencyResolver/UnableToResolveRequirementTest.php create mode 100644 test/unit/Downloading/AddAuthenticationHeaderTest.php create mode 100644 test/unit/Downloading/DownloadZipTest.php create mode 100644 test/unit/Downloading/DownloadedPackageTest.php create mode 100644 test/unit/Downloading/ExtractZipTest.php create mode 100644 test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a6f2dc9..d81e94a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,7 +6,6 @@ cacheDirectory=".phpunit.cache" executionOrder="depends,defects" requireCoverageMetadata="true" - beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true" failOnRisky="true" failOnWarning="true"> diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 7fabaeb..d6c1151 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -16,7 +16,7 @@ final class Package public const TYPE_PHP_MODULE = 'php-ext'; public const TYPE_ZEND_EXTENSION = 'php-ext-zend'; - private function __construct( + public function __construct( public readonly string $name, public readonly string $version, public readonly string|null $downloadUrl, diff --git a/src/DependencyResolver/UnableToResolveRequirement.php b/src/DependencyResolver/UnableToResolveRequirement.php index 3ab76ad..1815eef 100644 --- a/src/DependencyResolver/UnableToResolveRequirement.php +++ b/src/DependencyResolver/UnableToResolveRequirement.php @@ -23,10 +23,10 @@ public static function fromRequirement(string $requiredPackageName, string|null public static function toPhpOrZendExtension(PackageInterface $locatedComposerPackage, string $requiredPackageName, string|null $requiredVersion): self { return new self(sprintf( - 'Package %s was not of type php-ext or php-ext-zend (requested %s%s)', + 'Package %s was not of type php-ext or php-ext-zend (requested %s%s).', $locatedComposerPackage->getName(), $requiredPackageName, - $requiredVersion !== null ? sprintf(' for version %s.', $requiredVersion) : '.', + $requiredVersion !== null ? sprintf(' for version %s', $requiredVersion) : '', )); } } diff --git a/test/assets/test-zip.zip b/test/assets/test-zip.zip new file mode 100644 index 0000000000000000000000000000000000000000..451d018990941b09d4c018c564941a20ca61bdf3 GIT binary patch literal 427 zcmWIWW@h1H00G<7{s=GwN(eB>FqEVgm*`ey7U-ts=cJ?->4%1JGBCFZ$)@!IacKoN z10%}|W(Ec@5db$S1ZdKi_V%ULK$AdN7K=$>lhZPDQuRtIO2Ec$02;Xo(^!wxoSb}x zl8n@%R7HglpnDWTLp@y-k}Qo(lT9s?bd6KZ(sWHyO-ywy42&#w4J}QK4O7w#EmBfV z0=yZScreateMock(PackageInterface::class); + $package->method('getName')->willReturn('baz/bat'); + + $exception = UnableToResolveRequirement::toPhpOrZendExtension($package, 'foo/bar', '^1.2'); + + self::assertSame('Package baz/bat was not of type php-ext or php-ext-zend (requested foo/bar for version ^1.2).', $exception->getMessage()); + } + + public function testToPhpOrZendExtensionWithoutVersion(): void + { + $package = $this->createMock(PackageInterface::class); + $package->method('getName')->willReturn('baz/bat'); + + $exception = UnableToResolveRequirement::toPhpOrZendExtension($package, 'foo/bar', null); + + self::assertSame('Package baz/bat was not of type php-ext or php-ext-zend (requested foo/bar).', $exception->getMessage()); + } + + public function testFromRequirementWithVersion(): void + { + $exception = UnableToResolveRequirement::fromRequirement('foo/bar', '^1.2'); + + self::assertSame('Unable to find an installable package foo/bar for version ^1.2.', $exception->getMessage()); + } + + public function testFromRequirementWithoutVersion(): void + { + $exception = UnableToResolveRequirement::fromRequirement('foo/bar', null); + + self::assertSame('Unable to find an installable package foo/bar.', $exception->getMessage()); + } +} diff --git a/test/unit/Downloading/AddAuthenticationHeaderTest.php b/test/unit/Downloading/AddAuthenticationHeaderTest.php new file mode 100644 index 0000000..f24b0ee --- /dev/null +++ b/test/unit/Downloading/AddAuthenticationHeaderTest.php @@ -0,0 +1,56 @@ +createMock(AuthHelper::class); + $authHelper->expects(self::once()) + ->method('addAuthenticationHeader') + ->with([], 'github.com', $downloadUrl) + ->willReturn(['Authorization: whatever ABC123']); + + $requestWithAuthHeader = (new AddAuthenticationHeader())->withAuthHeaderFromComposer( + $request, + new Package('foo/bar', '1.2.3', $downloadUrl), + $authHelper, + ); + + self::assertSame('whatever ABC123', $requestWithAuthHeader->getHeaderLine('Authorization')); + } + + public function testExceptionIsThrownWhenPackageDoesNotHaveDownloadUrl(): void + { + $downloadUrl = 'http://test-uri/' . uniqid('path', true); + + $request = new Request('GET', $downloadUrl); + + $authHelper = $this->createMock(AuthHelper::class); + + $addAuthenticationHeader = new AddAuthenticationHeader(); + $package = new Package('foo/bar', '1.2.3', null); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The package foo/bar does not have a download URL'); + $addAuthenticationHeader->withAuthHeaderFromComposer($request, $package, $authHelper); + } +} diff --git a/test/unit/Downloading/DownloadZipTest.php b/test/unit/Downloading/DownloadZipTest.php new file mode 100644 index 0000000..a7e0473 --- /dev/null +++ b/test/unit/Downloading/DownloadZipTest.php @@ -0,0 +1,54 @@ + 'application/octet-stream', + 'Content-length' => (string) (strlen($fakeZipContent)), + ], + $fakeZipContent, + ), + ]); + + $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); + + $localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_', true); + mkdir($localPath, 0777, true); + $downloadedZipFile = (new DownloadZip($guzzleMockClient)) + ->downloadZipAndReturnLocalPath( + new Request('GET', 'http://test-uri/'), + $localPath, + ); + + self::assertSame($fakeZipContent, file_get_contents($downloadedZipFile)); + } +} diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php new file mode 100644 index 0000000..2d029c8 --- /dev/null +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -0,0 +1,28 @@ +extractedSourcePath); + self::assertSame($package, $downloadedPackage->package); + } +} diff --git a/test/unit/Downloading/ExtractZipTest.php b/test/unit/Downloading/ExtractZipTest.php new file mode 100644 index 0000000..f6d7149 --- /dev/null +++ b/test/unit/Downloading/ExtractZipTest.php @@ -0,0 +1,48 @@ +to(__DIR__ . '/../../assets/test-zip.zip', $localPath); + + // The test-zip.zip should contain a deterministic file content for this: + self::assertSame('Hello there! Test UUID b925c59b-3e6f-4e45-8029-19431df18de4', file_get_contents($extractedPath . DIRECTORY_SEPARATOR . 'test-file.txt')); + } + + public function testFailureToExtractZipWithInvalidZip(): void + { + $localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_DO_NOT_EXIST_', true); + + $extr = new ExtractZip(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(sprintf('Could not open ZIP [%d]: %s', ZipArchive::ER_NOZIP, __FILE__)); + $extr->to(__FILE__, $localPath); + } +} diff --git a/test/unit/TargetPhp/PhpBinaryPathTest.php b/test/unit/TargetPhp/PhpBinaryPathTest.php index a18bdb5..212a195 100644 --- a/test/unit/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/TargetPhp/PhpBinaryPathTest.php @@ -7,8 +7,12 @@ use Php\Pie\TargetPhp\PhpBinaryPath; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; +use function file_exists; +use function is_executable; use function sprintf; +use function trim; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; @@ -24,4 +28,26 @@ public function testVersionFromCurrentProcess(): void PhpBinaryPath::fromCurrentProcess()->version(), ); } + + public function testFromPhpConfigExecutable(): void + { + $process = (new Process(['which', 'php-config'])); + $exitCode = $process->run(); + $phpConfigExecutable = trim($process->getOutput()); + + if ($exitCode !== 0 || ! file_exists($phpConfigExecutable) || ! is_executable($phpConfigExecutable)) { + self::markTestSkipped('Needs php-config in path to run this test'); + } + + $phpBinary = PhpBinaryPath::fromPhpConfigExecutable($phpConfigExecutable); + + // NOTE: this makes an assumption that the `php-config` in path is the same as the version being executed + // In most cases, this will be the cases (e.g. in CI, running locally), but if you're trying to test this and + // the versions are not matching, that's probably why. + // @todo improve this assertion in future, if it becomes problematic + self::assertSame( + sprintf('%s.%s.%s', PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION), + $phpBinary->version(), + ); + } } diff --git a/test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php b/test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php new file mode 100644 index 0000000..70b80a7 --- /dev/null +++ b/test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php @@ -0,0 +1,41 @@ +getPackages(), + static function (BasePackage $package): bool { + return $package->getPrettyName() === 'php'; + }, + ); + + self::assertCount(1, $phpPackages); + assert(count($phpPackages) > 0); + + $phpPackage = $phpPackages[array_key_first($phpPackages)]; + + self::assertSame($php->version(), $phpPackage->getPrettyVersion()); + } +} From 32cc5b60e913fc073e53306b3964966996fe76e3 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 11:07:14 +0100 Subject: [PATCH 18/43] Split out some components and add test coverage --- src/Command/DownloadCommand.php | 7 +- src/Container.php | 44 +++--- src/DependencyResolver/Package.php | 5 + src/Downloading/DownloadZip.php | 41 +----- src/Downloading/DownloadZipWithGuzzle.php | 45 +++++++ .../Exception/CouldNotFindReleaseAsset.php | 22 +++ .../GithubPackageReleaseAssets.php | 126 ++++++++++++++++++ src/Downloading/PackageReleaseAssets.php | 14 ++ src/Downloading/UnixDownloadAndExtract.php | 1 + src/Downloading/WindowsDownloadAndExtract.php | 98 +------------- .../Command/DownloadCommandTest.php | 2 +- test/unit/DependencyResolver/PackageTest.php | 1 + ...Test.php => DownloadZipWithGuzzleTest.php} | 8 +- .../CouldNotFindReleaseAssetTest.php | 23 ++++ test/unit/Downloading/ExtractZipTest.php | 1 - .../GithubPackageReleaseAssetsTest.php | 75 +++++++++++ .../UnixDownloadAndExtractTest.php | 55 ++++++++ .../WindowsDownloadAndExtractTest.php | 69 ++++++++++ 18 files changed, 471 insertions(+), 166 deletions(-) create mode 100644 src/Downloading/DownloadZipWithGuzzle.php create mode 100644 src/Downloading/Exception/CouldNotFindReleaseAsset.php create mode 100644 src/Downloading/GithubPackageReleaseAssets.php create mode 100644 src/Downloading/PackageReleaseAssets.php rename test/unit/Downloading/{DownloadZipTest.php => DownloadZipWithGuzzleTest.php} (86%) create mode 100644 test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php create mode 100644 test/unit/Downloading/GithubPackageReleaseAssetsTest.php create mode 100644 test/unit/Downloading/UnixDownloadAndExtractTest.php create mode 100644 test/unit/Downloading/WindowsDownloadAndExtractTest.php diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 9fe973a..4a61d51 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -79,15 +79,14 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersionPair['version'], ); - $output->writeln(sprintf('Found package: %s (version: %s)', $package->name, $package->version)); + $output->writeln(sprintf('Found package: %s', $package->prettyNameAndVersion())); $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); $downloadedPackage = ($this->downloadAndExtract)($package); $output->writeln(sprintf( - 'Extracted %s:%s source to: %s', - $downloadedPackage->package->name, - $downloadedPackage->package->version, + 'Extracted %s source to: %s', + $downloadedPackage->package->prettyNameAndVersion(), $downloadedPackage->extractedSourcePath, )); diff --git a/src/Container.php b/src/Container.php index b3a954f..3839cc7 100644 --- a/src/Container.php +++ b/src/Container.php @@ -13,13 +13,17 @@ use Composer\Util\AuthHelper; use Composer\Util\Platform; use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\RequestOptions; use Illuminate\Container\Container as IlluminateContainer; use Php\Pie\Command\DownloadCommand; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\Downloading\DownloadAndExtract; use Php\Pie\Downloading\DownloadZip; -use Php\Pie\Downloading\ExtractZip; +use Php\Pie\Downloading\DownloadZipWithGuzzle; +use Php\Pie\Downloading\GithubPackageReleaseAssets; +use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Downloading\UnixDownloadAndExtract; use Php\Pie\Downloading\WindowsDownloadAndExtract; use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; @@ -74,39 +78,23 @@ static function (ContainerInterface $container): DependencyResolver { ); }, ); - $container->singleton( - UnixDownloadAndExtract::class, - static function (ContainerInterface $container): UnixDownloadAndExtract { - return new UnixDownloadAndExtract( - new DownloadZip( - new Client(), - ), - new ExtractZip(), - new AuthHelper( - $container->get(IOInterface::class), - $container->get(Composer::class)->getConfig(), - ), - ); + $container->bind( + ClientInterface::class, + static function (): ClientInterface { + return new Client([RequestOptions::HTTP_ERRORS => false]); }, ); $container->singleton( - WindowsDownloadAndExtract::class, - static function (ContainerInterface $container): WindowsDownloadAndExtract { - $guzzleClient = new Client(); - - return new WindowsDownloadAndExtract( - new DownloadZip( - $guzzleClient, - ), - new ExtractZip(), - new AuthHelper( - $container->get(IOInterface::class), - $container->get(Composer::class)->getConfig(), - ), - $guzzleClient, + AuthHelper::class, + static function (ContainerInterface $container): AuthHelper { + return new AuthHelper( + $container->get(IOInterface::class), + $container->get(Composer::class)->getConfig(), ); }, ); + $container->alias(DownloadZipWithGuzzle::class, DownloadZip::class); + $container->alias(GithubPackageReleaseAssets::class, PackageReleaseAssets::class); $container->singleton( DownloadAndExtract::class, static function (ContainerInterface $container): DownloadAndExtract { diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index d6c1151..0551e72 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -31,4 +31,9 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $completePackage->getDistUrl(), ); } + + public function prettyNameAndVersion(): string + { + return $this->name . ':' . $this->version; + } } diff --git a/src/Downloading/DownloadZip.php b/src/Downloading/DownloadZip.php index 4b13acc..0f9b0b5 100644 --- a/src/Downloading/DownloadZip.php +++ b/src/Downloading/DownloadZip.php @@ -4,42 +4,15 @@ namespace Php\Pie\Downloading; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\RequestOptions; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; - -use function assert; -use function file_put_contents; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ -final class DownloadZip +interface DownloadZip { - public function __construct( - private readonly ClientInterface $client, - ) { - } - - public function downloadZipAndReturnLocalPath(RequestInterface $request, string $localPath): string - { - $response = $this->client - ->sendAsync( - $request, - [ - RequestOptions::ALLOW_REDIRECTS => true, - RequestOptions::HTTP_ERRORS => false, - RequestOptions::SYNCHRONOUS => true, - ], - ) - ->wait(); - assert($response instanceof ResponseInterface); - - // @todo check response was successful - - // @todo handle this writing better - $tmpZipFile = $localPath . '/downloaded.zip'; - file_put_contents($tmpZipFile, $response->getBody()->__toString()); - - return $tmpZipFile; - } + /** + * @param non-empty-string $localPath + * + * @return non-empty-string + */ + public function downloadZipAndReturnLocalPath(RequestInterface $request, string $localPath): string; } diff --git a/src/Downloading/DownloadZipWithGuzzle.php b/src/Downloading/DownloadZipWithGuzzle.php new file mode 100644 index 0000000..ab1873d --- /dev/null +++ b/src/Downloading/DownloadZipWithGuzzle.php @@ -0,0 +1,45 @@ +client + ->sendAsync( + $request, + [ + RequestOptions::ALLOW_REDIRECTS => true, + RequestOptions::HTTP_ERRORS => false, + RequestOptions::SYNCHRONOUS => true, + ], + ) + ->wait(); + assert($response instanceof ResponseInterface); + + // @todo check response was successful + + // @todo handle this writing better + $tmpZipFile = $localPath . '/downloaded.zip'; + file_put_contents($tmpZipFile, $response->getBody()->__toString()); + + return $tmpZipFile; + } +} diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php new file mode 100644 index 0000000..ec1da61 --- /dev/null +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -0,0 +1,22 @@ +prettyNameAndVersion(), + $expectedAssetName, + )); + } +} diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php new file mode 100644 index 0000000..497d90d --- /dev/null +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -0,0 +1,126 @@ +selectMatchingReleaseAsset( + $package, + $this->getReleaseAssetsForPackage($package), + ); + + return $releaseAsset['browser_download_url']; + } + + /** @return non-empty-string */ + private function expectedWindowsAssetName(Package $package): string + { + // @todo source these from the right places... + $arch = 'x86'; + $ts = 'nts'; + $compiler = 'vs16'; + $phpVersion = '8.3'; + $extensionName = str_replace('-', '_', 'example-pie-extension'); + + return sprintf( + 'php_%s-%s-%s-%s-%s-%s.zip', + $extensionName, + $package->version, + $phpVersion, + $compiler, + $ts, + $arch, + ); + } + + /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ + // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName + /** + * @param list $releaseAssets + * + * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} + */ + // phpcs:enable + private function selectMatchingReleaseAsset(Package $package, array $releaseAssets): array + { + $expectedAssetName = $this->expectedWindowsAssetName($package); + + foreach ($releaseAssets as $releaseAsset) { + if ($releaseAsset['name'] === $expectedAssetName) { + return $releaseAsset; + } + } + + throw Exception\CouldNotFindReleaseAsset::forPackage($package, $expectedAssetName); + } + + /** @return list */ + private function getReleaseAssetsForPackage(Package $package): array + { + // @todo dynamic URL, don't hard code it... + // @todo confirm prettyName will always match the repo name - it might not + $request = AddAuthenticationHeader::withAuthHeaderFromComposer( + new Request('GET', 'https://api.github.com/repos/' . $package->name . '/releases/tags/' . $package->version), + $package, + $this->authHelper, + ); + + $response = $this->client + ->sendAsync( + $request, + [ + RequestOptions::ALLOW_REDIRECTS => true, + RequestOptions::HTTP_ERRORS => false, + RequestOptions::SYNCHRONOUS => true, + ], + ) + ->wait(); + assert($response instanceof ResponseInterface); + + // @todo check response was successful + + $releaseAssets = Json\typed( + (string) $response->getBody(), + Type\shape( + [ + 'assets' => Type\vec(Type\shape( + [ + 'name' => Type\non_empty_string(), + 'browser_download_url' => Type\non_empty_string(), + ], + true, + )), + ], + true, + ), + ); + + return $releaseAssets['assets']; + } +} diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php new file mode 100644 index 0000000..5872949 --- /dev/null +++ b/src/Downloading/PackageReleaseAssets.php @@ -0,0 +1,14 @@ +selectMatchingReleaseAsset( - $package, - $this->getReleaseAssetsForPackage($package), - ); + $windowsDownloadUrl = $this->packageReleaseAssets->findWindowsDownloadUrlForPackage($package); // @todo extract to a static util $localTempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_downloader_', true); @@ -50,7 +39,7 @@ public function __invoke(Package $package): DownloadedPackage $tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath( AddAuthenticationHeader::withAuthHeaderFromComposer( - new Request('GET', $releaseAsset['browser_download_url']), + new Request('GET', $windowsDownloadUrl), $package, $this->authHelper, ), @@ -61,83 +50,4 @@ public function __invoke(Package $package): DownloadedPackage return DownloadedPackage::fromPackageAndExtractedPath($package, $localTempPath); } - - /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ - // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName - /** - * @param list $releaseAssets - * - * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} - */ - // phpcs:enable - private function selectMatchingReleaseAsset(Package $package, array $releaseAssets): array - { - // @todo source these from the right places... - $arch = 'x86'; - $ts = 'nts'; - $compiler = 'vs16'; - $phpVersion = '8.3'; - $extensionName = str_replace('-', '_', 'example-pie-extension'); - $expectedAssetName = sprintf( - 'php_%s-%s-%s-%s-%s-%s.zip', - $extensionName, - $package->version, - $phpVersion, - $compiler, - $ts, - $arch, - ); - - foreach ($releaseAssets as $releaseAsset) { - if ($releaseAsset['name'] === $expectedAssetName) { - return $releaseAsset; - } - } - - throw new RuntimeException('Could not find release asset for ' . $package->version . ' named: ' . $expectedAssetName); - } - - /** @return list */ - private function getReleaseAssetsForPackage(Package $package): array - { - // @todo dynamic URL, don't hard code it... - // @todo confirm prettyName will always match the repo name - it might not - $request = AddAuthenticationHeader::withAuthHeaderFromComposer( - new Request('GET', 'https://api.github.com/repos/' . $package->name . '/releases/tags/' . $package->version), - $package, - $this->authHelper, - ); - - $response = $this->client - ->sendAsync( - $request, - [ - RequestOptions::ALLOW_REDIRECTS => true, - RequestOptions::HTTP_ERRORS => false, - RequestOptions::SYNCHRONOUS => true, - ], - ) - ->wait(); - assert($response instanceof ResponseInterface); - - // @todo check response was successful - - $releaseAssets = Json\typed( - (string) $response->getBody(), - Type\shape( - [ - 'assets' => Type\vec(Type\shape( - [ - 'name' => Type\non_empty_string(), - 'browser_download_url' => Type\non_empty_string(), - ], - true, - )), - ], - true, - ), - ); - - return $releaseAssets['assets']; - } } diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index def32e0..02ec7af 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -36,7 +36,7 @@ public function testDownloadCommand(): void $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Found package: asgrim/example-pie-extension (version: 1.0.0)', $outputString); + self::assertStringContainsString('Found package: asgrim/example-pie-extension:1.0.0', $outputString); self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString); self::assertStringContainsString('Extracted asgrim/example-pie-extension:1.0.0 source', $outputString); } diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index b052cff..a18ed83 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -20,6 +20,7 @@ public function testFromComposerCompletePackage(): void self::assertSame('foo', $package->name); self::assertSame('1.2.3', $package->version); + self::assertSame('foo:1.2.3', $package->prettyNameAndVersion()); self::assertNull($package->downloadUrl); } } diff --git a/test/unit/Downloading/DownloadZipTest.php b/test/unit/Downloading/DownloadZipWithGuzzleTest.php similarity index 86% rename from test/unit/Downloading/DownloadZipTest.php rename to test/unit/Downloading/DownloadZipWithGuzzleTest.php index a7e0473..ac58dcb 100644 --- a/test/unit/Downloading/DownloadZipTest.php +++ b/test/unit/Downloading/DownloadZipWithGuzzleTest.php @@ -9,7 +9,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use Php\Pie\Downloading\DownloadZip; +use Php\Pie\Downloading\DownloadZipWithGuzzle; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -21,8 +21,8 @@ use const DIRECTORY_SEPARATOR; -#[CoversClass(DownloadZip::class)] -final class DownloadZipTest extends TestCase +#[CoversClass(DownloadZipWithGuzzle::class)] +final class DownloadZipWithGuzzleTest extends TestCase { public function testDownloadZipAndReturnLocalPath(): void { @@ -43,7 +43,7 @@ public function testDownloadZipAndReturnLocalPath(): void $localPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_', true); mkdir($localPath, 0777, true); - $downloadedZipFile = (new DownloadZip($guzzleMockClient)) + $downloadedZipFile = (new DownloadZipWithGuzzle($guzzleMockClient)) ->downloadZipAndReturnLocalPath( new Request('GET', 'http://test-uri/'), $localPath, diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php new file mode 100644 index 0000000..f24da83 --- /dev/null +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -0,0 +1,23 @@ +getMessage()); + } +} diff --git a/test/unit/Downloading/ExtractZipTest.php b/test/unit/Downloading/ExtractZipTest.php index f6d7149..cbd8a3a 100644 --- a/test/unit/Downloading/ExtractZipTest.php +++ b/test/unit/Downloading/ExtractZipTest.php @@ -18,7 +18,6 @@ use const DIRECTORY_SEPARATOR; -/** @covers \Php\Pie\Downloading\ExtractZip */ #[CoversClass(ExtractZip::class)] final class ExtractZipTest extends TestCase { diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php new file mode 100644 index 0000000..82c9c23 --- /dev/null +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -0,0 +1,75 @@ +createMock(AuthHelper::class); + + $mockHandler = new MockHandler([ + new Response( + 200, + [], + json_encode([ + 'assets' => [ + [ + 'name' => 'php_example_pie_extension-1.2.3-8.3-vs16-nts-x86.zip', + 'browser_download_url' => 'actual_download_url', + ], + ], + ]), + ), + ]); + + $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); + + $package = new Package('asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + + $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient); + + self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($package)); + } + + public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void + { + $authHelper = $this->createMock(AuthHelper::class); + + $mockHandler = new MockHandler([ + new Response( + 200, + [], + json_encode([ + 'assets' => [], + ]), + ), + ]); + + $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); + + $package = new Package('asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + + $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient); + + $this->expectException(CouldNotFindReleaseAsset::class); + $releaseAssets->findWindowsDownloadUrlForPackage($package); + } +} diff --git a/test/unit/Downloading/UnixDownloadAndExtractTest.php b/test/unit/Downloading/UnixDownloadAndExtractTest.php new file mode 100644 index 0000000..1f3b240 --- /dev/null +++ b/test/unit/Downloading/UnixDownloadAndExtractTest.php @@ -0,0 +1,55 @@ +createMock(DownloadZip::class); + $extractZip = $this->createMock(ExtractZip::class); + $authHelper = $this->createMock(AuthHelper::class); + $unixDownloadAndExtract = new UnixDownloadAndExtract($downloadZip, $extractZip, $authHelper); + + $tmpZipFile = uniqid('tmpZipFile', true); + $extractedPath = uniqid('extractedPath', true); + + $downloadZip->expects(self::once()) + ->method('downloadZipAndReturnLocalPath') + ->with( + self::isInstanceOf(RequestInterface::class), + self::isType('string'), + ) + ->willReturn($tmpZipFile); + + $extractZip->expects(self::once()) + ->method('to') + ->with( + $tmpZipFile, + self::isType('string'), + ) + ->willReturn($extractedPath); + + $downloadUrl = 'https://test-uri/' . uniqid('downloadUrl', true); + $requestedPackage = new Package('foo/bar', '1.2.3', $downloadUrl); + + $downloadedPackage = $unixDownloadAndExtract->__invoke($requestedPackage); + + self::assertSame($requestedPackage, $downloadedPackage->package); + self::assertSame($extractedPath, $downloadedPackage->extractedSourcePath); + } +} diff --git a/test/unit/Downloading/WindowsDownloadAndExtractTest.php b/test/unit/Downloading/WindowsDownloadAndExtractTest.php new file mode 100644 index 0000000..041dfa7 --- /dev/null +++ b/test/unit/Downloading/WindowsDownloadAndExtractTest.php @@ -0,0 +1,69 @@ +createMock(DownloadZip::class); + $extractZip = $this->createMock(ExtractZip::class); + $authHelper = $this->createMock(AuthHelper::class); + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $windowsDownloadAndExtract = new WindowsDownloadAndExtract( + $downloadZip, + $extractZip, + $authHelper, + $packageReleaseAssets, + ); + + $packageReleaseAssets->expects(self::once()) + ->method('findWindowsDownloadUrlForPackage') + ->with(self::isInstanceOf(Package::class)) + ->willReturn(uniqid('windowsDownloadUrl', true)); + + $tmpZipFile = uniqid('tmpZipFile', true); + $extractedPath = uniqid('extractedPath', true); + + $downloadZip->expects(self::once()) + ->method('downloadZipAndReturnLocalPath') + ->with( + self::isInstanceOf(RequestInterface::class), + self::isType('string'), + ) + ->willReturn($tmpZipFile); + + $extractZip->expects(self::once()) + ->method('to') + ->with( + $tmpZipFile, + self::isType('string'), + ) + ->willReturn($extractedPath); + + $requestedPackage = new Package('foo/bar', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + + $downloadedPackage = $windowsDownloadAndExtract->__invoke($requestedPackage); + + self::assertSame($requestedPackage, $downloadedPackage->package); + self::assertStringContainsString(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pie_downloader_', $downloadedPackage->extractedSourcePath); + } +} From a8eae4617a2b1c1d15f710df013058df752d794e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 11:31:37 +0100 Subject: [PATCH 19/43] Added Windows test execution --- .github/workflows/continuous-integration.yml | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 91ccbe6..fb1d4a1 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -7,3 +7,26 @@ on: jobs: ci: uses: laminas/workflow-continuous-integration/.github/workflows/continuous-integration.yml@1.x + + windows-tests: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: + - windows-latest + php-versions: + - '8.1' + - '8.2' + - '8.3' + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: intl, sodium, zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - uses: ramsey/composer-install@v3 + - name: Run PHPUnit + run: vendor/bin/phpunit From b9bc67657b8c2a17ff833e08c202d7f4a95b864b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 11:41:27 +0100 Subject: [PATCH 20/43] Display skipped test details for CI pipelines --- phpunit.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d81e94a..2808b46 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,6 +7,7 @@ executionOrder="depends,defects" requireCoverageMetadata="true" beStrictAboutOutputDuringTests="true" + displayDetailsOnSkippedTests="true" failOnRisky="true" failOnWarning="true"> From 1b419399902e4aabb5d9cf23510656941f4b65bf Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 12:07:03 +0100 Subject: [PATCH 21/43] Added assertions on response status code --- src/Downloading/AssertHttp.php | 26 +++++++++++++++++++ src/Downloading/DownloadZipWithGuzzle.php | 2 +- .../GithubPackageReleaseAssets.php | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/Downloading/AssertHttp.php diff --git a/src/Downloading/AssertHttp.php b/src/Downloading/AssertHttp.php new file mode 100644 index 0000000..19715ef --- /dev/null +++ b/src/Downloading/AssertHttp.php @@ -0,0 +1,26 @@ +getStatusCode(); + if ($actualStatusCode !== $expectedStatusCode) { + throw new InvalidArgumentException(sprintf( + 'Expected HTTP %d response, got %d - response: %s', + $expectedStatusCode, + $actualStatusCode, + $response->getBody()->__toString(), + )); + } + } +} diff --git a/src/Downloading/DownloadZipWithGuzzle.php b/src/Downloading/DownloadZipWithGuzzle.php index ab1873d..70c4a99 100644 --- a/src/Downloading/DownloadZipWithGuzzle.php +++ b/src/Downloading/DownloadZipWithGuzzle.php @@ -34,7 +34,7 @@ public function downloadZipAndReturnLocalPath(RequestInterface $request, string ->wait(); assert($response instanceof ResponseInterface); - // @todo check response was successful + AssertHttp::responseStatusCode(200, $response); // @todo handle this writing better $tmpZipFile = $localPath . '/downloaded.zip'; diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 497d90d..2526298 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -103,7 +103,7 @@ private function getReleaseAssetsForPackage(Package $package): array ->wait(); assert($response instanceof ResponseInterface); - // @todo check response was successful + AssertHttp::responseStatusCode(200, $response); $releaseAssets = Json\typed( (string) $response->getBody(), From b580dbee3b39c38cbecf8d19e83b28278266e448 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 12:19:32 +0100 Subject: [PATCH 22/43] Handle specifically the release for a tag being missing in GH --- src/Downloading/Exception/CouldNotFindReleaseAsset.php | 8 ++++++++ src/Downloading/GithubPackageReleaseAssets.php | 5 +++++ .../Exception/CouldNotFindReleaseAssetTest.php | 9 +++++++++ 3 files changed, 22 insertions(+) diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php index ec1da61..d89ccc2 100644 --- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -19,4 +19,12 @@ public static function forPackage(Package $package, string $expectedAssetName): $expectedAssetName, )); } + + public static function forPackageWithMissingTag(Package $package): self + { + return new self(sprintf( + 'Could not find release by tag name for %s', + $package->prettyNameAndVersion(), + )); + } } diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 2526298..4bcb0de 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -103,6 +103,11 @@ private function getReleaseAssetsForPackage(Package $package): array ->wait(); assert($response instanceof ResponseInterface); + /** @link https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name */ + if ($response->getStatusCode() === 404) { + throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package); + } + AssertHttp::responseStatusCode(200, $response); $releaseAssets = Json\typed( diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index f24da83..5d557c3 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -20,4 +20,13 @@ public function testForPackage(): void self::assertSame('Could not find release asset for foo/bar:1.2.3 named "something.zip"', $exception->getMessage()); } + + public function testForPackageWithMissingTag(): void + { + $package = new Package('foo/bar', '1.2.3', null); + + $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); + + self::assertSame('Could not find release by tag name for foo/bar:1.2.3', $exception->getMessage()); + } } From 6891cc89f3e63409aeb55a3a6a17a644dff4caf2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 13:11:59 +0100 Subject: [PATCH 23/43] Ensure DownloadCommandTest can download any compatible version of example-pie-extension --- test/integration/Command/DownloadCommandTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 02ec7af..4c067f9 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -24,21 +24,21 @@ public function setUp(): void $this->commandTester = new CommandTester(Container::factory()->get(DownloadCommand::class)); } - public function testDownloadCommand(): void + public function testDownloadCommandWillDownloadCompatibleExtension(): void { if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); } // 1.0.0 is only compatible with PHP 8.3.0 - $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:1.0.0']); + $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:^1.0']); $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Found package: asgrim/example-pie-extension:1.0.0', $outputString); + self::assertStringContainsString('Found package: asgrim/example-pie-extension', $outputString); self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString); - self::assertStringContainsString('Extracted asgrim/example-pie-extension:1.0.0 source', $outputString); + self::assertStringContainsString('Extracted asgrim/example-pie-extension', $outputString); } public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void From 778d231ea42c1420f4d3b303e62cd8c9625f8f4d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 13:21:59 +0100 Subject: [PATCH 24/43] Extracted temp path creation to static helper --- src/Downloading/DownloadZipWithGuzzle.php | 1 - src/Downloading/Path.php | 31 +++++++++++++++++++ src/Downloading/UnixDownloadAndExtract.php | 12 +------ src/Downloading/WindowsDownloadAndExtract.php | 13 +------- 4 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 src/Downloading/Path.php diff --git a/src/Downloading/DownloadZipWithGuzzle.php b/src/Downloading/DownloadZipWithGuzzle.php index 70c4a99..142cf0e 100644 --- a/src/Downloading/DownloadZipWithGuzzle.php +++ b/src/Downloading/DownloadZipWithGuzzle.php @@ -36,7 +36,6 @@ public function downloadZipAndReturnLocalPath(RequestInterface $request, string AssertHttp::responseStatusCode(200, $response); - // @todo handle this writing better $tmpZipFile = $localPath . '/downloaded.zip'; file_put_contents($tmpZipFile, $response->getBody()->__toString()); diff --git a/src/Downloading/Path.php b/src/Downloading/Path.php new file mode 100644 index 0000000..37cf396 --- /dev/null +++ b/src/Downloading/Path.php @@ -0,0 +1,31 @@ +downloadZip->downloadZipAndReturnLocalPath( $this->createRequestForUnixDownloadUrl($package), diff --git a/src/Downloading/WindowsDownloadAndExtract.php b/src/Downloading/WindowsDownloadAndExtract.php index 8ee84e1..affb533 100644 --- a/src/Downloading/WindowsDownloadAndExtract.php +++ b/src/Downloading/WindowsDownloadAndExtract.php @@ -8,13 +8,6 @@ use GuzzleHttp\Psr7\Request; use Php\Pie\DependencyResolver\Package; -use function file_exists; -use function mkdir; -use function sys_get_temp_dir; -use function uniqid; - -use const DIRECTORY_SEPARATOR; - /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class WindowsDownloadAndExtract implements DownloadAndExtract { @@ -31,11 +24,7 @@ public function __invoke(Package $package): DownloadedPackage { $windowsDownloadUrl = $this->packageReleaseAssets->findWindowsDownloadUrlForPackage($package); - // @todo extract to a static util - $localTempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_downloader_', true); - if (! file_exists($localTempPath)) { - mkdir($localTempPath, recursive: true); - } + $localTempPath = Path::vaguelyRandomTempPath(); $tmpZipFile = $this->downloadZip->downloadZipAndReturnLocalPath( AddAuthenticationHeader::withAuthHeaderFromComposer( From dc1e64dd7180bc632ea925da3c12b7d5b0f45404 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 24 Apr 2024 13:54:19 +0100 Subject: [PATCH 25/43] Introduce ExtensionName value object with normalisation --- composer.lock | 54 ++++++++++++------------- src/DependencyResolver/Package.php | 1 + src/ExtensionName.php | 62 +++++++++++++++++++++++++++++ test/unit/ExtensionNameTest.php | 63 ++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 src/ExtensionName.php create mode 100644 test/unit/ExtensionNameTest.php diff --git a/composer.lock b/composer.lock index 952be8e..2010a1d 100644 --- a/composer.lock +++ b/composer.lock @@ -235,12 +235,12 @@ "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "c5ff69ed584449de3dbd01ecc0c145d80fa51fd2" + "reference": "0d5549f503675185a81b57317cd50abedeb64129" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/c5ff69ed584449de3dbd01ecc0c145d80fa51fd2", - "reference": "c5ff69ed584449de3dbd01ecc0c145d80fa51fd2", + "url": "https://api.github.com/repos/composer/composer/zipball/0d5549f503675185a81b57317cd50abedeb64129", + "reference": "0d5549f503675185a81b57317cd50abedeb64129", "shasum": "" }, "require": { @@ -342,7 +342,7 @@ "type": "tidelift" } ], - "time": "2024-04-03T09:05:07+00:00" + "time": "2024-04-22T19:17:04+00:00" }, { "name": "composer/metadata-minifier", @@ -1038,7 +1038,7 @@ }, { "name": "illuminate/container", - "version": "v10.48.7", + "version": "v10.48.9", "source": { "type": "git", "url": "https://github.com/illuminate/container.git", @@ -1089,7 +1089,7 @@ }, { "name": "illuminate/contracts", - "version": "v10.48.7", + "version": "v10.48.9", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -3099,16 +3099,16 @@ }, { "name": "amphp/byte-stream", - "version": "v1.8.1", + "version": "v1.8.2", "source": { "type": "git", "url": "https://github.com/amphp/byte-stream.git", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd" + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd", - "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", "shasum": "" }, "require": { @@ -3124,11 +3124,6 @@ "psalm/phar": "^3.11.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { "files": [ "lib/functions.php" @@ -3152,7 +3147,7 @@ } ], "description": "A stream abstraction to make working with non-blocking I/O simple.", - "homepage": "http://amphp.org/byte-stream", + "homepage": "https://amphp.org/byte-stream", "keywords": [ "amp", "amphp", @@ -3162,9 +3157,8 @@ "stream" ], "support": { - "irc": "irc://irc.freenode.org/amphp", "issues": "https://github.com/amphp/byte-stream/issues", - "source": "https://github.com/amphp/byte-stream/tree/v1.8.1" + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" }, "funding": [ { @@ -3172,7 +3166,7 @@ "type": "github" } ], - "time": "2021-03-30T17:13:30+00:00" + "time": "2024-04-13T18:00:56+00:00" }, { "name": "composer/package-versions-deprecated", @@ -4510,16 +4504,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.17", + "version": "10.5.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5" + "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1f736a473d21957ead7e94fcc029f571895abf5", - "reference": "c1f736a473d21957ead7e94fcc029f571895abf5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/547d314dc24ec1e177720d45c6263fb226cc2ae3", + "reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3", "shasum": "" }, "require": { @@ -4591,7 +4585,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.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.20" }, "funding": [ { @@ -4607,7 +4601,7 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:39:01+00:00" + "time": "2024-04-24T06:32:35+00:00" }, { "name": "psalm/plugin-phpunit", @@ -5715,16 +5709,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.1", + "version": "3.9.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909" + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/267a4405fff1d9c847134db3a3c92f1ab7f77909", - "reference": "267a4405fff1d9c847134db3a3c92f1ab7f77909", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480", + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480", "shasum": "" }, "require": { @@ -5791,7 +5785,7 @@ "type": "open_collective" } ], - "time": "2024-03-31T21:03:09+00:00" + "time": "2024-04-23T20:25:34+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 0551e72..de20ed6 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -25,6 +25,7 @@ public function __construct( public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self { + // @todo extension name from the phpExt.extension-name or package name return new self( $completePackage->getPrettyName(), $completePackage->getPrettyVersion(), diff --git a/src/ExtensionName.php b/src/ExtensionName.php new file mode 100644 index 0000000..2f1ff5f --- /dev/null +++ b/src/ExtensionName.php @@ -0,0 +1,62 @@ +normalisedExtensionName = $normalisedExtensionName; + } + + public static function normaliseFromString(string $extensionName): self + { + if (str_starts_with($extensionName, 'ext-')) { + return new self(substr($extensionName, strlen('ext-'))); + } + + return new self($extensionName); + } + + /** @return non-empty-string */ + public function name(): string + { + return $this->normalisedExtensionName; + } + + /** @return non-empty-string */ + public function nameWithExtPrefix(): string + { + return 'ext-' . $this->normalisedExtensionName; + } +} diff --git a/test/unit/ExtensionNameTest.php b/test/unit/ExtensionNameTest.php new file mode 100644 index 0000000..9d20616 --- /dev/null +++ b/test/unit/ExtensionNameTest.php @@ -0,0 +1,63 @@ + + * + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 + */ + public static function validExtensionNamesProvider(): array + { + return [ + 'ext-sodium' => ['ext-sodium', 'sodium'], + 'sodium' => ['sodium', 'sodium'], + ]; + } + + #[DataProvider('validExtensionNamesProvider')] + public function testValidExtensionNames(string $givenExtensionName, string $expectedNormalisedName): void + { + $extensionName = ExtensionName::normaliseFromString($givenExtensionName); + + self::assertSame($expectedNormalisedName, $extensionName->name()); + self::assertSame('ext-' . $expectedNormalisedName, $extensionName->nameWithExtPrefix()); + } + + /** + * @return array + * + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 + */ + public static function invalidExtensionNamesProvider(): array + { + $invalidExtensionNames = ['', 'kebab-case', 'money$ext']; + + return array_combine($invalidExtensionNames, array_map(static fn ($extensionName) => [$extensionName], $invalidExtensionNames)); + } + + #[DataProvider('invalidExtensionNamesProvider')] + public function testInvalidExtensionNames(string $invalidExtensionName): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(<< Date: Mon, 29 Apr 2024 10:17:10 +0100 Subject: [PATCH 26/43] Determine extension name for a Package --- composer.lock | 8 ++-- src/Command/DownloadCommand.php | 2 +- src/DependencyResolver/Package.php | 4 +- .../ResolveDependencyWithComposer.php | 2 +- src/ExtensionName.php | 25 +++++++++++ test/unit/DependencyResolver/PackageTest.php | 21 ++++++++-- .../ResolveDependencyWithComposerTest.php | 4 +- .../AddAuthenticationHeaderTest.php | 5 ++- .../Downloading/DownloadedPackageTest.php | 3 +- .../CouldNotFindReleaseAssetTest.php | 5 ++- .../GithubPackageReleaseAssetsTest.php | 5 ++- .../UnixDownloadAndExtractTest.php | 3 +- .../WindowsDownloadAndExtractTest.php | 3 +- test/unit/ExtensionNameTest.php | 41 +++++++++++++++++++ 14 files changed, 110 insertions(+), 21 deletions(-) diff --git a/composer.lock b/composer.lock index 2010a1d..3437487 100644 --- a/composer.lock +++ b/composer.lock @@ -235,12 +235,12 @@ "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "0d5549f503675185a81b57317cd50abedeb64129" + "reference": "877f1b150ffdc7b894e392ec05aa4a130fbb40ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/0d5549f503675185a81b57317cd50abedeb64129", - "reference": "0d5549f503675185a81b57317cd50abedeb64129", + "url": "https://api.github.com/repos/composer/composer/zipball/877f1b150ffdc7b894e392ec05aa4a130fbb40ae", + "reference": "877f1b150ffdc7b894e392ec05aa4a130fbb40ae", "shasum": "" }, "require": { @@ -342,7 +342,7 @@ "type": "tidelift" } ], - "time": "2024-04-22T19:17:04+00:00" + "time": "2024-04-29T09:03:27+00:00" }, { "name": "composer/metadata-minifier", diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 4a61d51..9a8c4cd 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -79,7 +79,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersionPair['version'], ); - $output->writeln(sprintf('Found package: %s', $package->prettyNameAndVersion())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); $downloadedPackage = ($this->downloadAndExtract)($package); diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index de20ed6..80f5a6d 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -5,6 +5,7 @@ namespace Php\Pie\DependencyResolver; use Composer\Package\CompletePackageInterface; +use Php\Pie\ExtensionName; /** * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks @@ -17,6 +18,7 @@ final class Package public const TYPE_ZEND_EXTENSION = 'php-ext-zend'; public function __construct( + public readonly ExtensionName $extensionName, public readonly string $name, public readonly string $version, public readonly string|null $downloadUrl, @@ -25,8 +27,8 @@ public function __construct( public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self { - // @todo extension name from the phpExt.extension-name or package name return new self( + ExtensionName::determineFromComposerPackage($completePackage), $completePackage->getPrettyName(), $completePackage->getPrettyVersion(), $completePackage->getDistUrl(), diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 21f487b..5ba3d19 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -27,7 +27,7 @@ public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, stri $this->repositorySet, ($this->resolveTargetPhpToPlatformRepository)($phpBinaryPath), )) - ->findBestCandidate($packageName, $requestedVersion, 'dev', null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); + ->findBestCandidate($packageName, $requestedVersion, 'alpha', null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); diff --git a/src/ExtensionName.php b/src/ExtensionName.php index 2f1ff5f..fc10433 100644 --- a/src/ExtensionName.php +++ b/src/ExtensionName.php @@ -4,9 +4,13 @@ namespace Php\Pie; +use Composer\Package\PackageInterface; use Webmozart\Assert\Assert; +use function array_key_exists; use function assert; +use function explode; +use function is_string; use function str_starts_with; use function strlen; use function substr; @@ -39,6 +43,27 @@ private function __construct(string $normalisedExtensionName) $this->normalisedExtensionName = $normalisedExtensionName; } + public static function determineFromComposerPackage(PackageInterface $package): self + { + $phpExt = $package->getPhpExt(); + + /** @psalm-suppress DocblockTypeContradiction just in case runtime type is not correct */ + if ( + $phpExt === null + || ! array_key_exists('extension-name', $phpExt) + || ! is_string($phpExt['extension-name']) + || $phpExt['extension-name'] === '' + ) { + $packageNameParts = explode('/', $package->getPrettyName()); + Assert::count($packageNameParts, 2, 'Expected a package name like vendor/package for ' . $package->getPrettyName()); + Assert::keyExists($packageNameParts, 1); + + return self::normaliseFromString($packageNameParts[1]); + } + + return self::normaliseFromString($phpExt['extension-name']); + } + public static function normaliseFromString(string $extensionName): self { if (str_starts_with($extensionName, 'ext-')) { diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index a18ed83..1454286 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -15,12 +15,27 @@ final class PackageTest extends TestCase public function testFromComposerCompletePackage(): void { $package = Package::fromComposerCompletePackage( - new CompletePackage('foo', '1.2.3.0', '1.2.3'), + new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'), ); - self::assertSame('foo', $package->name); + self::assertSame('foo', $package->extensionName->name()); + self::assertSame('vendor/foo', $package->name); self::assertSame('1.2.3', $package->version); - self::assertSame('foo:1.2.3', $package->prettyNameAndVersion()); + self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); + self::assertNull($package->downloadUrl); + } + + public function testFromComposerCompletePackageWithExtensionName(): void + { + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + $composerCompletePackage->setPhpExt(['extension-name' => 'ext-something_else']); + + $package = Package::fromComposerCompletePackage($composerCompletePackage); + + self::assertSame('something_else', $package->extensionName->name()); + self::assertSame('vendor/foo', $package->name); + self::assertSame('1.2.3', $package->version); + self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); self::assertNull($package->downloadUrl); } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index ce163a1..2de358e 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -42,10 +42,10 @@ public function testPackageThatCanBeResolved(): void $package = (new ResolveDependencyWithComposer( $this->repositorySet, $this->resolveTargetPhpToPlatformRepository, - ))($phpBinaryPath, 'asgrim/example-pie-extension', '1.0.0'); + ))($phpBinaryPath, 'asgrim/example-pie-extension', '^1.0'); self::assertSame('asgrim/example-pie-extension', $package->name); - self::assertSame('1.0.0', $package->version); + self::assertStringStartsWith('1.', $package->version); } /** diff --git a/test/unit/Downloading/AddAuthenticationHeaderTest.php b/test/unit/Downloading/AddAuthenticationHeaderTest.php index f24b0ee..854773c 100644 --- a/test/unit/Downloading/AddAuthenticationHeaderTest.php +++ b/test/unit/Downloading/AddAuthenticationHeaderTest.php @@ -8,6 +8,7 @@ use GuzzleHttp\Psr7\Request; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\AddAuthenticationHeader; +use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -31,7 +32,7 @@ public function testAuthorizationHeaderIsAdded(): void $requestWithAuthHeader = (new AddAuthenticationHeader())->withAuthHeaderFromComposer( $request, - new Package('foo/bar', '1.2.3', $downloadUrl), + new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', $downloadUrl), $authHelper, ); @@ -47,7 +48,7 @@ public function testExceptionIsThrownWhenPackageDoesNotHaveDownloadUrl(): void $authHelper = $this->createMock(AuthHelper::class); $addAuthenticationHeader = new AddAuthenticationHeader(); - $package = new Package('foo/bar', '1.2.3', null); + $package = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', null); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The package foo/bar does not have a download URL'); diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php index 2d029c8..5ead146 100644 --- a/test/unit/Downloading/DownloadedPackageTest.php +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -6,6 +6,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -16,7 +17,7 @@ final class DownloadedPackageTest extends TestCase { public function testFromPackageAndExtractedPath(): void { - $package = new Package('foo/bar', '1.2.3', null); + $package = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', null); $extractedSourcePath = uniqid('/path/to/downloaded/package', true); diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index 5d557c3..6bc61d4 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -6,6 +6,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; +use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -14,7 +15,7 @@ final class CouldNotFindReleaseAssetTest extends TestCase { public function testForPackage(): void { - $package = new Package('foo/bar', '1.2.3', null); + $package = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', null); $exception = CouldNotFindReleaseAsset::forPackage($package, 'something.zip'); @@ -23,7 +24,7 @@ public function testForPackage(): void public function testForPackageWithMissingTag(): void { - $package = new Package('foo/bar', '1.2.3', null); + $package = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', null); $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 82c9c23..5736506 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -12,6 +12,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\GithubPackageReleaseAssets; +use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -42,7 +43,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); - $package = new Package('asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + $package = new Package(ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient); @@ -65,7 +66,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); - $package = new Package('asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + $package = new Package(ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient); diff --git a/test/unit/Downloading/UnixDownloadAndExtractTest.php b/test/unit/Downloading/UnixDownloadAndExtractTest.php index 1f3b240..888aca4 100644 --- a/test/unit/Downloading/UnixDownloadAndExtractTest.php +++ b/test/unit/Downloading/UnixDownloadAndExtractTest.php @@ -9,6 +9,7 @@ use Php\Pie\Downloading\DownloadZip; use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\UnixDownloadAndExtract; +use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -45,7 +46,7 @@ public function testInvoke(): void ->willReturn($extractedPath); $downloadUrl = 'https://test-uri/' . uniqid('downloadUrl', true); - $requestedPackage = new Package('foo/bar', '1.2.3', $downloadUrl); + $requestedPackage = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', $downloadUrl); $downloadedPackage = $unixDownloadAndExtract->__invoke($requestedPackage); diff --git a/test/unit/Downloading/WindowsDownloadAndExtractTest.php b/test/unit/Downloading/WindowsDownloadAndExtractTest.php index 041dfa7..f5822b7 100644 --- a/test/unit/Downloading/WindowsDownloadAndExtractTest.php +++ b/test/unit/Downloading/WindowsDownloadAndExtractTest.php @@ -10,6 +10,7 @@ use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Downloading\WindowsDownloadAndExtract; +use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -59,7 +60,7 @@ public function testInvoke(): void ) ->willReturn($extractedPath); - $requestedPackage = new Package('foo/bar', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + $requestedPackage = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); $downloadedPackage = $windowsDownloadAndExtract->__invoke($requestedPackage); diff --git a/test/unit/ExtensionNameTest.php b/test/unit/ExtensionNameTest.php index 9d20616..da3e657 100644 --- a/test/unit/ExtensionNameTest.php +++ b/test/unit/ExtensionNameTest.php @@ -4,6 +4,7 @@ namespace Php\PieUnitTest; +use Composer\Package\CompletePackage; use InvalidArgumentException; use Php\Pie\ExtensionName; use PHPUnit\Framework\Attributes\CoversClass; @@ -60,4 +61,44 @@ public function testInvalidExtensionNames(string $invalidExtensionName): void ExtensionName::normaliseFromString($invalidExtensionName); } + + public function testFromComposerPackageWithPhpExtExtensionName(): void + { + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + $composerCompletePackage->setPhpExt(['extension-name' => 'ext-something_else']); + + self::assertSame('something_else', ExtensionName::determineFromComposerPackage($composerCompletePackage)->name()); + } + + public function testFromComposerPackageWithInvalidPhpExtExtensionNameType(): void + { + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + /** @psalm-suppress InvalidArgument - the type says otherwise, but in runtime, this might be possible in theory */ + $composerCompletePackage->setPhpExt(['extension-name' => null]); + + self::assertSame('foo', ExtensionName::determineFromComposerPackage($composerCompletePackage)->name()); + } + + public function testFromComposerPackageWithEmptyPhpExtExtensionName(): void + { + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + $composerCompletePackage->setPhpExt(['extension-name' => '']); + + self::assertSame('foo', ExtensionName::determineFromComposerPackage($composerCompletePackage)->name()); + } + + public function testFromComposerPackageWithoutPhpExtExtensionName(): void + { + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + $composerCompletePackage->setPhpExt(['priority' => 80]); + + self::assertSame('foo', ExtensionName::determineFromComposerPackage($composerCompletePackage)->name()); + } + + public function testFromComposerPackageWithoutPhpExt(): void + { + $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'); + + self::assertSame('foo', ExtensionName::determineFromComposerPackage($composerCompletePackage)->name()); + } } From 4debebb75360f019468311362ad65db3fec8df1f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 29 Apr 2024 10:29:10 +0100 Subject: [PATCH 27/43] Parameterise the GH URL from container --- src/Container.php | 3 +++ src/Downloading/GithubPackageReleaseAssets.php | 4 ++-- test/unit/Downloading/GithubPackageReleaseAssetsTest.php | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Container.php b/src/Container.php index 3839cc7..c9e6529 100644 --- a/src/Container.php +++ b/src/Container.php @@ -95,6 +95,9 @@ static function (ContainerInterface $container): AuthHelper { ); $container->alias(DownloadZipWithGuzzle::class, DownloadZip::class); $container->alias(GithubPackageReleaseAssets::class, PackageReleaseAssets::class); + $container->when(GithubPackageReleaseAssets::class) + ->needs('githubApiBaseUrl') + ->give('https://api.github.com'); $container->singleton( DownloadAndExtract::class, static function (ContainerInterface $container): DownloadAndExtract { diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 4bcb0de..97cb519 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -24,6 +24,7 @@ final class GithubPackageReleaseAssets implements PackageReleaseAssets public function __construct( private readonly AuthHelper $authHelper, private readonly ClientInterface $client, + private readonly string $githubApiBaseUrl, ) { } @@ -83,10 +84,9 @@ private function selectMatchingReleaseAsset(Package $package, array $releaseAsse /** @return list */ private function getReleaseAssetsForPackage(Package $package): array { - // @todo dynamic URL, don't hard code it... // @todo confirm prettyName will always match the repo name - it might not $request = AddAuthenticationHeader::withAuthHeaderFromComposer( - new Request('GET', 'https://api.github.com/repos/' . $package->name . '/releases/tags/' . $package->version), + new Request('GET', $this->githubApiBaseUrl . '/repos/' . $package->name . '/releases/tags/' . $package->version), $package, $this->authHelper, ); diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 5736506..170af58 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -45,7 +45,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $package = new Package(ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); - $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient); + $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation'); self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($package)); } @@ -68,7 +68,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $package = new Package(ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); - $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient); + $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation'); $this->expectException(CouldNotFindReleaseAsset::class); $releaseAssets->findWindowsDownloadUrlForPackage($package); From 0ba54f767bfc384d9409f568530c03c19d179298 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 29 Apr 2024 11:07:20 +0100 Subject: [PATCH 28/43] Move TargetPhp namespace into new Platform namespace --- src/Command/DownloadCommand.php | 2 +- src/Container.php | 2 +- src/DependencyResolver/DependencyResolver.php | 2 +- src/DependencyResolver/ResolveDependencyWithComposer.php | 4 ++-- src/{ => Platform}/TargetPhp/PhpBinaryPath.php | 2 +- .../TargetPhp/ResolveTargetPhpToPlatformRepository.php | 2 +- .../ResolveDependencyWithComposerTest.php | 4 ++-- test/unit/{ => Platform}/TargetPhp/PhpBinaryPathTest.php | 4 ++-- .../TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php | 6 +++--- 9 files changed, 14 insertions(+), 14 deletions(-) rename src/{ => Platform}/TargetPhp/PhpBinaryPath.php (97%) rename src/{ => Platform}/TargetPhp/ResolveTargetPhpToPlatformRepository.php (93%) rename test/unit/{ => Platform}/TargetPhp/PhpBinaryPathTest.php (95%) rename test/unit/{ => Platform}/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php (86%) diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 9a8c4cd..19b35dd 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -8,7 +8,7 @@ use InvalidArgumentException; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\Downloading\DownloadAndExtract; -use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; diff --git a/src/Container.php b/src/Container.php index c9e6529..901cd89 100644 --- a/src/Container.php +++ b/src/Container.php @@ -26,7 +26,7 @@ use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Downloading\UnixDownloadAndExtract; use Php\Pie\Downloading\WindowsDownloadAndExtract; -use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; +use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\ArgvInput; diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index 57a4316..e161980 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -4,7 +4,7 @@ namespace Php\Pie\DependencyResolver; -use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DependencyResolver diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 5ba3d19..e1986f3 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -7,8 +7,8 @@ use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; use Composer\Repository\RepositorySet; -use Php\Pie\TargetPhp\PhpBinaryPath; -use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use function in_array; diff --git a/src/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php similarity index 97% rename from src/TargetPhp/PhpBinaryPath.php rename to src/Platform/TargetPhp/PhpBinaryPath.php index 7c5c1e0..e22b603 100644 --- a/src/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Pie\TargetPhp; +namespace Php\Pie\Platform\TargetPhp; use Composer\Semver\VersionParser; use Symfony\Component\Process\PhpExecutableFinder; diff --git a/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php b/src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php similarity index 93% rename from src/TargetPhp/ResolveTargetPhpToPlatformRepository.php rename to src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php index 93c2ad0..5e4323e 100644 --- a/src/TargetPhp/ResolveTargetPhpToPlatformRepository.php +++ b/src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Pie\TargetPhp; +namespace Php\Pie\Platform\TargetPhp; use Composer\Repository\PlatformRepository; diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 2de358e..5dcd29f 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -10,8 +10,8 @@ use Composer\Repository\RepositorySet; use Php\Pie\DependencyResolver\ResolveDependencyWithComposer; use Php\Pie\DependencyResolver\UnableToResolveRequirement; -use Php\Pie\TargetPhp\PhpBinaryPath; -use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/test/unit/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php similarity index 95% rename from test/unit/TargetPhp/PhpBinaryPathTest.php rename to test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 212a195..6c19fbb 100644 --- a/test/unit/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Php\PieUnitTest\TargetPhp; +namespace Php\PieUnitTest\Platform\TargetPhp; -use Php\Pie\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; diff --git a/test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php b/test/unit/Platform/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php similarity index 86% rename from test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php rename to test/unit/Platform/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php index 70b80a7..49ce656 100644 --- a/test/unit/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php +++ b/test/unit/Platform/TargetPhp/ResolveTargetPhpToPlatformRepositoryTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Php\PieUnitTest\TargetPhp; +namespace Php\PieUnitTest\Platform\TargetPhp; use Composer\Package\BasePackage; -use Php\Pie\TargetPhp\PhpBinaryPath; -use Php\Pie\TargetPhp\ResolveTargetPhpToPlatformRepository; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; From 122327e2dbfb6f41626bfa17d5bb0140939b2c60 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 30 Apr 2024 14:57:54 +0100 Subject: [PATCH 29/43] Added TargetPlatform value object --- src/Command/DownloadCommand.php | 14 ++- src/DependencyResolver/DependencyResolver.php | 4 +- .../ResolveDependencyWithComposer.php | 6 +- src/Platform/Architecture.php | 27 +++++ src/Platform/OperatingSystem.php | 12 +++ src/Platform/TargetPhp/PhpBinaryPath.php | 47 ++++++++ src/Platform/TargetPlatform.php | 102 ++++++++++++++++++ src/Platform/ThreadSafetyMode.php | 12 +++ src/Platform/WindowsCompiler.php | 17 +++ .../ResolveDependencyWithComposerTest.php | 24 ++++- test/unit/Platform/TargetPlatformTest.php | 101 +++++++++++++++++ 11 files changed, 357 insertions(+), 9 deletions(-) create mode 100644 src/Platform/Architecture.php create mode 100644 src/Platform/OperatingSystem.php create mode 100644 src/Platform/TargetPlatform.php create mode 100644 src/Platform/ThreadSafetyMode.php create mode 100644 src/Platform/WindowsCompiler.php create mode 100644 test/unit/Platform/TargetPlatformTest.php diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 19b35dd..3fd76a2 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -9,6 +9,7 @@ use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\Downloading\DownloadAndExtract; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -68,13 +69,22 @@ public function execute(InputInterface $input, OutputInterface $output): int $phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($withPhpConfig); } + $targetPlatform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath); + $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); - $output->writeln(sprintf('Target PHP installation: %s (from %s)', $phpBinaryPath->version(), $phpBinaryPath->phpBinaryPath)); + $output->writeln(sprintf('Target PHP installation: %s (from %s)', $phpBinaryPath->version(), $phpBinaryPath->phpBinaryPath)); + $output->writeln(sprintf( + 'Platform: %s, %s, %s%s', + $targetPlatform->operatingSystem->name, + $targetPlatform->architecture->name, + $targetPlatform->threadSafety->name, + $targetPlatform->windowsCompiler !== null ? ', ' . $targetPlatform->windowsCompiler->name : '', + )); $requestedNameAndVersionPair = $this->requestedNameAndVersionPair($input); $package = ($this->dependencyResolver)( - $phpBinaryPath, + $targetPlatform, $requestedNameAndVersionPair['name'], $requestedNameAndVersionPair['version'], ); diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index e161980..e316091 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -4,11 +4,11 @@ namespace Php\Pie\DependencyResolver; -use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DependencyResolver { /** @throws UnableToResolveRequirement */ - public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, string|null $requestedVersion): Package; + public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package; } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index e1986f3..272c2e2 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -7,8 +7,8 @@ use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionSelector; use Composer\Repository\RepositorySet; -use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPhp\ResolveTargetPhpToPlatformRepository; +use Php\Pie\Platform\TargetPlatform; use function in_array; @@ -21,11 +21,11 @@ public function __construct( ) { } - public function __invoke(PhpBinaryPath $phpBinaryPath, string $packageName, string|null $requestedVersion): Package + public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package { $package = (new VersionSelector( $this->repositorySet, - ($this->resolveTargetPhpToPlatformRepository)($phpBinaryPath), + ($this->resolveTargetPhpToPlatformRepository)($targetPlatform->phpBinaryPath), )) ->findBestCandidate($packageName, $requestedVersion, 'alpha', null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); diff --git a/src/Platform/Architecture.php b/src/Platform/Architecture.php new file mode 100644 index 0000000..99bcb92 --- /dev/null +++ b/src/Platform/Architecture.php @@ -0,0 +1,27 @@ + self::x86_64, + 'arm64' => self::arm64, + default => self::x86, + }; + } +} diff --git a/src/Platform/OperatingSystem.php b/src/Platform/OperatingSystem.php new file mode 100644 index 0000000..4e5caf0 --- /dev/null +++ b/src/Platform/OperatingSystem.php @@ -0,0 +1,12 @@ +phpBinaryPath, + '-r', + 'echo \\defined(\'PHP_WINDOWS_VERSION_BUILD\') ? \'win\' : \'not\';', + ])) + ->mustRun() + ->getOutput()); + Assert::stringNotEmpty($winOrNot, 'Could not determine PHP version'); + + return $winOrNot === 'win' ? OperatingSystem::Windows : OperatingSystem::NonWindows; + } + + /** @return non-empty-string */ public function version(): string { $phpVersion = trim((new Process([ @@ -40,6 +58,35 @@ public function version(): string return $phpVersion; } + public function machineType(): Architecture + { + $phpMachineType = trim((new Process([ + $this->phpBinaryPath, + '-r', + 'echo php_uname("m");', + ])) + ->mustRun() + ->getOutput()); + Assert::stringNotEmpty($phpMachineType, 'Could not determine PHP machine type'); + + return Architecture::parseArchitecture($phpMachineType); + } + + /** @return non-empty-string */ + public function phpinfo(): string + { + $phpInfo = trim((new Process([ + $this->phpBinaryPath, + '-i', + ])) + ->mustRun() + ->getOutput()); + + Assert::stringNotEmpty($phpInfo, sprintf('Could not run phpinfo using %s', $this->phpBinaryPath)); + + return $phpInfo; + } + public static function fromPhpConfigExecutable(string $phpConfig): self { // @todo filter input/sanitize output diff --git a/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php new file mode 100644 index 0000000..1ede19d --- /dev/null +++ b/src/Platform/TargetPlatform.php @@ -0,0 +1,102 @@ +operatingSystem(); + + $phpinfo = $phpBinaryPath->phpinfo(); + + $architecture = $phpBinaryPath->machineType(); + + /** + * Based on xdebug.org wizard, copyright Derick Rethans, used under MIT licence + * + * @link https://github.com/xdebug/xdebug.org/blob/aff649f2c3ca303ad471e6ed9dd29c0db16d3e22/src/XdebugVersion.php#L186-L190 + */ + if ( + preg_match('/Architecture([ =>\t]*)(x[0-9]*)/', $phpinfo, $m) + && array_key_exists(2, $m) + && $m[2] !== '' + ) { + $architecture = Architecture::parseArchitecture($m[2]); + } + + $windowsCompiler = null; + $threadSafety = ThreadSafetyMode::ThreadSafe; + + /** + * Based on xdebug.org wizard, copyright Derick Rethans, used under MIT licence + * + * @link https://github.com/xdebug/xdebug.org/blob/aff649f2c3ca303ad471e6ed9dd29c0db16d3e22/src/XdebugVersion.php#L276-L299 + */ + if (preg_match('/PHP Extension Build([ =>\t]+)(API.*)/', $phpinfo, $m)) { + $parts = explode(',', trim($m[2])); + foreach ($parts as $part) { + switch ($part) { + case 'NTS': + $threadSafety = ThreadSafetyMode::NonThreadSafe; + break; + case 'TS': + $threadSafety = ThreadSafetyMode::ThreadSafe; + break; + case 'VC6': + $windowsCompiler = WindowsCompiler::VC6; + break; + case 'VC8': + $windowsCompiler = WindowsCompiler::VC8; + break; + case 'VC9': + $windowsCompiler = WindowsCompiler::VC9; + break; + case 'VC11': + $windowsCompiler = WindowsCompiler::VC11; + break; + case 'VC14': + $windowsCompiler = WindowsCompiler::VC14; + break; + case 'VC15': + $windowsCompiler = WindowsCompiler::VC15; + break; + case 'VS16': + $windowsCompiler = WindowsCompiler::VS16; + break; + } + } + } + + return new self( + $os, + $phpBinaryPath, + $architecture, + $threadSafety, + $windowsCompiler, + ); + } +} diff --git a/src/Platform/ThreadSafetyMode.php b/src/Platform/ThreadSafetyMode.php new file mode 100644 index 0000000..0e9650c --- /dev/null +++ b/src/Platform/ThreadSafetyMode.php @@ -0,0 +1,12 @@ +method('version') ->willReturn('8.3.0'); + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + null, + ); + $package = (new ResolveDependencyWithComposer( $this->repositorySet, $this->resolveTargetPhpToPlatformRepository, - ))($phpBinaryPath, 'asgrim/example-pie-extension', '^1.0'); + ))($targetPlatform, 'asgrim/example-pie-extension', '^1.0'); self::assertSame('asgrim/example-pie-extension', $package->name); self::assertStringStartsWith('1.', $package->version); @@ -71,13 +83,21 @@ public function testPackageThatCannotBeResolvedThrowsException(array $platformOv ->method('version') ->willReturn($platformOverrides['php']); + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + null, + ); + $this->expectException(UnableToResolveRequirement::class); (new ResolveDependencyWithComposer( $this->repositorySet, $this->resolveTargetPhpToPlatformRepository, ))( - $phpBinaryPath, + $targetPlatform, $package, $version, ); diff --git a/test/unit/Platform/TargetPlatformTest.php b/test/unit/Platform/TargetPlatformTest.php new file mode 100644 index 0000000..5f0c329 --- /dev/null +++ b/test/unit/Platform/TargetPlatformTest.php @@ -0,0 +1,101 @@ +createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('operatingSystem') + ->willReturn(OperatingSystem::Windows); + $phpBinaryPath->expects(self::any()) + ->method('machineType') + ->willReturn(Architecture::x86); + $phpBinaryPath->expects(self::any()) + ->method('phpinfo') + ->willReturn(<<<'TEXT' +phpinfo() +PHP Version => 8.3.6 + +System => Windows NT MYCOMPUTER 10.0 build 19045 (Windows 10) AMD64 +Build Date => Apr 10 2024 14:51:55 +Build System => Microsoft Windows Server 2019 Datacenter [10.0.17763] +Compiler => Visual C++ 2019 +Architecture => x64 +Configure Command => cscript /nologo /e:jscript configure.js "--enable-snapshot-build" "--enable-debug-pack" "--with-pdo-oci=..\..\..\..\instantclient\sdk,shared" "--with-oci8-19=..\..\..\..\instantclient\sdk,shared" "--enable-object-out-dir=../obj/" "--enable-com-dotnet=shared" "--without-analyzer" "--with-pgo" +Server API => Command Line Interface +Virtual Directory Support => enabled +Configuration File (php.ini) Path => +Loaded Configuration File => C:\php-8.3.6\php.ini +Scan this dir for additional .ini files => (none) +Additional .ini files parsed => (none) +PHP API => 20230831 +PHP Extension => 20230831 +Zend Extension => 420230831 +Zend Extension Build => API420230831,TS,VS16 +PHP Extension Build => API20230831,TS,VS16 +TEXT); + + $platform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath); + + self::assertSame(OperatingSystem::Windows, $platform->operatingSystem); + self::assertSame(WindowsCompiler::VS16, $platform->windowsCompiler); + self::assertSame(ThreadSafetyMode::ThreadSafe, $platform->threadSafety); + self::assertSame(Architecture::x86_64, $platform->architecture); + } + + public function testLinuxPlatform(): void + { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('operatingSystem') + ->willReturn(OperatingSystem::NonWindows); + $phpBinaryPath->expects(self::any()) + ->method('machineType') + ->willReturn(Architecture::x86_64); + $phpBinaryPath->expects(self::any()) + ->method('phpinfo') + ->willReturn(<<<'TEXT' +phpinfo() +PHP Version => 8.3.6 + +System => Linux myhostname 1.2.3 Ubuntu x86_64 +Build Date => Apr 11 2024 20:23:38 +Build System => Linux +Server API => Command Line Interface +Virtual Directory Support => disabled +Configuration File (php.ini) Path => /etc/php/8.3/cli +Loaded Configuration File => /etc/php/8.3/cli/php.ini +Scan this dir for additional .ini files => /etc/php/8.3/cli/conf.d +Additional .ini files parsed => (none) +PHP API => 20230831 +PHP Extension => 20230831 +Zend Extension => 420230831 +Zend Extension Build => API420230831,NTS +PHP Extension Build => API20230831,NTS +Debug Build => no +Thread Safety => disabled +TEXT); + + $platform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath); + + self::assertSame(OperatingSystem::NonWindows, $platform->operatingSystem); + self::assertSame(null, $platform->windowsCompiler); + self::assertSame(ThreadSafetyMode::NonThreadSafe, $platform->threadSafety); + self::assertSame(Architecture::x86_64, $platform->architecture); + } +} From 259e121d2c68abd05a55e24b446b01c7df628efd Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 08:31:50 +0100 Subject: [PATCH 30/43] Determine 32-bit/64-bit Intel architecture better --- src/Platform/TargetPhp/PhpBinaryPath.php | 15 +++++++++++++++ src/Platform/TargetPlatform.php | 5 +++++ .../unit/Platform/TargetPhp/PhpBinaryPathTest.php | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index abf772a..3b311b8 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -72,6 +72,21 @@ public function machineType(): Architecture return Architecture::parseArchitecture($phpMachineType); } + public function phpIntSize(): int + { + $phpIntSize = trim((new Process([ + $this->phpBinaryPath, + '-r', + 'echo PHP_INT_SIZE;', + ])) + ->mustRun() + ->getOutput()); + Assert::stringNotEmpty($phpIntSize, 'Could not fetch PHP_INT_SIZE'); + Assert::same($phpIntSize, (string)(int) $phpIntSize, 'PHP_INT_SIZE was not an integer processed %2$s from %s'); + + return (int) $phpIntSize; + } + /** @return non-empty-string */ public function phpinfo(): string { diff --git a/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php index 1ede19d..53b3a07 100644 --- a/src/Platform/TargetPlatform.php +++ b/src/Platform/TargetPlatform.php @@ -35,6 +35,11 @@ public static function fromPhpBinaryPath(PhpBinaryPath $phpBinaryPath): self $architecture = $phpBinaryPath->machineType(); + // If we're not on ARM, a more reliable way of determining 32-bit/64-bit is to use PHP_INT_SIZE + if ($architecture !== Architecture::arm64) { + $architecture = $phpBinaryPath->phpIntSize() === 4 ? Architecture::x86 : Architecture::x86_64; + } + /** * Based on xdebug.org wizard, copyright Derick Rethans, used under MIT licence * diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 6c19fbb..3451aa3 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -50,4 +50,14 @@ public function testFromPhpConfigExecutable(): void $phpBinary->version(), ); } + + public function testPhpIntSize(): void + { + self::assertSame( + PHP_INT_SIZE, + PhpBinaryPath + ::fromCurrentProcess() + ->phpIntSize() + ); + } } From 2df72b36181e8bc7384cbd24024527a8bbcd3d5f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 09:29:13 +0100 Subject: [PATCH 31/43] Use TargetPlatform to determine the correct Windows asset to download --- src/Command/DownloadCommand.php | 3 +- src/Container.php | 2 +- src/Downloading/DownloadAndExtract.php | 3 +- .../Exception/CouldNotFindReleaseAsset.php | 10 ++++++ .../GithubPackageReleaseAssets.php | 33 ++++++++++--------- src/Downloading/PackageReleaseAssets.php | 3 +- src/Downloading/UnixDownloadAndExtract.php | 3 +- src/Downloading/WindowsDownloadAndExtract.php | 5 +-- src/ExtensionName.php | 7 +++- src/Platform/TargetPhp/PhpBinaryPath.php | 20 ++++++++++- src/Platform/ThreadSafetyMode.php | 8 +++++ .../Command/DownloadCommandTest.php | 1 - .../GithubPackageReleaseAssetsTest.php | 28 ++++++++++++++-- .../UnixDownloadAndExtractTest.php | 15 ++++++++- .../WindowsDownloadAndExtractTest.php | 18 ++++++++-- .../Platform/TargetPhp/PhpBinaryPathTest.php | 15 +++++++-- test/unit/Platform/ThreadSafetyModeTest.php | 19 +++++++++++ 17 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 test/unit/Platform/ThreadSafetyModeTest.php diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 3fd76a2..df95454 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -90,9 +90,8 @@ public function execute(InputInterface $input, OutputInterface $output): int ); $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); - $output->writeln(sprintf('Dist download URL: %s', $package->downloadUrl ?? '(none)')); - $downloadedPackage = ($this->downloadAndExtract)($package); + $downloadedPackage = ($this->downloadAndExtract)($targetPlatform, $package); $output->writeln(sprintf( 'Extracted %s source to: %s', diff --git a/src/Container.php b/src/Container.php index 901cd89..9f8133e 100644 --- a/src/Container.php +++ b/src/Container.php @@ -96,7 +96,7 @@ static function (ContainerInterface $container): AuthHelper { $container->alias(DownloadZipWithGuzzle::class, DownloadZip::class); $container->alias(GithubPackageReleaseAssets::class, PackageReleaseAssets::class); $container->when(GithubPackageReleaseAssets::class) - ->needs('githubApiBaseUrl') + ->needs('$githubApiBaseUrl') ->give('https://api.github.com'); $container->singleton( DownloadAndExtract::class, diff --git a/src/Downloading/DownloadAndExtract.php b/src/Downloading/DownloadAndExtract.php index 413a246..f2f2091 100644 --- a/src/Downloading/DownloadAndExtract.php +++ b/src/Downloading/DownloadAndExtract.php @@ -5,9 +5,10 @@ namespace Php\Pie\Downloading; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\TargetPlatform; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface DownloadAndExtract { - public function __invoke(Package $package): DownloadedPackage; + public function __invoke(TargetPlatform $targetPlatform, Package $package): DownloadedPackage; } diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php index d89ccc2..e83aaed 100644 --- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -5,6 +5,7 @@ namespace Php\Pie\Downloading\Exception; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\TargetPlatform; use RuntimeException; use function sprintf; @@ -27,4 +28,13 @@ public static function forPackageWithMissingTag(Package $package): self $package->prettyNameAndVersion(), )); } + + public static function forMissingWindowsCompiler(TargetPlatform $targetPlatform): self + { + return new self(sprintf( + 'Could not determine Windows Compiler for PHP %s on %s', + $targetPlatform->phpBinaryPath->version(), + $targetPlatform->operatingSystem->name, + )); + } } diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 97cb519..365f742 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -9,13 +9,16 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\RequestOptions; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\TargetPlatform; use Psl\Json; use Psl\Type; use Psr\Http\Message\ResponseInterface; use function assert; use function sprintf; -use function str_replace; +use function strtolower; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class GithubPackageReleaseAssets implements PackageReleaseAssets @@ -29,9 +32,10 @@ public function __construct( } /** @return non-empty-string */ - public function findWindowsDownloadUrlForPackage(Package $package): string + public function findWindowsDownloadUrlForPackage(TargetPlatform $targetPlatform, Package $package): string { $releaseAsset = $this->selectMatchingReleaseAsset( + $targetPlatform, $package, $this->getReleaseAssetsForPackage($package), ); @@ -40,23 +44,20 @@ public function findWindowsDownloadUrlForPackage(Package $package): string } /** @return non-empty-string */ - private function expectedWindowsAssetName(Package $package): string + private function expectedWindowsAssetName(TargetPlatform $targetPlatform, Package $package): string { - // @todo source these from the right places... - $arch = 'x86'; - $ts = 'nts'; - $compiler = 'vs16'; - $phpVersion = '8.3'; - $extensionName = str_replace('-', '_', 'example-pie-extension'); + if ($targetPlatform->operatingSystem !== OperatingSystem::Windows || $targetPlatform->windowsCompiler === null) { + throw CouldNotFindReleaseAsset::forMissingWindowsCompiler($targetPlatform); + } return sprintf( 'php_%s-%s-%s-%s-%s-%s.zip', - $extensionName, + $package->extensionName->name(), $package->version, - $phpVersion, - $compiler, - $ts, - $arch, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + strtolower($targetPlatform->windowsCompiler->name), + $targetPlatform->threadSafety->asShort(), + $targetPlatform->architecture->name, ); } @@ -68,9 +69,9 @@ private function expectedWindowsAssetName(Package $package): string * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} */ // phpcs:enable - private function selectMatchingReleaseAsset(Package $package, array $releaseAssets): array + private function selectMatchingReleaseAsset(TargetPlatform $targetPlatform, Package $package, array $releaseAssets): array { - $expectedAssetName = $this->expectedWindowsAssetName($package); + $expectedAssetName = $this->expectedWindowsAssetName($targetPlatform, $package); foreach ($releaseAssets as $releaseAsset) { if ($releaseAsset['name'] === $expectedAssetName) { diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php index 5872949..d39902d 100644 --- a/src/Downloading/PackageReleaseAssets.php +++ b/src/Downloading/PackageReleaseAssets.php @@ -5,10 +5,11 @@ namespace Php\Pie\Downloading; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\TargetPlatform; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface PackageReleaseAssets { /** @return non-empty-string */ - public function findWindowsDownloadUrlForPackage(Package $package): string; + public function findWindowsDownloadUrlForPackage(TargetPlatform $targetPlatform, Package $package): string; } diff --git a/src/Downloading/UnixDownloadAndExtract.php b/src/Downloading/UnixDownloadAndExtract.php index 47a84e2..45ca602 100644 --- a/src/Downloading/UnixDownloadAndExtract.php +++ b/src/Downloading/UnixDownloadAndExtract.php @@ -7,6 +7,7 @@ use Composer\Util\AuthHelper; use GuzzleHttp\Psr7\Request; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\TargetPlatform; use Psr\Http\Message\RequestInterface; use RuntimeException; @@ -23,7 +24,7 @@ public function __construct( ) { } - public function __invoke(Package $package): DownloadedPackage + public function __invoke(TargetPlatform $targetPlatform, Package $package): DownloadedPackage { $localTempPath = Path::vaguelyRandomTempPath(); diff --git a/src/Downloading/WindowsDownloadAndExtract.php b/src/Downloading/WindowsDownloadAndExtract.php index affb533..fd317e9 100644 --- a/src/Downloading/WindowsDownloadAndExtract.php +++ b/src/Downloading/WindowsDownloadAndExtract.php @@ -7,6 +7,7 @@ use Composer\Util\AuthHelper; use GuzzleHttp\Psr7\Request; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\TargetPlatform; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class WindowsDownloadAndExtract implements DownloadAndExtract @@ -20,9 +21,9 @@ public function __construct( ) { } - public function __invoke(Package $package): DownloadedPackage + public function __invoke(TargetPlatform $targetPlatform, Package $package): DownloadedPackage { - $windowsDownloadUrl = $this->packageReleaseAssets->findWindowsDownloadUrlForPackage($package); + $windowsDownloadUrl = $this->packageReleaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package); $localTempPath = Path::vaguelyRandomTempPath(); diff --git a/src/ExtensionName.php b/src/ExtensionName.php index fc10433..afe876b 100644 --- a/src/ExtensionName.php +++ b/src/ExtensionName.php @@ -22,7 +22,12 @@ */ final class ExtensionName { - /** @link https://github.com/pear/pear-core/blob/6f4c3a0b134626d238d75a44af01a2f7c4e688d9/PEAR/Common.php#L28 */ + /** + * PECL extension names must contain only alphanumeric characters and underscores, and must start with an + * alphabetical character. PIE does not change this requirement for consistency. + * + * @link https://github.com/pear/pear-core/blob/6f4c3a0b134626d238d75a44af01a2f7c4e688d9/PEAR/Common.php#L28 + */ private const VALID_PACKAGE_NAME_REGEX = '#^[A-Za-z][a-zA-Z0-9_]+$#'; // phpcs:disable SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion.RequiredConstructorPropertyPromotion diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 3b311b8..78a3962 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -58,6 +58,24 @@ public function version(): string return $phpVersion; } + /** @return non-empty-string */ + public function majorMinorVersion(): string + { + $phpVersion = trim((new Process([ + $this->phpBinaryPath, + '-r', + 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;', + ])) + ->mustRun() + ->getOutput()); + Assert::stringNotEmpty($phpVersion, 'Could not determine PHP version'); + + // normalizing the version will throw an exception if it is not a valid version + (new VersionParser())->normalize($phpVersion); + + return $phpVersion; + } + public function machineType(): Architecture { $phpMachineType = trim((new Process([ @@ -82,7 +100,7 @@ public function phpIntSize(): int ->mustRun() ->getOutput()); Assert::stringNotEmpty($phpIntSize, 'Could not fetch PHP_INT_SIZE'); - Assert::same($phpIntSize, (string)(int) $phpIntSize, 'PHP_INT_SIZE was not an integer processed %2$s from %s'); + Assert::same($phpIntSize, (string) (int) $phpIntSize, 'PHP_INT_SIZE was not an integer processed %2$s from %s'); return (int) $phpIntSize; } diff --git a/src/Platform/ThreadSafetyMode.php b/src/Platform/ThreadSafetyMode.php index 0e9650c..f5fd923 100644 --- a/src/Platform/ThreadSafetyMode.php +++ b/src/Platform/ThreadSafetyMode.php @@ -9,4 +9,12 @@ enum ThreadSafetyMode { case ThreadSafe; case NonThreadSafe; + + public function asShort(): string + { + return match ($this) { + self::ThreadSafe => 'ts', + self::NonThreadSafe => 'nts', + }; + } } diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 4c067f9..e8dd2b0 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -37,7 +37,6 @@ public function testDownloadCommandWillDownloadCompatibleExtension(): void $outputString = $this->commandTester->getDisplay(); self::assertStringContainsString('Found package: asgrim/example-pie-extension', $outputString); - self::assertStringContainsString('Dist download URL: https://api.github.com/repos/asgrim/example-pie-extension/zipball/', $outputString); self::assertStringContainsString('Extracted asgrim/example-pie-extension', $outputString); } diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 170af58..0cf4bcc 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -13,6 +13,12 @@ use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\Downloading\GithubPackageReleaseAssets; use Php\Pie\ExtensionName; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\ThreadSafetyMode; +use Php\Pie\Platform\WindowsCompiler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -24,6 +30,14 @@ final class GithubPackageReleaseAssetsTest extends TestCase { public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void { + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86, + ThreadSafetyMode::ThreadSafe, + WindowsCompiler::VC14, + ); + $authHelper = $this->createMock(AuthHelper::class); $mockHandler = new MockHandler([ @@ -33,7 +47,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void json_encode([ 'assets' => [ [ - 'name' => 'php_example_pie_extension-1.2.3-8.3-vs16-nts-x86.zip', + 'name' => 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', 'browser_download_url' => 'actual_download_url', ], ], @@ -47,11 +61,19 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation'); - self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($package)); + self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package)); } public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void { + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86, + ThreadSafetyMode::ThreadSafe, + WindowsCompiler::VC14, + ); + $authHelper = $this->createMock(AuthHelper::class); $mockHandler = new MockHandler([ @@ -71,6 +93,6 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation'); $this->expectException(CouldNotFindReleaseAsset::class); - $releaseAssets->findWindowsDownloadUrlForPackage($package); + $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package); } } diff --git a/test/unit/Downloading/UnixDownloadAndExtractTest.php b/test/unit/Downloading/UnixDownloadAndExtractTest.php index 888aca4..f0bc4da 100644 --- a/test/unit/Downloading/UnixDownloadAndExtractTest.php +++ b/test/unit/Downloading/UnixDownloadAndExtractTest.php @@ -10,6 +10,11 @@ use Php\Pie\Downloading\ExtractZip; use Php\Pie\Downloading\UnixDownloadAndExtract; use Php\Pie\ExtensionName; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\ThreadSafetyMode; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -21,6 +26,14 @@ final class UnixDownloadAndExtractTest extends TestCase { public function testInvoke(): void { + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86, + ThreadSafetyMode::NonThreadSafe, + null, + ); + $downloadZip = $this->createMock(DownloadZip::class); $extractZip = $this->createMock(ExtractZip::class); $authHelper = $this->createMock(AuthHelper::class); @@ -48,7 +61,7 @@ public function testInvoke(): void $downloadUrl = 'https://test-uri/' . uniqid('downloadUrl', true); $requestedPackage = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', $downloadUrl); - $downloadedPackage = $unixDownloadAndExtract->__invoke($requestedPackage); + $downloadedPackage = $unixDownloadAndExtract->__invoke($targetPlatform, $requestedPackage); self::assertSame($requestedPackage, $downloadedPackage->package); self::assertSame($extractedPath, $downloadedPackage->extractedSourcePath); diff --git a/test/unit/Downloading/WindowsDownloadAndExtractTest.php b/test/unit/Downloading/WindowsDownloadAndExtractTest.php index f5822b7..448bdd5 100644 --- a/test/unit/Downloading/WindowsDownloadAndExtractTest.php +++ b/test/unit/Downloading/WindowsDownloadAndExtractTest.php @@ -11,6 +11,12 @@ use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Downloading\WindowsDownloadAndExtract; use Php\Pie\ExtensionName; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\ThreadSafetyMode; +use Php\Pie\Platform\WindowsCompiler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -25,6 +31,14 @@ final class WindowsDownloadAndExtractTest extends TestCase { public function testInvoke(): void { + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86, + ThreadSafetyMode::ThreadSafe, + WindowsCompiler::VC14, + ); + $downloadZip = $this->createMock(DownloadZip::class); $extractZip = $this->createMock(ExtractZip::class); $authHelper = $this->createMock(AuthHelper::class); @@ -38,7 +52,7 @@ public function testInvoke(): void $packageReleaseAssets->expects(self::once()) ->method('findWindowsDownloadUrlForPackage') - ->with(self::isInstanceOf(Package::class)) + ->with($targetPlatform, self::isInstanceOf(Package::class)) ->willReturn(uniqid('windowsDownloadUrl', true)); $tmpZipFile = uniqid('tmpZipFile', true); @@ -62,7 +76,7 @@ public function testInvoke(): void $requestedPackage = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); - $downloadedPackage = $windowsDownloadAndExtract->__invoke($requestedPackage); + $downloadedPackage = $windowsDownloadAndExtract->__invoke($targetPlatform, $requestedPackage); self::assertSame($requestedPackage, $downloadedPackage->package); self::assertStringContainsString(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'pie_downloader_', $downloadedPackage->extractedSourcePath); diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 3451aa3..1469412 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -14,6 +14,7 @@ use function sprintf; use function trim; +use const PHP_INT_SIZE; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; use const PHP_RELEASE_VERSION; @@ -51,13 +52,21 @@ public function testFromPhpConfigExecutable(): void ); } + public function testMajorMinorVersion(): void + { + self::assertSame( + PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, + PhpBinaryPath::fromCurrentProcess() + ->majorMinorVersion(), + ); + } + public function testPhpIntSize(): void { self::assertSame( PHP_INT_SIZE, - PhpBinaryPath - ::fromCurrentProcess() - ->phpIntSize() + PhpBinaryPath::fromCurrentProcess() + ->phpIntSize(), ); } } diff --git a/test/unit/Platform/ThreadSafetyModeTest.php b/test/unit/Platform/ThreadSafetyModeTest.php new file mode 100644 index 0000000..712db06 --- /dev/null +++ b/test/unit/Platform/ThreadSafetyModeTest.php @@ -0,0 +1,19 @@ +asShort()); + self::assertSame('nts', ThreadSafetyMode::NonThreadSafe->asShort()); + } +} From 812a8143f511156fd320c107a86490d9f41b36ed Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 09:40:10 +0100 Subject: [PATCH 32/43] Support both compiler-ts and ts-compiler naming --- .../Exception/CouldNotFindReleaseAsset.php | 8 ++-- .../GithubPackageReleaseAssets.php | 44 +++++++++++++------ .../CouldNotFindReleaseAssetTest.php | 4 +- .../GithubPackageReleaseAssetsTest.php | 44 +++++++++++++++++++ 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php index e83aaed..528529d 100644 --- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php +++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php @@ -8,16 +8,18 @@ use Php\Pie\Platform\TargetPlatform; use RuntimeException; +use function implode; use function sprintf; class CouldNotFindReleaseAsset extends RuntimeException { - public static function forPackage(Package $package, string $expectedAssetName): self + /** @param non-empty-list $expectedAssetNames */ + public static function forPackage(Package $package, array $expectedAssetNames): self { return new self(sprintf( - 'Could not find release asset for %s named "%s"', + 'Could not find release asset for %s named one of "%s"', $package->prettyNameAndVersion(), - $expectedAssetName, + implode(', ', $expectedAssetNames), )); } diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 365f742..0d9bd70 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -17,6 +17,7 @@ use Psr\Http\Message\ResponseInterface; use function assert; +use function in_array; use function sprintf; use function strtolower; @@ -43,22 +44,37 @@ public function findWindowsDownloadUrlForPackage(TargetPlatform $targetPlatform, return $releaseAsset['browser_download_url']; } - /** @return non-empty-string */ - private function expectedWindowsAssetName(TargetPlatform $targetPlatform, Package $package): string + /** @return non-empty-list */ + private function expectedWindowsAssetNames(TargetPlatform $targetPlatform, Package $package): array { if ($targetPlatform->operatingSystem !== OperatingSystem::Windows || $targetPlatform->windowsCompiler === null) { throw CouldNotFindReleaseAsset::forMissingWindowsCompiler($targetPlatform); } - return sprintf( - 'php_%s-%s-%s-%s-%s-%s.zip', - $package->extensionName->name(), - $package->version, - $targetPlatform->phpBinaryPath->majorMinorVersion(), - strtolower($targetPlatform->windowsCompiler->name), - $targetPlatform->threadSafety->asShort(), - $targetPlatform->architecture->name, - ); + /** + * During development, we swapped compiler/ts around. It is fairly trivial to support both, so we can check + * both formats pretty easily, just to avoid confusion for package maintainers... + */ + return [ + strtolower(sprintf( + 'php_%s-%s-%s-%s-%s-%s.zip', + $package->extensionName->name(), + $package->version, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + $targetPlatform->threadSafety->asShort(), + strtolower($targetPlatform->windowsCompiler->name), + $targetPlatform->architecture->name, + )), + strtolower(sprintf( + 'php_%s-%s-%s-%s-%s-%s.zip', + $package->extensionName->name(), + $package->version, + $targetPlatform->phpBinaryPath->majorMinorVersion(), + strtolower($targetPlatform->windowsCompiler->name), + $targetPlatform->threadSafety->asShort(), + $targetPlatform->architecture->name, + )), + ]; } /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ @@ -71,15 +87,15 @@ private function expectedWindowsAssetName(TargetPlatform $targetPlatform, Packag // phpcs:enable private function selectMatchingReleaseAsset(TargetPlatform $targetPlatform, Package $package, array $releaseAssets): array { - $expectedAssetName = $this->expectedWindowsAssetName($targetPlatform, $package); + $expectedAssetNames = $this->expectedWindowsAssetNames($targetPlatform, $package); foreach ($releaseAssets as $releaseAsset) { - if ($releaseAsset['name'] === $expectedAssetName) { + if (in_array(strtolower($releaseAsset['name']), $expectedAssetNames, true)) { return $releaseAsset; } } - throw Exception\CouldNotFindReleaseAsset::forPackage($package, $expectedAssetName); + throw Exception\CouldNotFindReleaseAsset::forPackage($package, $expectedAssetNames); } /** @return list */ diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index 6bc61d4..a73c47b 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -17,9 +17,9 @@ public function testForPackage(): void { $package = new Package(ExtensionName::normaliseFromString('foo'), 'foo/bar', '1.2.3', null); - $exception = CouldNotFindReleaseAsset::forPackage($package, 'something.zip'); + $exception = CouldNotFindReleaseAsset::forPackage($package, ['something.zip', 'something2.zip']); - self::assertSame('Could not find release asset for foo/bar:1.2.3 named "something.zip"', $exception->getMessage()); + self::assertSame('Could not find release asset for foo/bar:1.2.3 named one of "something.zip, something2.zip"', $exception->getMessage()); } public function testForPackageWithMissingTag(): void diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 0cf4bcc..241634f 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -46,6 +46,10 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void [], json_encode([ 'assets' => [ + [ + 'name' => 'php_foo-1.2.3-8.3-vc14-nts-x86.zip', + 'browser_download_url' => 'wrong_download_url', + ], [ 'name' => 'php_foo-1.2.3-8.3-vc14-ts-x86.zip', 'browser_download_url' => 'actual_download_url', @@ -64,6 +68,46 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package)); } + public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThreadSafetySwapped(): void + { + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86, + ThreadSafetyMode::ThreadSafe, + WindowsCompiler::VC14, + ); + + $authHelper = $this->createMock(AuthHelper::class); + + $mockHandler = new MockHandler([ + new Response( + 200, + [], + json_encode([ + 'assets' => [ + [ + 'name' => 'php_foo-1.2.3-8.3-nts-vc14-x86.zip', + 'browser_download_url' => 'wrong_download_url', + ], + [ + 'name' => 'php_foo-1.2.3-8.3-ts-vc14-x86.zip', + 'browser_download_url' => 'actual_download_url', + ], + ], + ]), + ), + ]); + + $guzzleMockClient = new Client(['handler' => HandlerStack::create($mockHandler)]); + + $package = new Package(ExtensionName::normaliseFromString('foo'), 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true)); + + $releaseAssets = new GithubPackageReleaseAssets($authHelper, $guzzleMockClient, 'https://test-github-api-base-url.thephp.foundation'); + + self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package)); + } + public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void { $targetPlatform = new TargetPlatform( From 9629815ff268240b06d5830d0f2180127d3c9c23 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 10:01:16 +0100 Subject: [PATCH 33/43] Test coverage improvement --- src/Container.php | 1 + src/Downloading/AssertHttp.php | 2 + src/Downloading/Path.php | 8 +++- test/unit/Downloading/AssertHttpTest.php | 24 ++++++++++++ .../CouldNotFindReleaseAssetTest.php | 19 ++++++++++ test/unit/Downloading/PathTest.php | 21 ++++++++++ test/unit/Platform/ArchitectureTest.php | 38 +++++++++++++++++++ .../Platform/TargetPhp/PhpBinaryPathTest.php | 25 ++++++++++++ 8 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 test/unit/Downloading/AssertHttpTest.php create mode 100644 test/unit/Downloading/PathTest.php create mode 100644 test/unit/Platform/ArchitectureTest.php diff --git a/src/Container.php b/src/Container.php index 9f8133e..c8e8c8f 100644 --- a/src/Container.php +++ b/src/Container.php @@ -34,6 +34,7 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class Container { public static function factory(): ContainerInterface diff --git a/src/Downloading/AssertHttp.php b/src/Downloading/AssertHttp.php index 19715ef..d4639d0 100644 --- a/src/Downloading/AssertHttp.php +++ b/src/Downloading/AssertHttp.php @@ -9,8 +9,10 @@ use function sprintf; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class AssertHttp { + /** Helper assertion that includes the HTTP response body when the HTTP status code does not match */ public static function responseStatusCode(int $expectedStatusCode, ResponseInterface $response): void { $actualStatusCode = $response->getStatusCode(); diff --git a/src/Downloading/Path.php b/src/Downloading/Path.php index 37cf396..06151aa 100644 --- a/src/Downloading/Path.php +++ b/src/Downloading/Path.php @@ -13,9 +13,15 @@ use const DIRECTORY_SEPARATOR; +/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class Path { - /** @return non-empty-string */ + /** + * Static helper to generate a vaguely random temporary path. This is not intended to be cryptographically secure, + * nor do we need to support high concurrency or strong randomness. + * + * @return non-empty-string + */ public static function vaguelyRandomTempPath(): string { $localTempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_downloader_', true); diff --git a/test/unit/Downloading/AssertHttpTest.php b/test/unit/Downloading/AssertHttpTest.php new file mode 100644 index 0000000..7568b51 --- /dev/null +++ b/test/unit/Downloading/AssertHttpTest.php @@ -0,0 +1,24 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected HTTP 201 response, got 404 - response: some body content'); + AssertHttp::responseStatusCode(201, $response); + } +} diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index a73c47b..b03a335 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -7,6 +7,11 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset; use Php\Pie\ExtensionName; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\ThreadSafetyMode; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -30,4 +35,18 @@ public function testForPackageWithMissingTag(): void self::assertSame('Could not find release by tag name for foo/bar:1.2.3', $exception->getMessage()); } + + public function testForMissingWindowsCompiler(): void + { + $phpBinary = PhpBinaryPath::fromCurrentProcess(); + $exception = CouldNotFindReleaseAsset::forMissingWindowsCompiler(new TargetPlatform( + OperatingSystem::NonWindows, + $phpBinary, + Architecture::x86, + ThreadSafetyMode::NonThreadSafe, + null, + )); + + self::assertSame('Could not determine Windows Compiler for PHP ' . $phpBinary->version() . ' on NonWindows', $exception->getMessage()); + } } diff --git a/test/unit/Downloading/PathTest.php b/test/unit/Downloading/PathTest.php new file mode 100644 index 0000000..8797f86 --- /dev/null +++ b/test/unit/Downloading/PathTest.php @@ -0,0 +1,21 @@ + + * + * @psalm-suppress PossiblyUnusedMethod + */ + public static function architectureMapProvider(): array + { + return [ + 'x64' => ['x64', Architecture::x86_64], + 'x86_64' => ['x86_64', Architecture::x86_64], + 'AMD64' => ['AMD64', Architecture::x86_64], + 'arm64' => ['arm64', Architecture::arm64], + 'x86' => ['x86', Architecture::x86], + 'something' => ['something', Architecture::x86], + ]; + } + + /** @param non-empty-string $architectureString */ + #[DataProvider('architectureMapProvider')] + public function testParseArchitecture(string $architectureString, Architecture $expectedArchitecture): void + { + self::assertSame($expectedArchitecture, Architecture::parseArchitecture($architectureString)); + } +} diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 1469412..dde65d6 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -4,13 +4,18 @@ namespace Php\PieUnitTest\Platform\TargetPhp; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +use function assert; +use function defined; use function file_exists; use function is_executable; +use function php_uname; use function sprintf; use function trim; @@ -52,6 +57,15 @@ public function testFromPhpConfigExecutable(): void ); } + public function testOperatingSystem(): void + { + self::assertSame( + defined('PHP_WINDOWS_VERSION_BUILD') ? OperatingSystem::Windows : OperatingSystem::NonWindows, + PhpBinaryPath::fromCurrentProcess() + ->operatingSystem(), + ); + } + public function testMajorMinorVersion(): void { self::assertSame( @@ -61,6 +75,17 @@ public function testMajorMinorVersion(): void ); } + public function testMachineType(): void + { + $myUnameMachineType = php_uname('m'); + assert($myUnameMachineType !== ''); + self::assertSame( + Architecture::parseArchitecture($myUnameMachineType), + PhpBinaryPath::fromCurrentProcess() + ->machineType(), + ); + } + public function testPhpIntSize(): void { self::assertSame( From 9be752b89711a53ff52a32257c7bef722fee438b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 10:08:32 +0100 Subject: [PATCH 34/43] Do not use phpBinaryPath from current process where a fixed name is needed in the test --- .../Downloading/GithubPackageReleaseAssetsTest.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 241634f..7c7efd4 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -30,9 +30,14 @@ final class GithubPackageReleaseAssetsTest extends TestCase { public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('majorMinorVersion') + ->willReturn('8.3'); + $targetPlatform = new TargetPlatform( OperatingSystem::Windows, - PhpBinaryPath::fromCurrentProcess(), + $phpBinaryPath, Architecture::x86, ThreadSafetyMode::ThreadSafe, WindowsCompiler::VC14, @@ -70,9 +75,14 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThreadSafetySwapped(): void { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::any()) + ->method('majorMinorVersion') + ->willReturn('8.3'); + $targetPlatform = new TargetPlatform( OperatingSystem::Windows, - PhpBinaryPath::fromCurrentProcess(), + $phpBinaryPath, Architecture::x86, ThreadSafetyMode::ThreadSafe, WindowsCompiler::VC14, From d6dd3baf67aaf4e6e3bc15400a339b5b447dbd2a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 12:31:24 +0100 Subject: [PATCH 35/43] Added more test cases for various version constraints to download command --- .../ResolveDependencyWithComposer.php | 12 +++- .../Command/DownloadCommandTest.php | 70 +++++++++++++++++-- .../ResolveDependencyWithComposerTest.php | 2 +- test/unit/Platform/ArchitectureTest.php | 2 +- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 272c2e2..23c199a 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -11,6 +11,7 @@ use Php\Pie\Platform\TargetPlatform; use function in_array; +use function preg_match; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ final class ResolveDependencyWithComposer implements DependencyResolver @@ -23,11 +24,20 @@ public function __construct( public function __invoke(TargetPlatform $targetPlatform, string $packageName, string|null $requestedVersion): Package { + $preferredStability = 'stable'; + $repoSetFlags = 0; + + /** Stability options from {@see https://getcomposer.org/doc/04-schema.md#minimum-stability} */ + if ($requestedVersion !== null && preg_match('#@(dev|alpha|beta|RC|stable)$#', $requestedVersion, $matches)) { + $preferredStability = $matches[1]; + $repoSetFlags |= RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES; + } + $package = (new VersionSelector( $this->repositorySet, ($this->resolveTargetPhpToPlatformRepository)($targetPlatform->phpBinaryPath), )) - ->findBestCandidate($packageName, $requestedVersion, 'alpha', null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); + ->findBestCandidate($packageName, $requestedVersion, $preferredStability, null, $repoSetFlags); if (! $package instanceof CompletePackageInterface) { throw UnableToResolveRequirement::fromRequirement($packageName, $requestedVersion); diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index e8dd2b0..1fadeeb 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -8,15 +8,23 @@ use Php\Pie\Container; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; +use function array_combine; +use function array_map; +use function file_exists; +use function is_executable; + use const PHP_VERSION; use const PHP_VERSION_ID; #[CoversClass(DownloadCommand::class)] class DownloadCommandTest extends TestCase { + private const TEST_PACKAGE = 'asgrim/example-pie-extension'; + private CommandTester $commandTester; public function setUp(): void @@ -24,20 +32,70 @@ public function setUp(): void $this->commandTester = new CommandTester(Container::factory()->get(DownloadCommand::class)); } - public function testDownloadCommandWillDownloadCompatibleExtension(): void + /** + * @return array + * + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 + */ + public static function validVersionsList(): array + { + $versionsAndExpected = [ + [self::TEST_PACKAGE, self::TEST_PACKAGE . ':1.0.1'], + [self::TEST_PACKAGE . ':^1.0', self::TEST_PACKAGE . ':1.0.1'], + [self::TEST_PACKAGE . ':1.0.1-alpha.3@alpha', self::TEST_PACKAGE . ':1.0.1-alpha.3'], + [self::TEST_PACKAGE . ':*', self::TEST_PACKAGE . ':1.0.1'], + [self::TEST_PACKAGE . ':~1.0.0@alpha', self::TEST_PACKAGE . ':1.0.1'], + [self::TEST_PACKAGE . ':^1.1.0@alpha', self::TEST_PACKAGE . ':1.1.0-alpha.1'], + [self::TEST_PACKAGE . ':~1.0.0', self::TEST_PACKAGE . ':1.0.1'], + // @todo in theory, these could work, on NonWindows at least + //[self::TEST_PACKAGE . ':dev-master', self::TEST_PACKAGE . ':???'], + //[self::TEST_PACKAGE . ':dev-master#769f906413d6d1e12152f6d34134cbcd347ca253', self::TEST_PACKAGE . ':???'], + ]; + + return array_combine( + array_map(static fn ($item) => $item[0], $versionsAndExpected), + $versionsAndExpected, + ); + } + + #[DataProvider('validVersionsList')] + public function testDownloadCommandWillDownloadCompatibleExtension(string $requestedVersion, string $expectedVersion): void { if (PHP_VERSION_ID < 80300 || PHP_VERSION_ID >= 80400) { self::markTestSkipped('This test can only run on PHP 8.3 - you are running ' . PHP_VERSION); } - // 1.0.0 is only compatible with PHP 8.3.0 - $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:^1.0']); + $this->commandTester->execute(['requested-package-and-version' => $requestedVersion]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Found package: ' . $expectedVersion . ' which provides', $outputString); + self::assertStringContainsString('Extracted ' . $expectedVersion . ' source to', $outputString); + } + + #[DataProvider('validVersionsList')] + public function testDownloadingWithPhpConfig(string $requestedVersion, string $expectedVersion): void + { + // @todo This test makes an assumption you're using `ppa:ondrej/php` to have multiple PHP versions. This allows + // us to test scenarios where you run with PHP 8.1 but want to install to a PHP 8.3 instance, for example. + // However, this test isn't very portable, and won't run in CI, so we could do with improving this later. + $phpConfigPath = '/usr/bin/php-config8.3'; + + if (! file_exists($phpConfigPath) || ! is_executable($phpConfigPath)) { + self::markTestSkipped('This test can only run where "' . $phpConfigPath . '" exists and is executable, to target PHP 8.3'); + } + + $this->commandTester->execute([ + '--with-php-config' => $phpConfigPath, + 'requested-package-and-version' => $requestedVersion, + ]); $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Found package: asgrim/example-pie-extension', $outputString); - self::assertStringContainsString('Extracted asgrim/example-pie-extension', $outputString); + self::assertStringContainsString('Found package: ' . $expectedVersion . ' which provides', $outputString); + self::assertStringContainsString('Extracted ' . $expectedVersion . ' source to', $outputString); } public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void @@ -48,6 +106,6 @@ public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void $this->expectException(UnableToResolveRequirement::class); // 1.0.0 is only compatible with PHP 8.3.0 - $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:1.0.0']); + $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE . ':1.0.0']); } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 0006a50..604911a 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -63,7 +63,7 @@ public function testPackageThatCanBeResolved(): void /** * @return array, 1: string, 2: string}> * - * @psalm-suppress PossiblyUnusedMethod + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 */ public static function unresolvableDependencies(): array { diff --git a/test/unit/Platform/ArchitectureTest.php b/test/unit/Platform/ArchitectureTest.php index 97ec8e4..d785786 100644 --- a/test/unit/Platform/ArchitectureTest.php +++ b/test/unit/Platform/ArchitectureTest.php @@ -15,7 +15,7 @@ final class ArchitectureTest extends TestCase /** * @return array * - * @psalm-suppress PossiblyUnusedMethod + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 */ public static function architectureMapProvider(): array { From d8b03136271cddc6ce249f67bec73f6746b347c3 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 16 May 2024 12:47:44 +0100 Subject: [PATCH 36/43] Added basic docs for download command --- README.md | 24 +++++++++++++++++++++++- src/Command/DownloadCommand.php | 2 ++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 19b1baa..df5e472 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,33 @@ # 🥧 PIE (PHP Installer for Extensions) -(to be completed...) +You will need PHP 8.1 or newer to run PIE, but PIE can install an extension to any installed PHP version. ## Installing ## Usage +You can download an extension ready to be built or installed using the `download` command. For example, to download the +`example_pie_extension` extension, you would run: + +```shell +$ bin/pie download asgrim/example-pie-extension +You are running PHP 8.3.7 +Target PHP installation: 8.3.7 (from /usr/bin/php8.3) +Platform: NonWindows, x86_64, NonThreadSafe +Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension +Extracted asgrim/example-pie-extension:1.0.1 source to: /tmp/pie_downloader_6645f07a28bec9.66045489/asgrim-example-pie-extension-769f906 +$ +``` + +If you are trying to install an extension for a different version of PHP, you may specify this on non-Windows systems +with the `--with-php-config` option like: + +```shell +bin/pie download --with-php-config=/usr/bin/php-config7.2 my/extension +``` + +Windows TBD + ## Developing ### Testing diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index df95454..e360a6e 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -69,6 +69,8 @@ public function execute(InputInterface $input, OutputInterface $output): int $phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($withPhpConfig); } + // @todo Support Windows using `--with-php-path="C:\usr\php7.4.33"` + $targetPlatform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath); $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); From d8d93a22cf3c2f81fd1f7d4ab29b3e43269be348 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 27 May 2024 13:04:46 +0100 Subject: [PATCH 37/43] Include extensions in the platform list based on the given PhpBinaryPath --- src/Platform/TargetPhp/PhpBinaryPath.php | 39 +++++++++++ .../PhpBinaryPathBasedPlatformRepository.php | 67 +++++++++++++++++++ .../ResolveTargetPhpToPlatformRepository.php | 3 +- ...pBinaryPathBasedPlatformRepositoryTest.php | 49 ++++++++++++++ .../Platform/TargetPhp/PhpBinaryPathTest.php | 25 +++++++ 5 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepository.php create mode 100644 test/unit/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepositoryTest.php diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 78a3962..4bdfb69 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -7,6 +7,8 @@ use Composer\Semver\VersionParser; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; +use Psl\Json; +use Psl\Type; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; @@ -26,6 +28,43 @@ private function __construct(readonly string $phpBinaryPath) { } + /** + * Returns a map where the key is the name of the extension and the value is the version ('0' if not defined) + * + * @return array + */ + public function extensions(): array + { + $extVersionsRawJson = trim((new Process([ + $this->phpBinaryPath, + '-r', + <<<'PHP' +$exts = get_loaded_extensions(); +$extVersions = array_map( + static function ($extension) { + $extVersion = phpversion($extension); + if ($extVersion === false) { + return '0'; + } + return $extVersion; + }, + $exts +); +echo json_encode(array_combine($exts, $extVersions)); +PHP, + ])) + ->mustRun() + ->getOutput()); + + return Json\typed( + $extVersionsRawJson, + Type\dict( + Type\string(), + Type\string(), + ), + ); + } + public function operatingSystem(): OperatingSystem { $winOrNot = trim((new Process([ diff --git a/src/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepository.php b/src/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepository.php new file mode 100644 index 0000000..b9e9d65 --- /dev/null +++ b/src/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepository.php @@ -0,0 +1,67 @@ +versionParser = new VersionParser(); + $this->packages = []; + + $phpVersion = $phpBinaryPath->version(); + $php = new CompletePackage('php', $this->versionParser->normalize($phpVersion), $phpVersion); + $php->setDescription('The PHP interpreter'); + $this->addPackage($php); + + $extVersions = $phpBinaryPath->extensions(); + + foreach ($extVersions as $extension => $extensionVersion) { + $this->addPackage($this->packageForExtension($extension, $extensionVersion)); + } + + parent::__construct(); + } + + private function packageForExtension(string $name, string $prettyVersion): CompletePackage + { + $extraDescription = ''; + + try { + $version = $this->versionParser->normalize($prettyVersion); + } catch (UnexpectedValueException) { + $extraDescription = ' (actual version: ' . $prettyVersion . ')'; + if (Preg::isMatchStrictGroups('{^(\d+\.\d+\.\d+(?:\.\d+)?)}', $prettyVersion, $match)) { + $prettyVersion = $match[1]; + } else { + $prettyVersion = '0'; + } + + $version = $this->versionParser->normalize($prettyVersion); + } + + $package = new CompletePackage( + 'ext-' . str_replace(' ', '-', strtolower($name)), + $version, + $prettyVersion, + ); + $package->setDescription('The ' . $name . ' PHP extension' . $extraDescription); + $package->setType('php-ext'); + + return $package; + } +} diff --git a/src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php b/src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php index 5e4323e..b1abf1e 100644 --- a/src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php +++ b/src/Platform/TargetPhp/ResolveTargetPhpToPlatformRepository.php @@ -11,7 +11,6 @@ class ResolveTargetPhpToPlatformRepository { public function __invoke(PhpBinaryPath $phpBinaryPath): PlatformRepository { - // @todo I expect we also need to map the extensions for the given PHP binary, somehow? - return new PlatformRepository([], ['php' => $phpBinaryPath->version()]); + return new PhpBinaryPathBasedPlatformRepository($phpBinaryPath); } } diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepositoryTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepositoryTest.php new file mode 100644 index 0000000..83d0e4a --- /dev/null +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathBasedPlatformRepositoryTest.php @@ -0,0 +1,49 @@ +createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::once()) + ->method('version') + ->willReturn('8.1.0'); + $phpBinaryPath->expects(self::once()) + ->method('extensions') + ->willReturn([ + 'json' => '8.1.0-extra', + 'foo' => '8.1.0', + 'without-version' => '0', + 'another' => '1.2.3-alpha.34', + ]); + + $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath); + + self::assertSame( + [ + 'php:8.1.0', + 'ext-json:8.1.0', + 'ext-foo:8.1.0', + 'ext-without-version:0', + 'ext-another:1.2.3-alpha.34', + ], + array_map( + static fn (PackageInterface $package): string => $package->getName() . ':' . $package->getPrettyVersion(), + $platformRepository->getPackages(), + ), + ); + } +} diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index dde65d6..44ba903 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -11,11 +11,15 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Process; +use function array_combine; +use function array_map; use function assert; use function defined; use function file_exists; +use function get_loaded_extensions; use function is_executable; use function php_uname; +use function phpversion; use function sprintf; use function trim; @@ -57,6 +61,27 @@ public function testFromPhpConfigExecutable(): void ); } + public function testExtensions(): void + { + $exts = get_loaded_extensions(); + $extVersions = array_map( + static function ($extension) { + $extVersion = phpversion($extension); + if ($extVersion === false) { + return '0'; + } + + return $extVersion; + }, + $exts, + ); + self::assertSame( + array_combine($exts, $extVersions), + PhpBinaryPath::fromCurrentProcess() + ->extensions(), + ); + } + public function testOperatingSystem(): void { self::assertSame( From e4261c3bbf80df1198231e53985bb5bbe17c3846 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 27 May 2024 13:18:47 +0100 Subject: [PATCH 38/43] Added --with-php-path option to bin/pie download --- src/Command/DownloadCommand.php | 15 ++++++++++-- src/Platform/TargetPhp/PhpBinaryPath.php | 8 ++++++- .../Command/DownloadCommandTest.php | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index e360a6e..43b04b4 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -34,6 +34,7 @@ final class DownloadCommand extends Command { private const ARG_REQUESTED_PACKAGE_AND_VERSION = 'requested-package-and-version'; private const OPTION_WITH_PHP_CONFIG = 'with-php-config'; + private const OPTION_WITH_PHP_PATH = 'with-php-path'; public function __construct( private readonly DependencyResolver $dependencyResolver, @@ -55,7 +56,13 @@ public function configure(): void self::OPTION_WITH_PHP_CONFIG, null, InputOption::VALUE_OPTIONAL, - 'The path to `php-config` to use', + 'The path to the `php-config` binary to find the target PHP platform, e.g. --' . self::OPTION_WITH_PHP_CONFIG . '=/usr/bin/php-config7.4', + ); + $this->addOption( + self::OPTION_WITH_PHP_PATH, + null, + InputOption::VALUE_OPTIONAL, + 'The path to the `php` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe', ); } @@ -69,7 +76,11 @@ public function execute(InputInterface $input, OutputInterface $output): int $phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($withPhpConfig); } - // @todo Support Windows using `--with-php-path="C:\usr\php7.4.33"` + /** @var mixed $withPhpPath */ + $withPhpPath = $input->getOption(self::OPTION_WITH_PHP_PATH); + if (is_string($withPhpPath) && $withPhpPath !== '') { + $phpBinaryPath = PhpBinaryPath::fromPhpBinaryPath($withPhpPath); + } $targetPlatform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath); diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 4bdfb69..0c28d06 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -26,6 +26,7 @@ class PhpBinaryPath /** @param non-empty-string $phpBinaryPath */ private function __construct(readonly string $phpBinaryPath) { + // @todo https://github.com/php/pie/issues/12 - we could verify that the given $phpBinaryPath really is a PHP install } /** @@ -161,7 +162,6 @@ public function phpinfo(): string public static function fromPhpConfigExecutable(string $phpConfig): self { - // @todo filter input/sanitize output $phpExecutable = trim((new Process([$phpConfig, '--php-binary'])) ->mustRun() ->getOutput()); @@ -170,6 +170,12 @@ public static function fromPhpConfigExecutable(string $phpConfig): self return new self($phpExecutable); } + /** @param non-empty-string $phpBinary */ + public static function fromPhpBinaryPath(string $phpBinary): self + { + return new self($phpBinary); + } + public static function fromCurrentProcess(): self { $phpExecutable = trim((string) (new PhpExecutableFinder())->find()); diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 1fadeeb..972767b 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -98,6 +98,30 @@ public function testDownloadingWithPhpConfig(string $requestedVersion, string $e self::assertStringContainsString('Extracted ' . $expectedVersion . ' source to', $outputString); } + #[DataProvider('validVersionsList')] + public function testDownloadingWithPhpPath(string $requestedVersion, string $expectedVersion): void + { + // @todo This test makes an assumption you're using `ppa:ondrej/php` to have multiple PHP versions. This allows + // us to test scenarios where you run with PHP 8.1 but want to install to a PHP 8.3 instance, for example. + // However, this test isn't very portable, and won't run in CI, so we could do with improving this later. + $phpBinaryPath = '/usr/bin/php8.3'; + + if (! file_exists($phpBinaryPath) || ! is_executable($phpBinaryPath)) { + self::markTestSkipped('This test can only run where "' . $phpBinaryPath . '" exists and is executable, to target PHP 8.3'); + } + + $this->commandTester->execute([ + '--with-php-path' => $phpBinaryPath, + 'requested-package-and-version' => $requestedVersion, + ]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Found package: ' . $expectedVersion . ' which provides', $outputString); + self::assertStringContainsString('Extracted ' . $expectedVersion . ' source to', $outputString); + } + public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void { if (PHP_VERSION_ID >= 80200) { From aaa26e77908c2932892d7c5e2743bb7c11ebc4c0 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 27 May 2024 13:21:56 +0100 Subject: [PATCH 39/43] Add link to GH issue for todo comment --- test/integration/Command/DownloadCommandTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 972767b..7297ed4 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -47,9 +47,9 @@ public static function validVersionsList(): array [self::TEST_PACKAGE . ':~1.0.0@alpha', self::TEST_PACKAGE . ':1.0.1'], [self::TEST_PACKAGE . ':^1.1.0@alpha', self::TEST_PACKAGE . ':1.1.0-alpha.1'], [self::TEST_PACKAGE . ':~1.0.0', self::TEST_PACKAGE . ':1.0.1'], - // @todo in theory, these could work, on NonWindows at least - //[self::TEST_PACKAGE . ':dev-master', self::TEST_PACKAGE . ':???'], - //[self::TEST_PACKAGE . ':dev-master#769f906413d6d1e12152f6d34134cbcd347ca253', self::TEST_PACKAGE . ':???'], + // @todo https://github.com/php/pie/issues/13 - in theory, these could work, on NonWindows at least + // [self::TEST_PACKAGE . ':dev-main', self::TEST_PACKAGE . ':???'], + // [self::TEST_PACKAGE . ':dev-main#769f906413d6d1e12152f6d34134cbcd347ca253', self::TEST_PACKAGE . ':???'], ]; return array_combine( From 0ab0adaa6dca684c63e52b46efd1ea63658044cd Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 27 May 2024 13:45:36 +0100 Subject: [PATCH 40/43] Added README notes for --with-php-path and examples for Win/nonWin --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df5e472..fc231e2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,28 @@ with the `--with-php-config` option like: bin/pie download --with-php-config=/usr/bin/php-config7.2 my/extension ``` -Windows TBD +On all platforms, you may provide a path to the `php` executable itself using the `--with-php-path` option. This is an +example on Windows where PHP 8.1 is used to run PIE, but we want to download the extension for PHP 8.3: + +```shell +> C:\php-8.1.7\php.exe bin/pie download --with-php-path=C:\php-8.3.6\php.exe asgrim/example-pie-extension +You are running PHP 8.1.7 +Target PHP installation: 8.3.6 (from C:\php-8.3.6\php.exe) +Platform: Windows, x86_64, ThreadSafe, VS16 +Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension +Extracted asgrim/example-pie-extension:1.0.1 source to: C:\path\to\temp\pie_downloader_66547faa7db3d7.06129230 +``` + +And this is a very similar example (using PHP 8.1 to run PIE to download a PHP 8.3 extension) on a non-Windows platform: + +```shell +$ php8.1 bin/pie download --with-php-path=/usr/bin/php8.3 asgrim/example-pie-extension +You are running PHP 8.1.28 +Target PHP installation: 8.3.7 (from /usr/bin/php8.3) +Platform: NonWindows, x86_64, NonThreadSafe +Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension +Extracted asgrim/example-pie-extension:1.0.1 source to: /tmp/pie_downloader_66547da1e6c685.25242810/asgrim-example-pie-extension-769f906 +``` ## Developing From 1188579d96834f9ec309a0dffc9925e79b471dcf Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Jun 2024 13:38:17 +0100 Subject: [PATCH 41/43] Improve help text for requested package and version option --- src/Command/DownloadCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 43b04b4..15eaee8 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -50,7 +50,7 @@ public function configure(): void $this->addArgument( self::ARG_REQUESTED_PACKAGE_AND_VERSION, InputArgument::REQUIRED, - 'The extension name and version constraint to use, in the format {ext-name}{?:version-constraint}{?@dev-branch-name}, for example `ext-debug:^1.0`', + 'The extension name and version constraint to use, in the format {ext-name}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', ); $this->addOption( self::OPTION_WITH_PHP_CONFIG, From 2298ae7f6958a2f2efb5ca469c21c7dfbef2de81 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Jun 2024 13:44:22 +0100 Subject: [PATCH 42/43] Adjust output of target PHP install information to simplify --- README.md | 9 +++------ src/Command/DownloadCommand.php | 12 +++++++----- src/Platform/OperatingSystem.php | 8 ++++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fc231e2..e9c4a00 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,7 @@ You can download an extension ready to be built or installed using the `download ```shell $ bin/pie download asgrim/example-pie-extension You are running PHP 8.3.7 -Target PHP installation: 8.3.7 (from /usr/bin/php8.3) -Platform: NonWindows, x86_64, NonThreadSafe +Target PHP installation: 8.3.7 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3) Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension Extracted asgrim/example-pie-extension:1.0.1 source to: /tmp/pie_downloader_6645f07a28bec9.66045489/asgrim-example-pie-extension-769f906 $ @@ -32,8 +31,7 @@ example on Windows where PHP 8.1 is used to run PIE, but we want to download the ```shell > C:\php-8.1.7\php.exe bin/pie download --with-php-path=C:\php-8.3.6\php.exe asgrim/example-pie-extension You are running PHP 8.1.7 -Target PHP installation: 8.3.6 (from C:\php-8.3.6\php.exe) -Platform: Windows, x86_64, ThreadSafe, VS16 +Target PHP installation: 8.3.6 ts, vs16, on Windows x86_64 (from C:\php-8.3.6\php.exe) Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension Extracted asgrim/example-pie-extension:1.0.1 source to: C:\path\to\temp\pie_downloader_66547faa7db3d7.06129230 ``` @@ -43,8 +41,7 @@ And this is a very similar example (using PHP 8.1 to run PIE to download a PHP 8 ```shell $ php8.1 bin/pie download --with-php-path=/usr/bin/php8.3 asgrim/example-pie-extension You are running PHP 8.1.28 -Target PHP installation: 8.3.7 (from /usr/bin/php8.3) -Platform: NonWindows, x86_64, NonThreadSafe +Target PHP installation: 8.3.7 nts, on Linux/OSX/etc x86_64 (from /usr/bin/php8.3) Found package: asgrim/example-pie-extension:1.0.1 which provides ext-example_pie_extension Extracted asgrim/example-pie-extension:1.0.1 source to: /tmp/pie_downloader_66547da1e6c685.25242810/asgrim-example-pie-extension-769f906 ``` diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 15eaee8..3527112 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -23,6 +23,7 @@ use function is_string; use function reset; use function sprintf; +use function strtolower; use const PHP_VERSION; @@ -85,13 +86,14 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform = TargetPlatform::fromPhpBinaryPath($phpBinaryPath); $output->writeln(sprintf('You are running PHP %s', PHP_VERSION)); - $output->writeln(sprintf('Target PHP installation: %s (from %s)', $phpBinaryPath->version(), $phpBinaryPath->phpBinaryPath)); $output->writeln(sprintf( - 'Platform: %s, %s, %s%s', - $targetPlatform->operatingSystem->name, + 'Target PHP installation: %s %s%s, on %s %s (from %s)', + $phpBinaryPath->version(), + $targetPlatform->threadSafety->asShort(), + strtolower($targetPlatform->windowsCompiler !== null ? ', ' . $targetPlatform->windowsCompiler->name : ''), + $targetPlatform->operatingSystem->asFriendlyName(), $targetPlatform->architecture->name, - $targetPlatform->threadSafety->name, - $targetPlatform->windowsCompiler !== null ? ', ' . $targetPlatform->windowsCompiler->name : '', + $phpBinaryPath->phpBinaryPath, )); $requestedNameAndVersionPair = $this->requestedNameAndVersionPair($input); diff --git a/src/Platform/OperatingSystem.php b/src/Platform/OperatingSystem.php index 4e5caf0..3b66b81 100644 --- a/src/Platform/OperatingSystem.php +++ b/src/Platform/OperatingSystem.php @@ -9,4 +9,12 @@ enum OperatingSystem { case NonWindows; case Windows; + + public function asFriendlyName(): string + { + return match ($this) { + self::Windows => 'Windows', + self::NonWindows => 'Linux/OSX/etc', + }; + } } From ff55375d3949a73f9b61003e34e14432dabe780d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Jun 2024 13:53:24 +0100 Subject: [PATCH 43/43] Improve wording for php-config and php-path help options --- src/Command/DownloadCommand.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 3527112..9611d6a 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -8,6 +8,7 @@ use InvalidArgumentException; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\Downloading\DownloadAndExtract; +use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Attribute\AsCommand; @@ -57,13 +58,13 @@ public function configure(): void self::OPTION_WITH_PHP_CONFIG, null, InputOption::VALUE_OPTIONAL, - 'The path to the `php-config` binary to find the target PHP platform, e.g. --' . self::OPTION_WITH_PHP_CONFIG . '=/usr/bin/php-config7.4', + 'The path to the `php-config` binary to find the target PHP platform on ' . OperatingSystem::NonWindows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_CONFIG . '=/usr/bin/php-config7.4', ); $this->addOption( self::OPTION_WITH_PHP_PATH, null, InputOption::VALUE_OPTIONAL, - 'The path to the `php` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe', + 'The path to the `php` binary to use as the target PHP platform on ' . OperatingSystem::Windows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe', ); }