Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions docs/extension-maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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

Expand Down
10 changes: 5 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 22 additions & 4 deletions resources/composer-json-php-ext-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions src/Building/ExtensionBinaryNotFound.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
41 changes: 41 additions & 0 deletions src/Building/UnixBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
'<info>Pre-packaged binary found:</info> %s',
$expectedSoFile,
));

return BinaryFile::fromFileWithSha256Checksum($expectedSoFile);
}

/** @param list<non-empty-string> $configureOptions */
private function buildFromSource(
DownloadedPackage $downloadedPackage,
TargetPlatform $targetPlatform,
array $configureOptions,
IOInterface $io,
PhpizePath|null $phpizePath,
): BinaryFile {
$outputCallback = null;
if ($io->isVerbose()) {
Expand Down
2 changes: 2 additions & 0 deletions src/ComposerIntegration/InstallAndBuildProcess.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function __invoke(
$composerPackage,
);

$builtBinaryFile = null;
if ($composerRequest->operation->shouldBuild()) {
$builtBinaryFile = ($this->pieBuild)(
$downloadedPackage,
Expand Down Expand Up @@ -75,6 +76,7 @@ public function __invoke(
($this->pieInstall)(
$downloadedPackage,
$composerRequest->targetPlatform,
$builtBinaryFile,
$io,
$composerRequest->attemptToSetupIniFile,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace Php\Pie\ComposerIntegration\Listeners;

use Php\Pie\DependencyResolver\Package;
use Php\Pie\Downloading\DownloadUrlMethod;
use RuntimeException;

use function array_key_first;
use function count;
use function sprintf;

use const PHP_EOL;

class CouldNotDetermineDownloadUrlMethod extends RuntimeException
{
/**
* @param non-empty-list<DownloadUrlMethod> $downloadMethods
* @param array<string, string> $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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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('<info>Selected download URL method: ' . $selectedDownloadUrlMethod->value . '</info>', verbosity: IOInterface::VERBOSE);
},
);
}
Expand Down
Loading
Loading