diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md
index 8d279c63..1b2f1e8c 100644
--- a/docs/extension-maintainers.md
+++ b/docs/extension-maintainers.md
@@ -228,9 +228,11 @@ The `build-path` may contain some templated values which are replaced:
##### `download-url-method`
The `download-url-method` directive allows extension maintainers to
-change the behaviour of downloading the source package.
+change the behaviour of downloading the source package. This should be defined
+as a list of supported methods, but for backwards compatibility a single
+string may be used.
- * Setting this to `composer-default`, which is the default value if not
+ * Setting this to `composer-default`, which is the default value if nothing is
specified, will use the default behaviour implemented by Composer, which is
to use the standard ZIP archive from the GitHub API (or other source control
system).
@@ -240,6 +242,50 @@ change the behaviour of downloading the source package.
* `php_{ExtensionName}-{Version}-src.zip` (e.g. `php_myext-1.20.1-src.zip`)
* `{ExtensionName}-{Version}.tgz` (this is intended for backwards
compatibility with PECL packages)
+ * Using `pre-packaged-binary` will locate a tgz or zip archive in the release
+ assets list based on matching one of the following naming conventions:
+ * `php_{ExtensionName}-{Version}_php{PhpVersion}-{Arch}-{OS}-{Libc}-{Debug}-{TSMode}.{Format}`
+ * The replacements are:
+ * `{ExtensionName}` the name of your extension, e.g. `xdebug` (hint: this
+ is not your Composer package name!)
+ * `{PhpVersion}` the major and minor version of PHP, e.g. `8.5`
+ * `{Version}` the version of your extension, e.g. `1.20.1`
+ * `{Arch}` the architecture of the binary, one of `x86`, `x86_64`, `arm64`
+ * `{OS}` the operating system, one of `windows`, `darwin`, `linux`, `bsd`, `solaris`, `unknown`
+ * `{Libc}` the libc flavour, one of `glibc`, `musl`, `bsdlibc`
+ * `{Debug}` the debug mode, one of `debug`, `nodebug` (or omitted)
+ * `{TSMode}` the thread safety mode, one of `zts`, `nts` (or omitted)
+ * `{Format}` the archive format, one of `zip`, `tgz`
+ * Some examples of valid asset names:
+ * `php_xdebug-4.1_php8.4-x86_64-linux-glibc.tgz` (or `php_xdebug-4.1_php8.4-x86_64-glibc-nts.tgz`)
+ * `php_xdebug-4.1_php8.4-x86_64-linux-musl.tgz` (or `php_xdebug-4.1_php8.4-x86_64-musl-nts.tgz`)
+ * `php_xdebug-4.1_php8.4-arm64-linux-glibc.tgz` (or `php_xdebug-4.1_php8.4-arm64-glibc-nts.tgz`)
+ * `php_xdebug-4.1_php8.4-arm64-linux-musl.tgz` (or `php_xdebug-4.1_php8.4-arm64-musl-nts.tgz`)
+ * `php_xdebug-4.1_php8.4-x86_64-linux-glibc-zts.tgz`
+ * `php_xdebug-4.1_php8.4-x86_64-linux-musl-zts.tgz`
+ * `php_xdebug-4.1_php8.4-arm64-linux-glibc-zts.tgz`
+ * `php_xdebug-4.1_php8.4-arm64-linux-musl-zts.tgz`
+ * `php_xdebug-4.1_php8.4-x86_64-linux-glibc-debug.tgz`
+ * `php_xdebug-4.1_php8.4-x86_64-linux-musl-debug.tgz`
+ * `php_xdebug-4.1_php8.4-arm64-linux-glibc-debug.tgz`
+ * `php_xdebug-4.1_php8.4-arm64-linux-musl-debug.tgz`
+ * It is recommended that `pre-packaged-binary` is combined with `composer-default`
+ as a fallback mechanism, if a particular combination is supported, but not
+ pre-packaged on the release, e.g. `"download-url-method": ["pre-packaged-binary", "composer-default"]`.
+ PIE will try to find a pre-packaged binary asset first, but if it cannot
+ find an appropriate binary, it will download the source code and build it
+ in the traditional manner.
+
+###### Example of using `pre-packaged-binary` with `composer-default` fallback
+
+```json
+{
+ "name": "myvendor/myext",
+ "php-ext": {
+ "download-url-method": ["pre-packaged-binary", "composer-default"]
+ }
+}
+```
##### `os-families` restrictions
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 2cda1c75..02419ada 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -175,31 +175,31 @@ parameters:
path: src/DependencyResolver/Package.php
-
- message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$downloadUrlMethod is assigned outside of the constructor\.$#'
+ message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$incompatibleOsFamilies is assigned outside of the constructor\.$#'
identifier: property.readOnlyByPhpDocAssignNotInConstructor
count: 1
path: src/DependencyResolver/Package.php
-
- message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$incompatibleOsFamilies is assigned outside of the constructor\.$#'
+ message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$priority is assigned outside of the constructor\.$#'
identifier: property.readOnlyByPhpDocAssignNotInConstructor
count: 1
path: src/DependencyResolver/Package.php
-
- message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$priority is assigned outside of the constructor\.$#'
+ message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportNts is assigned outside of the constructor\.$#'
identifier: property.readOnlyByPhpDocAssignNotInConstructor
count: 1
path: src/DependencyResolver/Package.php
-
- message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportNts is assigned outside of the constructor\.$#'
+ message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportZts is assigned outside of the constructor\.$#'
identifier: property.readOnlyByPhpDocAssignNotInConstructor
count: 1
path: src/DependencyResolver/Package.php
-
- message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportZts is assigned outside of the constructor\.$#'
+ message: '#^@readonly property Php\\Pie\\DependencyResolver\\Package\:\:\$supportedDownloadUrlMethods is assigned outside of the constructor\.$#'
identifier: property.readOnlyByPhpDocAssignNotInConstructor
count: 1
path: src/DependencyResolver/Package.php
diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json
index 55ac0820..9095708b 100644
--- a/resources/composer-json-php-ext-schema.json
+++ b/resources/composer-json-php-ext-schema.json
@@ -41,10 +41,28 @@
"default": null
},
"download-url-method": {
- "type": "string",
- "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.",
- "enum": ["composer-default", "pre-packaged-source"],
- "example": "composer-default"
+ "oneOf": [
+ {
+ "type": "string",
+ "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.",
+ "deprecated": true,
+ "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"],
+ "example": "composer-default",
+ "default": "composer-default"
+ },
+ {
+ "type": "array",
+ "description": "Multiple techniques can be specified, in which case PIE will try each in turn until one succeeds. The first technique that succeeds will be used.",
+ "items": {
+ "type": "string",
+ "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.",
+ "enum": ["composer-default", "pre-packaged-source", "pre-packaged-binary"],
+ "example": ["pre-packaged-binary", "composer-default"]
+ },
+ "minItems": 1,
+ "default": ["composer-default"]
+ }
+ ]
},
"os-families": {
"type": "array",
diff --git a/src/Building/ExtensionBinaryNotFound.php b/src/Building/ExtensionBinaryNotFound.php
index 1074361b..9974ee98 100644
--- a/src/Building/ExtensionBinaryNotFound.php
+++ b/src/Building/ExtensionBinaryNotFound.php
@@ -10,6 +10,14 @@
class ExtensionBinaryNotFound extends RuntimeException
{
+ public static function fromPrePackagedBinary(string $expectedBinaryName): self
+ {
+ return new self(sprintf(
+ 'Expected pre-packaged binary does not exist: %s',
+ $expectedBinaryName,
+ ));
+ }
+
public static function fromExpectedBinary(string $expectedBinaryName): self
{
return new self(sprintf(
diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php
index ef05661e..f46f6df1 100644
--- a/src/Building/UnixBuild.php
+++ b/src/Building/UnixBuild.php
@@ -5,8 +5,10 @@
namespace Php\Pie\Building;
use Composer\IO\IOInterface;
+use LogicException;
use Php\Pie\ComposerIntegration\BundledPhpExtensionsRepository;
use Php\Pie\Downloading\DownloadedPackage;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\File\BinaryFile;
use Php\Pie\Platform\TargetPhp\PhpizePath;
use Php\Pie\Platform\TargetPlatform;
@@ -33,6 +35,45 @@ public function __invoke(
array $configureOptions,
IOInterface $io,
PhpizePath|null $phpizePath,
+ ): BinaryFile {
+ switch (DownloadUrlMethod::fromDownloadedPackage($downloadedPackage)) {
+ case DownloadUrlMethod::PrePackagedBinary:
+ return $this->prePackagedBinary($downloadedPackage, $io);
+
+ case DownloadUrlMethod::ComposerDefaultDownload:
+ case DownloadUrlMethod::PrePackagedSourceDownload:
+ return $this->buildFromSource($downloadedPackage, $targetPlatform, $configureOptions, $io, $phpizePath);
+
+ default:
+ throw new LogicException('Unknown download method');
+ }
+ }
+
+ private function prePackagedBinary(
+ DownloadedPackage $downloadedPackage,
+ IOInterface $io,
+ ): BinaryFile {
+ $expectedSoFile = $downloadedPackage->extractedSourcePath . '/' . $downloadedPackage->package->extensionName()->name() . '.so';
+
+ if (! file_exists($expectedSoFile)) {
+ throw ExtensionBinaryNotFound::fromPrePackagedBinary($expectedSoFile);
+ }
+
+ $io->write(sprintf(
+ 'Pre-packaged binary found: %s',
+ $expectedSoFile,
+ ));
+
+ return BinaryFile::fromFileWithSha256Checksum($expectedSoFile);
+ }
+
+ /** @param list $configureOptions */
+ private function buildFromSource(
+ DownloadedPackage $downloadedPackage,
+ TargetPlatform $targetPlatform,
+ array $configureOptions,
+ IOInterface $io,
+ PhpizePath|null $phpizePath,
): BinaryFile {
$outputCallback = null;
if ($io->isVerbose()) {
diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php
index b036b24c..d64a4d79 100644
--- a/src/ComposerIntegration/InstallAndBuildProcess.php
+++ b/src/ComposerIntegration/InstallAndBuildProcess.php
@@ -48,6 +48,7 @@ public function __invoke(
$composerPackage,
);
+ $builtBinaryFile = null;
if ($composerRequest->operation->shouldBuild()) {
$builtBinaryFile = ($this->pieBuild)(
$downloadedPackage,
@@ -75,6 +76,7 @@ public function __invoke(
($this->pieInstall)(
$downloadedPackage,
$composerRequest->targetPlatform,
+ $builtBinaryFile,
$io,
$composerRequest->attemptToSetupIniFile,
),
diff --git a/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php b/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php
new file mode 100644
index 00000000..a5782c56
--- /dev/null
+++ b/src/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethod.php
@@ -0,0 +1,42 @@
+ $downloadMethods
+ * @param array $failureReasons
+ */
+ public static function fromDownloadUrlMethods(Package $piePackage, array $downloadMethods, array $failureReasons): self
+ {
+ $message = sprintf('Could not download %s', $piePackage->name());
+
+ if (count($downloadMethods) === 1) {
+ $first = array_key_first($downloadMethods);
+ $message .= sprintf(' using %s method: %s', $downloadMethods[$first]->value, $failureReasons[$downloadMethods[$first]->value] ?? '(unknown failure)');
+
+ return new self($message);
+ }
+
+ $message .= ' using the following methods:' . PHP_EOL;
+
+ foreach ($downloadMethods as $downloadMethod) {
+ $message .= sprintf(' - %s: %s%s', $downloadMethod->value, $failureReasons[$downloadMethod->value] ?? '(unknown failure)', PHP_EOL);
+ }
+
+ return new self($message);
+ }
+}
diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php
index 12118fb9..cf7b6e5a 100644
--- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php
+++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php
@@ -17,6 +17,7 @@
use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\Downloading\PackageReleaseAssets;
use Psr\Container\ContainerInterface;
+use Throwable;
use function array_walk;
use function pathinfo;
@@ -69,38 +70,70 @@ function (OperationInterface $operation): void {
return;
}
- $piePackage = Package::fromComposerCompletePackage($composerPackage);
- $targetPlatform = $this->composerRequest->targetPlatform;
- $downloadUrlMethod = DownloadUrlMethod::fromPackage($piePackage, $targetPlatform);
-
- // Exit early if we should just use Composer's normal download
- if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) {
- return;
- }
-
- $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform);
- if ($possibleAssetNames === null) {
- return;
+ $piePackage = Package::fromComposerCompletePackage($composerPackage);
+ $targetPlatform = $this->composerRequest->targetPlatform;
+ $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($piePackage, $targetPlatform);
+
+ $selectedDownloadUrlMethod = null;
+ $downloadMethodFailures = [];
+
+ foreach ($downloadUrlMethods as $downloadUrlMethod) {
+ $this->io->write('Trying to download using: ' . $downloadUrlMethod->value, verbosity: IOInterface::VERY_VERBOSE);
+
+ // Exit early if we should just use Composer's normal download
+ if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) {
+ $selectedDownloadUrlMethod = $downloadUrlMethod;
+ break;
+ }
+
+ try {
+ $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform);
+ } catch (Throwable $t) {
+ $downloadMethodFailures[$downloadUrlMethod->value] = $t->getMessage();
+ $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: ' . $t->getMessage(), verbosity: IOInterface::VERBOSE);
+ continue;
+ }
+
+ if ($possibleAssetNames === null) {
+ $downloadMethodFailures[$downloadUrlMethod->value] = 'No asset names';
+ $this->io->write('Failed fetching asset names [' . $downloadUrlMethod->value . ']: No asset names', verbosity: IOInterface::VERBOSE);
+ continue;
+ }
+
+ // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.)
+ $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class);
+
+ try {
+ $url = $packageReleaseAssets->findMatchingReleaseAssetUrl(
+ $targetPlatform,
+ $piePackage,
+ new HttpDownloader($this->io, $this->composer->getConfig()),
+ $downloadUrlMethod,
+ $possibleAssetNames,
+ );
+ } catch (Throwable $t) {
+ $downloadMethodFailures[$downloadUrlMethod->value] = $t->getMessage();
+ $this->io->write('Failed locating asset [' . $downloadUrlMethod->value . ']: ' . $t->getMessage(), verbosity: IOInterface::VERBOSE);
+ continue;
+ }
+
+ $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url);
+ $composerPackage->setDistUrl($url);
+
+ if (pathinfo($url, PATHINFO_EXTENSION) === 'tgz') {
+ $composerPackage->setDistType('tar');
+ }
+
+ $selectedDownloadUrlMethod = $downloadUrlMethod;
+ break;
}
- // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.)
- $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class);
-
- $url = $packageReleaseAssets->findMatchingReleaseAssetUrl(
- $targetPlatform,
- $piePackage,
- new HttpDownloader($this->io, $this->composer->getConfig()),
- $possibleAssetNames,
- );
-
- $this->composerRequest->pieOutput->write('Found prebuilt archive: ' . $url);
- $composerPackage->setDistUrl($url);
-
- if (pathinfo($url, PATHINFO_EXTENSION) !== 'tgz') {
- return;
+ if ($selectedDownloadUrlMethod === null) {
+ throw CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods($piePackage, $downloadUrlMethods, $downloadMethodFailures);
}
- $composerPackage->setDistType('tar');
+ $selectedDownloadUrlMethod->writeToComposerPackage($composerPackage);
+ $this->io->write('Selected download URL method: ' . $selectedDownloadUrlMethod->value . '', verbosity: IOInterface::VERBOSE);
},
);
}
diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php
index 13111a35..154c01ec 100644
--- a/src/DependencyResolver/Package.php
+++ b/src/DependencyResolver/Package.php
@@ -16,8 +16,10 @@
use function array_key_exists;
use function array_map;
use function array_slice;
+use function count;
use function explode;
use function implode;
+use function is_array;
use function parse_url;
use function str_contains;
use function str_starts_with;
@@ -37,10 +39,11 @@ final class Package
/** @var non-empty-list|null */
private array|null $compatibleOsFamilies = null;
/** @var non-empty-list|null */
- private array|null $incompatibleOsFamilies = null;
- private bool $supportZts = true;
- private bool $supportNts = true;
- private DownloadUrlMethod|null $downloadUrlMethod = null;
+ private array|null $incompatibleOsFamilies = null;
+ private bool $supportZts = true;
+ private bool $supportNts = true;
+ /** @var non-empty-list|null */
+ private array|null $supportedDownloadUrlMethods = null;
public function __construct(
private readonly CompletePackageInterface $composerPackage,
@@ -89,7 +92,15 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com
$package->priority = $phpExtOptions['priority'] ?? 80;
if ($phpExtOptions !== null && array_key_exists('download-url-method', $phpExtOptions)) {
- $package->downloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['download-url-method']);
+ /** @var string|list $extOptionValue */
+ $extOptionValue = $phpExtOptions['download-url-method'];
+ $methods = is_array($extOptionValue) ? $extOptionValue : [$extOptionValue];
+ if (count($methods) > 0) {
+ $package->supportedDownloadUrlMethods = array_map(
+ static fn (string $method): DownloadUrlMethod => DownloadUrlMethod::from($method),
+ $methods,
+ );
+ }
}
return $package;
@@ -219,8 +230,9 @@ public function supportNts(): bool
return $this->supportNts;
}
- public function downloadUrlMethod(): DownloadUrlMethod|null
+ /** @return non-empty-list|null */
+ public function supportedDownloadUrlMethods(): array|null
{
- return $this->downloadUrlMethod;
+ return $this->supportedDownloadUrlMethods;
}
}
diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php
index 6a5a95d8..4b1d99d8 100644
--- a/src/Downloading/DownloadUrlMethod.php
+++ b/src/Downloading/DownloadUrlMethod.php
@@ -4,18 +4,29 @@
namespace Php\Pie\Downloading;
+use Composer\Package\CompletePackageInterface;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\Platform\OperatingSystem;
+use Php\Pie\Platform\PrePackagedBinaryAssetName;
use Php\Pie\Platform\PrePackagedSourceAssetName;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Platform\WindowsExtensionAssetName;
+use function array_key_exists;
+use function array_merge;
+use function assert;
+use function is_string;
+use function method_exists;
+
/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
enum DownloadUrlMethod: string
{
+ private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method';
+
case ComposerDefaultDownload = 'composer-default';
case WindowsBinaryDownload = 'windows-binary';
case PrePackagedSourceDownload = 'pre-packaged-source';
+ case PrePackagedBinary = 'pre-packaged-binary';
/** @return non-empty-list|null */
public function possibleAssetNames(Package $package, TargetPlatform $targetPlatform): array|null
@@ -24,28 +35,45 @@ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatf
self::WindowsBinaryDownload => WindowsExtensionAssetName::zipNames($targetPlatform, $package),
self::PrePackagedSourceDownload => PrePackagedSourceAssetName::packageNames($package),
self::ComposerDefaultDownload => null,
+ self::PrePackagedBinary => PrePackagedBinaryAssetName::packageNames($targetPlatform, $package),
};
}
- public static function fromPackage(Package $package, TargetPlatform $targetPlatform): self
+ public static function fromDownloadedPackage(DownloadedPackage $downloadedPackage): self
+ {
+ return self::fromComposerPackage($downloadedPackage->package->composerPackage());
+ }
+
+ public static function fromComposerPackage(CompletePackageInterface $completePackage): self
+ {
+ $extra = $completePackage->getExtra();
+
+ return self::from(array_key_exists(self::COMPOSER_PACKAGE_EXTRA_KEY, $extra) && is_string($extra[self::COMPOSER_PACKAGE_EXTRA_KEY]) ? $extra[self::COMPOSER_PACKAGE_EXTRA_KEY] : '');
+ }
+
+ public function writeToComposerPackage(CompletePackageInterface $composerPackage): void
+ {
+ assert(method_exists($composerPackage, 'setExtra'));
+
+ $composerPackage->setExtra(array_merge($composerPackage->getExtra(), [self::COMPOSER_PACKAGE_EXTRA_KEY => $this->value]));
+ }
+
+ /** @return non-empty-list */
+ public static function possibleDownloadUrlMethodsForPackage(Package $package, TargetPlatform $targetPlatform): array
{
/**
* PIE does not support building on Windows (yet, at least). Maintainers
* should provide pre-built Windows binaries.
*/
if ($targetPlatform->operatingSystem === OperatingSystem::Windows) {
- return self::WindowsBinaryDownload;
+ return [self::WindowsBinaryDownload];
}
- /**
- * Some packages pre-package source code (e.g. mongodb) as there are
- * external dependencies in Git submodules that otherwise aren't
- * included in GitHub/Gitlab/etc "dist" downloads
- */
- if ($package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) {
- return self::PrePackagedSourceDownload;
+ $configuredSupportedMethods = $package->supportedDownloadUrlMethods();
+ if ($configuredSupportedMethods === null) {
+ return [self::ComposerDefaultDownload];
}
- return self::ComposerDefaultDownload;
+ return $configuredSupportedMethods;
}
}
diff --git a/src/Downloading/Exception/CouldNotFindReleaseAsset.php b/src/Downloading/Exception/CouldNotFindReleaseAsset.php
index 815086bd..e52d0101 100644
--- a/src/Downloading/Exception/CouldNotFindReleaseAsset.php
+++ b/src/Downloading/Exception/CouldNotFindReleaseAsset.php
@@ -17,10 +17,8 @@
class CouldNotFindReleaseAsset extends RuntimeException
{
/** @param non-empty-list $expectedAssetNames */
- public static function forPackage(TargetPlatform $targetPlatform, Package $package, array $expectedAssetNames): self
+ public static function forPackage(TargetPlatform $targetPlatform, Package $package, DownloadUrlMethod $downloadUrlMethod, array $expectedAssetNames): self
{
- $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform);
-
if ($downloadUrlMethod === DownloadUrlMethod::WindowsBinaryDownload) {
return new self(sprintf(
'Windows archive with prebuilt extension for %s was not attached on release %s - looked for one of "%s"',
@@ -37,10 +35,10 @@ public static function forPackage(TargetPlatform $targetPlatform, Package $packa
));
}
- public static function forPackageWithMissingTag(Package $package): self
+ public static function forPackageWithMissingTag(Package $package, DownloadUrlMethod $downloadUrlMethod): self
{
if (
- $package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload
+ $downloadUrlMethod === DownloadUrlMethod::PrePackagedSourceDownload
&& $package->composerPackage()->isDev()
) {
return new self(sprintf(
diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php
index 04c7aed9..ad1bd13a 100644
--- a/src/Downloading/GithubPackageReleaseAssets.php
+++ b/src/Downloading/GithubPackageReleaseAssets.php
@@ -31,12 +31,14 @@ public function findMatchingReleaseAssetUrl(
TargetPlatform $targetPlatform,
Package $package,
HttpDownloader $httpDownloader,
+ DownloadUrlMethod $downloadUrlMethod,
array $possibleReleaseAssetNames,
): string {
$releaseAsset = $this->selectMatchingReleaseAsset(
$targetPlatform,
$package,
- $this->getReleaseAssetsForPackage($package, $httpDownloader),
+ $this->getReleaseAssetsForPackage($package, $httpDownloader, $downloadUrlMethod),
+ $downloadUrlMethod,
$possibleReleaseAssetNames,
);
@@ -56,6 +58,7 @@ private function selectMatchingReleaseAsset(
TargetPlatform $targetPlatform,
Package $package,
array $releaseAssets,
+ DownloadUrlMethod $downloadUrlMethod,
array $possibleReleaseAssetNames,
): array {
foreach ($releaseAssets as $releaseAsset) {
@@ -64,13 +67,14 @@ private function selectMatchingReleaseAsset(
}
}
- throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $possibleReleaseAssetNames);
+ throw Exception\CouldNotFindReleaseAsset::forPackage($targetPlatform, $package, $downloadUrlMethod, $possibleReleaseAssetNames);
}
/** @return list */
private function getReleaseAssetsForPackage(
Package $package,
HttpDownloader $httpDownloader,
+ DownloadUrlMethod $downloadUrlMethod,
): array {
Assert::notNull($package->downloadUrl());
@@ -88,7 +92,7 @@ private function getReleaseAssetsForPackage(
} catch (TransportException $t) {
/** @link https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-a-release-by-tag-name */
if ($t->getStatusCode() === 404) {
- throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package);
+ throw Exception\CouldNotFindReleaseAsset::forPackageWithMissingTag($package, $downloadUrlMethod);
}
throw $t;
diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php
index 87a903f2..622bebdb 100644
--- a/src/Downloading/PackageReleaseAssets.php
+++ b/src/Downloading/PackageReleaseAssets.php
@@ -20,6 +20,7 @@ public function findMatchingReleaseAssetUrl(
TargetPlatform $targetPlatform,
Package $package,
HttpDownloader $httpDownloader,
+ DownloadUrlMethod $downloadUrlMethod,
array $possibleReleaseAssetNames,
): string;
}
diff --git a/src/Installing/Install.php b/src/Installing/Install.php
index ebdda216..81724d0a 100644
--- a/src/Installing/Install.php
+++ b/src/Installing/Install.php
@@ -19,6 +19,7 @@ interface Install
public function __invoke(
DownloadedPackage $downloadedPackage,
TargetPlatform $targetPlatform,
+ BinaryFile|null $builtBinaryFile,
IOInterface $io,
bool $attemptToSetupIniFile,
): BinaryFile;
diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php
index 02e32489..2462dab6 100644
--- a/src/Installing/UnixInstall.php
+++ b/src/Installing/UnixInstall.php
@@ -6,14 +6,17 @@
use Composer\IO\IOInterface;
use Php\Pie\Downloading\DownloadedPackage;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\File\BinaryFile;
use Php\Pie\File\Sudo;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Util\Process;
use RuntimeException;
+use Webmozart\Assert\Assert;
use function array_unshift;
use function file_exists;
+use function implode;
use function is_writable;
use function sprintf;
@@ -29,6 +32,7 @@ public function __construct(private readonly SetupIniFile $setupIniFile)
public function __invoke(
DownloadedPackage $downloadedPackage,
TargetPlatform $targetPlatform,
+ BinaryFile|null $builtBinaryFile,
IOInterface $io,
bool $attemptToSetupIniFile,
): BinaryFile {
@@ -41,7 +45,19 @@ public function __invoke(
$sharedObjectName,
);
- $makeInstallCommand = ['make', 'install'];
+ switch (DownloadUrlMethod::fromDownloadedPackage($downloadedPackage)) {
+ case DownloadUrlMethod::PrePackagedBinary:
+ Assert::notNull($builtBinaryFile);
+ $installCommand = [
+ 'cp',
+ $builtBinaryFile->filePath,
+ $targetExtensionPath,
+ ];
+ break;
+
+ default:
+ $installCommand = ['make', 'install'];
+ }
// If the target directory isn't writable, or a .so file already exists and isn't writable, try to use sudo
if (
@@ -55,11 +71,13 @@ public function __invoke(
'Cannot write to %s, so using sudo to elevate privileges.',
$targetExtensionPath,
));
- array_unshift($makeInstallCommand, Sudo::find());
+ array_unshift($installCommand, Sudo::find());
}
+ $io->write(sprintf('Install command is: %s', implode(' ', $installCommand)), verbosity: IOInterface::VERY_VERBOSE);
+
$makeInstallOutput = Process::run(
- $makeInstallCommand,
+ $installCommand,
$downloadedPackage->extractedSourcePath,
self::MAKE_INSTALL_TIMEOUT_SECS,
);
diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php
index 97eae97e..e674a561 100644
--- a/src/Installing/WindowsInstall.php
+++ b/src/Installing/WindowsInstall.php
@@ -37,6 +37,7 @@ public function __construct(private readonly SetupIniFile $setupIniFile)
public function __invoke(
DownloadedPackage $downloadedPackage,
TargetPlatform $targetPlatform,
+ BinaryFile|null $builtBinaryFile,
IOInterface $io,
bool $attemptToSetupIniFile,
): BinaryFile {
diff --git a/src/Platform/DebugBuild.php b/src/Platform/DebugBuild.php
new file mode 100644
index 00000000..691dd50d
--- /dev/null
+++ b/src/Platform/DebugBuild.php
@@ -0,0 +1,12 @@
+find('otool');
+ if ($otool !== null) {
+ return self::Bsd;
+ }
+
+ $lddPath = $executableFinder->find('ldd');
+ $lsPath = $executableFinder->find('ls');
+
+ if ($lddPath === null || $lsPath === null) {
+ return self::Gnu;
+ }
+
+ try {
+ $linkResult = Process::run([$lddPath, $lsPath]);
+ } catch (Throwable) {
+ return self::Gnu;
+ }
+
+ return str_contains($linkResult, 'musl') ? self::Musl : self::Gnu;
+ }
+}
diff --git a/src/Platform/PrePackagedBinaryAssetName.php b/src/Platform/PrePackagedBinaryAssetName.php
new file mode 100644
index 00000000..eda3f947
--- /dev/null
+++ b/src/Platform/PrePackagedBinaryAssetName.php
@@ -0,0 +1,71 @@
+ */
+ public static function packageNames(TargetPlatform $targetPlatform, Package $package): array
+ {
+ return array_values(array_unique([
+ strtolower(sprintf(
+ 'php_%s-%s_php%s-%s-%s-%s%s%s.zip',
+ $package->extensionName()->name(),
+ $package->version(),
+ $targetPlatform->phpBinaryPath->majorMinorVersion(),
+ $targetPlatform->architecture->name,
+ $targetPlatform->operatingSystemFamily->value,
+ $targetPlatform->libcFlavour()->value,
+ $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '',
+ $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '',
+ )),
+ strtolower(sprintf(
+ 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz',
+ $package->extensionName()->name(),
+ $package->version(),
+ $targetPlatform->phpBinaryPath->majorMinorVersion(),
+ $targetPlatform->architecture->name,
+ $targetPlatform->operatingSystemFamily->value,
+ $targetPlatform->libcFlavour()->value,
+ $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '',
+ $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '',
+ )),
+ strtolower(sprintf(
+ 'php_%s-%s_php%s-%s-%s-%s%s%s.zip',
+ $package->extensionName()->name(),
+ $package->version(),
+ $targetPlatform->phpBinaryPath->majorMinorVersion(),
+ $targetPlatform->architecture->name,
+ $targetPlatform->operatingSystemFamily->value,
+ $targetPlatform->libcFlavour()->value,
+ $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '',
+ $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts',
+ )),
+ strtolower(sprintf(
+ 'php_%s-%s_php%s-%s-%s-%s%s%s.tgz',
+ $package->extensionName()->name(),
+ $package->version(),
+ $targetPlatform->phpBinaryPath->majorMinorVersion(),
+ $targetPlatform->architecture->name,
+ $targetPlatform->operatingSystemFamily->value,
+ $targetPlatform->libcFlavour()->value,
+ $targetPlatform->phpBinaryPath->debugMode() === DebugBuild::Debug ? '-debug' : '',
+ $targetPlatform->threadSafety === ThreadSafetyMode::ThreadSafe ? '-zts' : '-nts',
+ )),
+ ]));
+ }
+}
diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php
index c6abc55f..32108921 100644
--- a/src/Platform/TargetPhp/PhpBinaryPath.php
+++ b/src/Platform/TargetPhp/PhpBinaryPath.php
@@ -9,6 +9,7 @@
use Composer\Util\Platform;
use Php\Pie\ExtensionName;
use Php\Pie\Platform\Architecture;
+use Php\Pie\Platform\DebugBuild;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Util\Process;
@@ -89,6 +90,18 @@ public function phpApiVersion(): string
throw new RuntimeException('Failed to find PHP API version...');
}
+ public function debugMode(): DebugBuild
+ {
+ if (
+ preg_match('/Debug Build([ =>\t]*)(.*)/', $this->phpinfo(), $m)
+ && $m[2] !== ''
+ ) {
+ return $m[2] === 'yes' ? DebugBuild::Debug : DebugBuild::NoDebug;
+ }
+
+ throw new RuntimeException('Failed to find PHP API version...');
+ }
+
/** @return non-empty-string */
public function extensionPath(): string
{
diff --git a/src/Platform/TargetPlatform.php b/src/Platform/TargetPlatform.php
index c6cf5f62..9a50542c 100644
--- a/src/Platform/TargetPlatform.php
+++ b/src/Platform/TargetPlatform.php
@@ -21,6 +21,8 @@
*/
class TargetPlatform
{
+ private static LibcFlavour $libcFlavour;
+
public function __construct(
public readonly OperatingSystem $operatingSystem,
public readonly OperatingSystemFamily $operatingSystemFamily,
@@ -32,6 +34,15 @@ public function __construct(
) {
}
+ public function libcFlavour(): LibcFlavour
+ {
+ if (! isset(self::$libcFlavour)) {
+ self::$libcFlavour = LibcFlavour::detect();
+ }
+
+ return self::$libcFlavour;
+ }
+
public static function isRunningAsRoot(): bool
{
return function_exists('posix_getuid') && posix_getuid() === 0;
diff --git a/test/assets/fake-ldd/bsdlibc/otool b/test/assets/fake-ldd/bsdlibc/otool
new file mode 100755
index 00000000..69ef7881
--- /dev/null
+++ b/test/assets/fake-ldd/bsdlibc/otool
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+echo "/bin/ls:
+ /usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0)
+ /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
+ /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)"
diff --git a/test/assets/fake-ldd/glibc/ldd b/test/assets/fake-ldd/glibc/ldd
new file mode 100755
index 00000000..0a6f9719
--- /dev/null
+++ b/test/assets/fake-ldd/glibc/ldd
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+echo " linux-vdso.so.1 (0x000078fecccd6000)
+ libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x000078feccc57000)
+ libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x000078feccc49000)
+ libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000078fecca00000)
+ libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x000078fecc941000)
+ /lib64/ld-linux-x86-64.so.2 (0x000078fecccd8000)"
diff --git a/test/assets/fake-ldd/musl/ldd b/test/assets/fake-ldd/musl/ldd
new file mode 100755
index 00000000..1824d33d
--- /dev/null
+++ b/test/assets/fake-ldd/musl/ldd
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo " /lib/ld-musl-x86_64.so.1 (0x7146f93b1000)
+ libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7146f93b1000)"
diff --git a/test/assets/pre-packaged-binary-examples/invalid/wrong-name.so b/test/assets/pre-packaged-binary-examples/invalid/wrong-name.so
new file mode 100644
index 00000000..e69de29b
diff --git a/test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so b/test/assets/pre-packaged-binary-examples/valid/pie_test_ext.so
new file mode 100644
index 00000000..e69de29b
diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php
index 73d1c80f..bcf221e0 100644
--- a/test/integration/Building/UnixBuildTest.php
+++ b/test/integration/Building/UnixBuildTest.php
@@ -11,6 +11,7 @@
use Php\Pie\Building\UnixBuild;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\Downloading\DownloadedPackage;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
@@ -25,9 +26,12 @@
#[CoversClass(UnixBuild::class)]
final class UnixBuildTest extends TestCase
{
- private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext';
+ private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method';
+ private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext';
+ private const TEST_PREBUILT_PATH_VALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/valid';
+ private const TEST_PREBUILT_PATH_INVALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/invalid';
- public function testUnixBuildCanBuildExtension(): void
+ public function testUnixSourceBuildCanBuildExtension(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Unix build test cannot be run on Windows');
@@ -35,9 +39,14 @@ public function testUnixBuildCanBuildExtension(): void
$output = new BufferIO();
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
new Package(
- $this->createMock(CompletePackageInterface::class),
+ $composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('pie_test_ext'),
'pie_test_ext',
@@ -76,7 +85,7 @@ public function testUnixBuildCanBuildExtension(): void
(new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun();
}
- public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(): void
+ public function testUnixSourceBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Unix build test cannot be run on Windows');
@@ -84,9 +93,14 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(
$output = new BufferIO();
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
new Package(
- $this->createMock(CompletePackageInterface::class),
+ $composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('mismatched_name'),
'pie_test_ext',
@@ -113,7 +127,7 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches(
}
}
- public function testUnixBuildCanBuildExtensionWithBuildPath(): void
+ public function testUnixSourceBuildCanBuildExtensionWithBuildPath(): void
{
if (Platform::isWindows()) {
self::markTestSkipped('Unix build test cannot be run on Windows');
@@ -126,6 +140,9 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void
$composerPackage->method('getPrettyVersion')->willReturn('0.1.0');
$composerPackage->method('getType')->willReturn('php-ext');
$composerPackage->method('getPhpExt')->willReturn(['build-path' => 'pie_test_ext']);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
Package::fromComposerCompletePackage($composerPackage),
@@ -161,6 +178,77 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void
(new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun();
}
+ public function testUnixBinaryBuildThrowsErrorWhenBinaryFileNotFound(): void
+ {
+ if (Platform::isWindows()) {
+ self::markTestSkipped('Unix build test cannot be run on Windows');
+ }
+
+ $output = new BufferIO();
+
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext');
+ $composerPackage->method('getPrettyVersion')->willReturn('0.1.0');
+ $composerPackage->method('getType')->willReturn('php-ext');
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]);
+
+ $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ Package::fromComposerCompletePackage($composerPackage),
+ self::TEST_PREBUILT_PATH_INVALID,
+ );
+
+ $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null);
+ $unixBuilder = new UnixBuild();
+
+ $this->expectException(ExtensionBinaryNotFound::class);
+ $this->expectExceptionMessage('Expected pre-packaged binary does not exist');
+ $unixBuilder->__invoke(
+ $downloadedPackage,
+ $targetPlatform,
+ ['--enable-pie_test_ext'],
+ $output,
+ null,
+ );
+ }
+
+ public function testUnixBinaryBuildReturnsBinaryFile(): void
+ {
+ if (Platform::isWindows()) {
+ self::markTestSkipped('Unix build test cannot be run on Windows');
+ }
+
+ $output = new BufferIO();
+
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext');
+ $composerPackage->method('getPrettyVersion')->willReturn('0.1.0');
+ $composerPackage->method('getType')->willReturn('php-ext');
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]);
+
+ $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
+ Package::fromComposerCompletePackage($composerPackage),
+ self::TEST_PREBUILT_PATH_VALID,
+ );
+
+ $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null);
+ $unixBuilder = new UnixBuild();
+
+ $binaryFile = $unixBuilder->__invoke(
+ $downloadedPackage,
+ $targetPlatform,
+ ['--enable-pie_test_ext'],
+ $output,
+ null,
+ );
+
+ self::assertSame('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', $binaryFile->checksum);
+ self::assertStringEndsWith('pre-packaged-binary-examples/valid/pie_test_ext.so', $binaryFile->filePath);
+ }
+
public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void
{
if (Platform::isWindows()) {
@@ -172,9 +260,14 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void
$output = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE);
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
new Package(
- $this->createMock(CompletePackageInterface::class),
+ $composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('pie_test_ext'),
'pie_test_ext',
@@ -209,9 +302,14 @@ public function testVerboseOutputShowsCleanupMessages(): void
$output = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERBOSE);
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
new Package(
- $this->createMock(CompletePackageInterface::class),
+ $composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('pie_test_ext'),
'pie_test_ext',
diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
index cd9cb4b3..f26b0ef7 100644
--- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
+++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php
@@ -9,6 +9,7 @@
use Composer\Package\CompletePackageInterface;
use Composer\Util\HttpDownloader;
use Php\Pie\DependencyResolver\Package;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\Downloading\GithubPackageReleaseAssets;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
@@ -65,6 +66,7 @@ public function testDeterminingReleaseAssetUrlForWindows(): void
$targetPlatform,
$package,
new HttpDownloader($io, $config),
+ DownloadUrlMethod::WindowsBinaryDownload,
WindowsExtensionAssetName::zipNames(
$targetPlatform,
$package,
diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php
index cae63683..f80e21d8 100644
--- a/test/integration/Installing/UnixInstallTest.php
+++ b/test/integration/Installing/UnixInstallTest.php
@@ -10,8 +10,10 @@
use Php\Pie\Building\UnixBuild;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\Downloading\DownloadedPackage;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
+use Php\Pie\File\BinaryFile;
use Php\Pie\Installing\Ini\PickBestSetupIniApproach;
use Php\Pie\Installing\SetupIniFile;
use Php\Pie\Installing\UnixInstall;
@@ -30,11 +32,18 @@
use function file_exists;
use function is_executable;
use function is_writable;
+use function mkdir;
+use function rename;
+use function unlink;
+
+use const DIRECTORY_SEPARATOR;
#[CoversClass(UnixInstall::class)]
final class UnixInstallTest extends TestCase
{
- private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext';
+ private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method';
+ private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext';
+ private const TEST_PREBUILT_PATH = __DIR__ . '/../../assets/pre-packaged-binary-examples/install';
/** @return array */
public static function phpPathProvider(): array
@@ -68,7 +77,7 @@ public static function phpPathProvider(): array
}
#[DataProvider('phpPathProvider')]
- public function testUnixInstallCanInstallExtension(string $phpConfig): void
+ public function testUnixInstallCanInstallExtensionBuiltFromSource(string $phpConfig): void
{
assert($phpConfig !== '');
if (Platform::isWindows()) {
@@ -79,9 +88,14 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void
$targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable($phpConfig), null);
$extensionPath = $targetPlatform->phpBinaryPath->extensionPath();
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
+
$downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath(
new Package(
- $this->createMock(CompletePackageInterface::class),
+ $composerPackage,
ExtensionType::PhpModule,
ExtensionName::normaliseFromString('pie_test_ext'),
'pie_test_ext',
@@ -91,7 +105,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void
self::TEST_EXTENSION_PATH,
);
- (new UnixBuild())->__invoke(
+ $built = (new UnixBuild())->__invoke(
$downloadedPackage,
$targetPlatform,
['--enable-pie_test_ext'],
@@ -102,6 +116,7 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void
$installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke(
$downloadedPackage,
$targetPlatform,
+ $built,
$output,
true,
);
@@ -122,4 +137,89 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void
(new Process(['make', 'clean'], $downloadedPackage->extractedSourcePath))->mustRun();
(new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun();
}
+
+ #[DataProvider('phpPathProvider')]
+ public function testUnixInstallCanInstallPrePackagedBinary(string $phpConfig): void
+ {
+ assert($phpConfig !== '');
+ if (Platform::isWindows()) {
+ self::markTestSkipped('Unix build test cannot be run on Windows');
+ }
+
+ $output = new BufferIO();
+ $targetPlatform = TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable($phpConfig), null);
+ $extensionPath = $targetPlatform->phpBinaryPath->extensionPath();
+
+ // First build it (otherwise the test assets would need to have a binary for every test platform...)
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]);
+
+ $built = (new UnixBuild())->__invoke(
+ DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $composerPackage,
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('pie_test_ext'),
+ 'pie_test_ext',
+ '0.1.0',
+ null,
+ ),
+ self::TEST_EXTENSION_PATH,
+ ),
+ $targetPlatform,
+ ['--enable-pie_test_ext'],
+ $output,
+ null,
+ );
+
+ /**
+ * Move the built .so into a new path; this simulates a pre-packaged binary, which would not have Makefile etc
+ * so this ensures we're not accidentally relying on any build mechanism (`make install` or otherwise)
+ */
+ mkdir(self::TEST_PREBUILT_PATH, 0777, true);
+ $prebuiltBinaryFilePath = self::TEST_PREBUILT_PATH . DIRECTORY_SEPARATOR . 'pie_test_ext.so';
+ rename($built->filePath, $prebuiltBinaryFilePath);
+
+ $prebuiltBinaryFile = BinaryFile::fromFileWithSha256Checksum($prebuiltBinaryFilePath);
+
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage
+ ->method('getExtra')
+ ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::PrePackagedBinary->value]);
+
+ $installedSharedObject = (new UnixInstall(new SetupIniFile(new PickBestSetupIniApproach([]))))->__invoke(
+ DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $composerPackage,
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('pie_test_ext'),
+ 'pie_test_ext',
+ '0.1.0',
+ null,
+ ),
+ self::TEST_PREBUILT_PATH,
+ ),
+ $targetPlatform,
+ $prebuiltBinaryFile,
+ $output,
+ true,
+ );
+ $outputString = $output->getOutput();
+
+ self::assertStringContainsString('Install complete: ' . $extensionPath . '/pie_test_ext.so', $outputString);
+ self::assertStringContainsString('You must now add "extension=pie_test_ext" to your php.ini', $outputString);
+
+ self::assertSame($extensionPath . '/pie_test_ext.so', $installedSharedObject->filePath);
+ self::assertFileExists($installedSharedObject->filePath);
+
+ $rmCommand = ['rm', $installedSharedObject->filePath];
+ if (! is_writable($installedSharedObject->filePath)) {
+ array_unshift($rmCommand, 'sudo');
+ }
+
+ (new Process($rmCommand))->mustRun();
+ unlink($prebuiltBinaryFile->filePath);
+ }
}
diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php
index 086fe026..a4142da6 100644
--- a/test/integration/Installing/WindowsInstallTest.php
+++ b/test/integration/Installing/WindowsInstallTest.php
@@ -71,7 +71,7 @@ public function testWindowsInstallCanInstallExtension(): void
$installer = new WindowsInstall(new SetupIniFile(new PickBestSetupIniApproach([])));
- $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, $output, true);
+ $installedDll = $installer->__invoke($downloadedPackage, $targetPlatform, null, $output, true);
self::assertSame($extensionPath . '\php_pie_test_ext.dll', $installedDll->filePath);
$outputString = $output->getOutput();
diff --git a/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php b/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php
new file mode 100644
index 00000000..7b0b0fce
--- /dev/null
+++ b/test/unit/ComposerIntegration/Listeners/CouldNotDetermineDownloadUrlMethodTest.php
@@ -0,0 +1,73 @@
+createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foo'),
+ 'foo/foo',
+ '1.2.3',
+ null,
+ );
+
+ $e = CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods(
+ $package,
+ [DownloadUrlMethod::PrePackagedBinary],
+ [DownloadUrlMethod::PrePackagedBinary->value => 'A bad thing happened downloading the binary'],
+ );
+
+ self::assertSame(
+ 'Could not download foo/foo using pre-packaged-binary method: A bad thing happened downloading the binary',
+ $e->getMessage(),
+ );
+ }
+
+ public function testMultipleDownloadUrlMethods(): void
+ {
+ $package = new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foo'),
+ 'foo/foo',
+ '1.2.3',
+ null,
+ );
+
+ $e = CouldNotDetermineDownloadUrlMethod::fromDownloadUrlMethods(
+ $package,
+ [
+ DownloadUrlMethod::PrePackagedBinary,
+ DownloadUrlMethod::PrePackagedSourceDownload,
+ ],
+ [
+ DownloadUrlMethod::PrePackagedBinary->value => 'A bad thing happened downloading the binary',
+ DownloadUrlMethod::PrePackagedSourceDownload->value => 'Another bad thing happened downloading the source',
+ ],
+ );
+
+ self::assertSame(
+ 'Could not download foo/foo using the following methods:
+ - pre-packaged-binary: A bad thing happened downloading the binary
+ - pre-packaged-source: Another bad thing happened downloading the source
+',
+ $e->getMessage(),
+ );
+ }
+}
diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php
index 5a356b8e..657a2aab 100644
--- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php
+++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php
@@ -12,10 +12,13 @@
use Composer\IO\IOInterface;
use Composer\Package\CompletePackage;
use Composer\Package\Package;
+use Php\Pie\ComposerIntegration\Listeners\CouldNotDetermineDownloadUrlMethod;
use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
+use Php\Pie\Downloading\DownloadUrlMethod;
+use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset;
use Php\Pie\Downloading\PackageReleaseAssets;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
@@ -256,6 +259,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void
'https://example.com/git-archive-zip-url',
$composerPackage->getDistUrl(),
);
+ self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage));
}
public function testDistUrlIsUpdatedForWindowsInstallers(): void
@@ -310,6 +314,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void
'https://example.com/windows-download-url',
$composerPackage->getDistUrl(),
);
+ self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, DownloadUrlMethod::fromComposerPackage($composerPackage));
}
public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void
@@ -369,6 +374,187 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void
'https://example.com/pre-packaged-source-download-url.tgz',
$composerPackage->getDistUrl(),
);
+ self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromComposerPackage($composerPackage));
self::assertSame('tar', $composerPackage->getDistType());
}
+
+ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): void
+ {
+ $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
+ $composerPackage->setDistType('zip');
+ $composerPackage->setDistUrl('https://example.com/git-archive-zip-url');
+ $composerPackage->setPhpExt([
+ 'extension-name' => 'foobar',
+ 'download-url-method' => ['pre-packaged-binary', 'composer-default'],
+ ]);
+
+ $installerEvent = new InstallerEvent(
+ InstallerEvents::PRE_OPERATIONS_EXEC,
+ $this->composer,
+ $this->io,
+ false,
+ true,
+ new Transaction([], [$composerPackage]),
+ );
+
+ $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class);
+ $packageReleaseAssets
+ ->expects(self::once())
+ ->method('findMatchingReleaseAssetUrl')
+ ->willReturn('https://example.com/pre-packaged-binary-download-url.tgz');
+
+ $this->container
+ ->method('get')
+ ->with(PackageReleaseAssets::class)
+ ->willReturn($packageReleaseAssets);
+
+ (new OverrideDownloadUrlInstallListener(
+ $this->composer,
+ $this->io,
+ $this->container,
+ new PieComposerRequest(
+ $this->createMock(IOInterface::class),
+ new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ PhpBinaryPath::fromCurrentProcess(),
+ Architecture::x86_64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ WindowsCompiler::VC15,
+ ),
+ new RequestedPackageAndVersion('foo/bar', '^1.1'),
+ PieOperation::Install,
+ [],
+ null,
+ false,
+ ),
+ ))($installerEvent);
+
+ self::assertSame(
+ 'https://example.com/pre-packaged-binary-download-url.tgz',
+ $composerPackage->getDistUrl(),
+ );
+ self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage));
+ self::assertSame('tar', $composerPackage->getDistType());
+ }
+
+ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound(): void
+ {
+ $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
+ $composerPackage->setDistType('zip');
+ $composerPackage->setDistUrl('https://example.com/git-archive-zip-url');
+ $composerPackage->setPhpExt([
+ 'extension-name' => 'foobar',
+ 'download-url-method' => ['pre-packaged-binary', 'composer-default'],
+ ]);
+
+ $installerEvent = new InstallerEvent(
+ InstallerEvents::PRE_OPERATIONS_EXEC,
+ $this->composer,
+ $this->io,
+ false,
+ true,
+ new Transaction([], [$composerPackage]),
+ );
+
+ $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class);
+ $packageReleaseAssets
+ ->expects(self::once())
+ ->method('findMatchingReleaseAssetUrl')
+ ->willThrowException(new CouldNotFindReleaseAsset('nope not found'));
+
+ $this->container
+ ->method('get')
+ ->with(PackageReleaseAssets::class)
+ ->willReturn($packageReleaseAssets);
+
+ (new OverrideDownloadUrlInstallListener(
+ $this->composer,
+ $this->io,
+ $this->container,
+ new PieComposerRequest(
+ $this->createMock(IOInterface::class),
+ new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ PhpBinaryPath::fromCurrentProcess(),
+ Architecture::x86_64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ WindowsCompiler::VC15,
+ ),
+ new RequestedPackageAndVersion('foo/bar', '^1.1'),
+ PieOperation::Install,
+ [],
+ null,
+ false,
+ ),
+ ))($installerEvent);
+
+ self::assertSame(
+ 'https://example.com/git-archive-zip-url',
+ $composerPackage->getDistUrl(),
+ );
+ self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, DownloadUrlMethod::fromComposerPackage($composerPackage));
+ self::assertSame('zip', $composerPackage->getDistType());
+ }
+
+ public function testNoSelectedDownloadUrlMethodWillThrowException(): void
+ {
+ $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
+ $composerPackage->setDistType('zip');
+ $composerPackage->setDistUrl('https://example.com/git-archive-zip-url');
+ $composerPackage->setPhpExt([
+ 'extension-name' => 'foobar',
+ 'download-url-method' => ['pre-packaged-binary'],
+ ]);
+
+ $installerEvent = new InstallerEvent(
+ InstallerEvents::PRE_OPERATIONS_EXEC,
+ $this->composer,
+ $this->io,
+ false,
+ true,
+ new Transaction([], [$composerPackage]),
+ );
+
+ $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class);
+ $packageReleaseAssets
+ ->expects(self::once())
+ ->method('findMatchingReleaseAssetUrl')
+ ->willThrowException(new CouldNotFindReleaseAsset('nope not found'));
+
+ $this->container
+ ->method('get')
+ ->with(PackageReleaseAssets::class)
+ ->willReturn($packageReleaseAssets);
+
+ $listener = new OverrideDownloadUrlInstallListener(
+ $this->composer,
+ $this->io,
+ $this->container,
+ new PieComposerRequest(
+ $this->createMock(IOInterface::class),
+ new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ PhpBinaryPath::fromCurrentProcess(),
+ Architecture::x86_64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ WindowsCompiler::VC15,
+ ),
+ new RequestedPackageAndVersion('foo/bar', '^1.1'),
+ PieOperation::Install,
+ [],
+ null,
+ false,
+ ),
+ );
+
+ $this->expectException(CouldNotDetermineDownloadUrlMethod::class);
+ $this->expectExceptionMessage('Could not download foo/bar using pre-packaged-binary method: nope not found');
+ $listener($installerEvent);
+ }
}
diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php
index ea8b3080..14576c3b 100644
--- a/test/unit/DependencyResolver/PackageTest.php
+++ b/test/unit/DependencyResolver/PackageTest.php
@@ -8,6 +8,7 @@
use Composer\Package\CompletePackageInterface;
use InvalidArgumentException;
use Php\Pie\DependencyResolver\Package;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Platform\OperatingSystemFamily;
@@ -146,4 +147,33 @@ public function testFromComposerCompletePackageWithBuildPath(): void
self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion());
self::assertSame('some/subdirectory/path/', $package->buildPath());
}
+
+ public function testFromComposerCompletePackageWithStringDownloadUrlMethod(): void
+ {
+ $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3');
+ $composerCompletePackage->setPhpExt(['download-url-method' => 'pre-packaged-binary']);
+
+ self::assertSame(
+ [DownloadUrlMethod::PrePackagedBinary],
+ Package::fromComposerCompletePackage($composerCompletePackage)->supportedDownloadUrlMethods(),
+ );
+ }
+
+ public function testFromComposerCompletePackageWithListDownloadUrlMethods(): void
+ {
+ $composerCompletePackage = new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3');
+ $composerCompletePackage->setPhpExt(['download-url-method' => ['pre-packaged-binary', 'composer-default']]);
+
+ self::assertSame(
+ [DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::ComposerDefaultDownload],
+ Package::fromComposerCompletePackage($composerCompletePackage)->supportedDownloadUrlMethods(),
+ );
+ }
+
+ public function testFromComposerCompletePackageWithOmittedDownloadUrlMethod(): void
+ {
+ self::assertNull(Package::fromComposerCompletePackage(
+ new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'),
+ )->supportedDownloadUrlMethods());
+ }
}
diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php
index 26712bec..b1113b9d 100644
--- a/test/unit/Downloading/DownloadUrlMethodTest.php
+++ b/test/unit/Downloading/DownloadUrlMethodTest.php
@@ -4,12 +4,15 @@
namespace Php\PieUnitTest\Downloading;
+use Composer\Package\CompletePackage;
use Composer\Package\CompletePackageInterface;
use Php\Pie\DependencyResolver\Package;
+use Php\Pie\Downloading\DownloadedPackage;
use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Platform\Architecture;
+use Php\Pie\Platform\DebugBuild;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
@@ -18,6 +21,9 @@
use Php\Pie\Platform\WindowsCompiler;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
+use ValueError;
+
+use function array_key_first;
#[CoversClass(DownloadUrlMethod::class)]
final class DownloadUrlMethodTest extends TestCase
@@ -48,7 +54,10 @@ public function testWindowsPackages(): void
WindowsCompiler::VC15,
);
- $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform);
+ $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform);
+
+ self::assertCount(1, $downloadUrlMethods);
+ $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)];
self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, $downloadUrlMethod);
@@ -81,7 +90,10 @@ public function testPrePackagedSourceDownloads(): void
null,
);
- $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform);
+ $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform);
+
+ self::assertCount(1, $downloadUrlMethods);
+ $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)];
self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $downloadUrlMethod);
@@ -95,6 +107,50 @@ public function testPrePackagedSourceDownloads(): void
);
}
+ public function testPrePackagedBinaryDownloads(): void
+ {
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage->method('getPrettyName')->willReturn('foo/bar');
+ $composerPackage->method('getPrettyVersion')->willReturn('1.2.3');
+ $composerPackage->method('getType')->willReturn('php-ext');
+ $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => ['pre-packaged-binary']]);
+
+ $package = Package::fromComposerCompletePackage($composerPackage);
+
+ $phpBinaryPath = $this->createMock(PhpBinaryPath::class);
+ $phpBinaryPath
+ ->method('majorMinorVersion')
+ ->willReturn('8.3');
+ $phpBinaryPath
+ ->method('debugMode')
+ ->willReturn(DebugBuild::Debug);
+
+ $targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $phpBinaryPath,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform);
+
+ self::assertCount(1, $downloadUrlMethods);
+ $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)];
+
+ self::assertSame(DownloadUrlMethod::PrePackagedBinary, $downloadUrlMethod);
+
+ self::assertSame(
+ [
+ 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-zts.zip',
+ 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-zts.tgz',
+ ],
+ $downloadUrlMethod->possibleAssetNames($package, $targetPlatform),
+ );
+ }
+
public function testComposerDefaultDownload(): void
{
$package = new Package(
@@ -116,10 +172,108 @@ public function testComposerDefaultDownload(): void
null,
);
- $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform);
+ $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform);
+
+ self::assertCount(1, $downloadUrlMethods);
+ $downloadUrlMethod = $downloadUrlMethods[array_key_first($downloadUrlMethods)];
self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $downloadUrlMethod);
self::assertNull($downloadUrlMethod->possibleAssetNames($package, $targetPlatform));
}
+
+ public function testMultipleDownloadUrlMethods(): void
+ {
+ $composerPackage = $this->createMock(CompletePackageInterface::class);
+ $composerPackage->method('getPrettyName')->willReturn('foo/bar');
+ $composerPackage->method('getPrettyVersion')->willReturn('1.2.3');
+ $composerPackage->method('getType')->willReturn('php-ext');
+ $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => ['pre-packaged-binary', 'pre-packaged-source', 'composer-default']]);
+
+ $package = Package::fromComposerCompletePackage($composerPackage);
+
+ $phpBinaryPath = $this->createMock(PhpBinaryPath::class);
+ $phpBinaryPath
+ ->method('majorMinorVersion')
+ ->willReturn('8.3');
+ $phpBinaryPath
+ ->method('debugMode')
+ ->willReturn(DebugBuild::Debug);
+
+ $targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $phpBinaryPath,
+ Architecture::x86_64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ null,
+ );
+
+ $downloadUrlMethods = DownloadUrlMethod::possibleDownloadUrlMethodsForPackage($package, $targetPlatform);
+
+ self::assertCount(3, $downloadUrlMethods);
+
+ $firstMethod = $downloadUrlMethods[0];
+ self::assertSame(DownloadUrlMethod::PrePackagedBinary, $firstMethod);
+ self::assertSame(
+ [
+ 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug.zip',
+ 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug.tgz',
+ 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-nts.zip',
+ 'php_bar-1.2.3_php8.3-x86_64-linux-glibc-debug-nts.tgz',
+ ],
+ $firstMethod->possibleAssetNames($package, $targetPlatform),
+ );
+
+ $secondMethod = $downloadUrlMethods[1];
+ self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $secondMethod);
+ self::assertSame(
+ [
+ 'php_bar-1.2.3-src.tgz',
+ 'php_bar-1.2.3-src.zip',
+ 'bar-1.2.3.tgz',
+ ],
+ $secondMethod->possibleAssetNames($package, $targetPlatform),
+ );
+
+ $thirdMethod = $downloadUrlMethods[2];
+ self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $thirdMethod);
+ self::assertNull($thirdMethod->possibleAssetNames($package, $targetPlatform));
+ }
+
+ public function testFromComposerPackageWhenPackageKeyWasDefined(): void
+ {
+ $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
+ DownloadUrlMethod::PrePackagedBinary->writeToComposerPackage($composerPackage);
+ self::assertSame(DownloadUrlMethod::PrePackagedBinary, DownloadUrlMethod::fromComposerPackage($composerPackage));
+ }
+
+ public function testFromComposerPackageWhenPackageKeyWasNotDefined(): void
+ {
+ $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
+
+ $this->expectException(ValueError::class);
+ DownloadUrlMethod::fromComposerPackage($composerPackage);
+ }
+
+ public function testFromDownloadedPackage(): void
+ {
+ $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3');
+ DownloadUrlMethod::PrePackagedSourceDownload->writeToComposerPackage($composerPackage);
+
+ $downloaded = DownloadedPackage::fromPackageAndExtractedPath(
+ new Package(
+ $composerPackage,
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foo'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ ),
+ '/path/to/extracted/source',
+ );
+
+ self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, DownloadUrlMethod::fromDownloadedPackage($downloaded));
+ }
}
diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
index d7b153d3..32bd161c 100644
--- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
+++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php
@@ -6,6 +6,7 @@
use Composer\Package\CompletePackageInterface;
use Php\Pie\DependencyResolver\Package;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
@@ -44,6 +45,7 @@ public function testForPackageWithRegularPackage(): void
null,
),
$package,
+ DownloadUrlMethod::PrePackagedSourceDownload,
['something.zip', 'something2.zip'],
);
@@ -72,6 +74,7 @@ public function testForPackageWithWindowsPackage(): void
WindowsCompiler::VS17,
),
$package,
+ DownloadUrlMethod::WindowsBinaryDownload,
['something.zip', 'something2.zip'],
);
@@ -89,7 +92,7 @@ public function testForPackageWithMissingTag(): void
null,
);
- $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package);
+ $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package, DownloadUrlMethod::PrePackagedSourceDownload);
self::assertSame('Could not find release by tag name for foo/bar:1.2.3', $exception->getMessage());
}
diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
index eb2985fd..4002a366 100644
--- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
+++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php
@@ -9,6 +9,7 @@
use Composer\Util\Http\Response;
use Composer\Util\HttpDownloader;
use Php\Pie\DependencyResolver\Package;
+use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\Downloading\Exception\CouldNotFindReleaseAsset;
use Php\Pie\Downloading\GithubPackageReleaseAssets;
use Php\Pie\ExtensionName;
@@ -86,6 +87,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void
$targetPlatform,
$package,
$httpDownloader,
+ DownloadUrlMethod::WindowsBinaryDownload,
WindowsExtensionAssetName::zipNames(
$targetPlatform,
$package,
@@ -151,6 +153,7 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr
$targetPlatform,
$package,
$httpDownloader,
+ DownloadUrlMethod::WindowsBinaryDownload,
WindowsExtensionAssetName::zipNames(
$targetPlatform,
$package,
@@ -196,6 +199,7 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF
$targetPlatform,
$package,
$httpDownloader,
+ DownloadUrlMethod::WindowsBinaryDownload,
WindowsExtensionAssetName::zipNames(
$targetPlatform,
$package,
diff --git a/test/unit/Platform/LibcFlavourTest.php b/test/unit/Platform/LibcFlavourTest.php
new file mode 100644
index 00000000..231462e8
--- /dev/null
+++ b/test/unit/Platform/LibcFlavourTest.php
@@ -0,0 +1,63 @@
+createMock(PhpBinaryPath::class);
+ $php->method('debugMode')->willReturn(DebugBuild::NoDebug);
+ $php->method('majorMinorVersion')->willReturn('8.2');
+
+ $targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $php,
+ Architecture::x86_64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ null,
+ );
+
+ $libc = $targetPlatform->libcFlavour();
+ self::assertSame(
+ [
+ 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '.zip',
+ 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '.tgz',
+ 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '-nts.zip',
+ 'php_foobar-1.2.3_php8.2-x86_64-linux-' . $libc->value . '-nts.tgz',
+ ],
+ PrePackagedBinaryAssetName::packageNames(
+ $targetPlatform,
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ ),
+ ),
+ );
+ }
+
+ public function testPackageNamesZts(): void
+ {
+ $php = $this->createMock(PhpBinaryPath::class);
+ $php->method('debugMode')->willReturn(DebugBuild::NoDebug);
+ $php->method('majorMinorVersion')->willReturn('8.3');
+
+ $targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $php,
+ Architecture::x86_64,
+ ThreadSafetyMode::ThreadSafe,
+ 1,
+ null,
+ );
+
+ $libc = $targetPlatform->libcFlavour();
+ self::assertSame(
+ [
+ 'php_foobar-1.2.3_php8.3-x86_64-linux-' . $libc->value . '-zts.zip',
+ 'php_foobar-1.2.3_php8.3-x86_64-linux-' . $libc->value . '-zts.tgz',
+ ],
+ PrePackagedBinaryAssetName::packageNames(
+ $targetPlatform,
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ ),
+ ),
+ );
+ }
+
+ public function testPackageNamesDebug(): void
+ {
+ $php = $this->createMock(PhpBinaryPath::class);
+ $php->method('debugMode')->willReturn(DebugBuild::Debug);
+ $php->method('majorMinorVersion')->willReturn('8.4');
+
+ $targetPlatform = new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Darwin,
+ $php,
+ Architecture::arm64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ null,
+ );
+
+ $libc = $targetPlatform->libcFlavour();
+ self::assertSame(
+ [
+ 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug.zip',
+ 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug.tgz',
+ 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug-nts.zip',
+ 'php_foobar-1.2.3_php8.4-arm64-darwin-' . $libc->value . '-debug-nts.tgz',
+ ],
+ PrePackagedBinaryAssetName::packageNames(
+ $targetPlatform,
+ new Package(
+ $this->createMock(CompletePackageInterface::class),
+ ExtensionType::PhpModule,
+ ExtensionName::normaliseFromString('foobar'),
+ 'foo/bar',
+ '1.2.3',
+ null,
+ ),
+ ),
+ );
+ }
+}
diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
index 80badbc2..ce2db33e 100644
--- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
+++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
@@ -8,6 +8,7 @@
use Composer\Util\Platform;
use Php\Pie\ExtensionName;
use Php\Pie\Platform\Architecture;
+use Php\Pie\Platform\DebugBuild;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded;
@@ -445,4 +446,26 @@ public function testBuildProviderNullWhenNotConfigured(): void
self::assertNull($phpBinary->buildProvider());
}
+
+ public function testDebugBuildModeReturnsDebugWhenYes(): void
+ {
+ $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
+
+ $phpBinary->expects(self::once())
+ ->method('phpinfo')
+ ->willReturn('Debug Build => no');
+
+ self::assertSame(DebugBuild::NoDebug, $phpBinary->debugMode());
+ }
+
+ public function testDebugBuildModeReturnsNoDebugWhenNo(): void
+ {
+ $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
+
+ $phpBinary->expects(self::once())
+ ->method('phpinfo')
+ ->willReturn('Debug Build => yes');
+
+ self::assertSame(DebugBuild::Debug, $phpBinary->debugMode());
+ }
}
diff --git a/test/unit/Platform/TargetPlatformTest.php b/test/unit/Platform/TargetPlatformTest.php
index 3753181a..48c79df3 100644
--- a/test/unit/Platform/TargetPlatformTest.php
+++ b/test/unit/Platform/TargetPlatformTest.php
@@ -107,4 +107,12 @@ public function testLinuxPlatform(): void
self::assertSame(ThreadSafetyMode::NonThreadSafe, $platform->threadSafety);
self::assertSame(Architecture::x86_64, $platform->architecture);
}
+
+ public function testLibcFlavourIsMemoized(): void
+ {
+ self::assertSame(
+ TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null)->libcFlavour(),
+ TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromCurrentProcess(), null)->libcFlavour(),
+ );
+ }
}