diff --git a/src/configs/pie-extensions.json b/src/configs/pie-extensions.json new file mode 100644 index 00000000..02319bde --- /dev/null +++ b/src/configs/pie-extensions.json @@ -0,0 +1,111 @@ +{ + "amqp": "php-amqp/php-amqp", + "apcu": "apcu/apcu", + "apfd": "m6w6/ext-apfd", + "arrow": "flow-php/arrow-ext", + "ast": "nikic/php-ast", + "biscuit_php": "ptondereau/biscuit-php", + "bitset": "pecl/bitset", + "brotli": "kjdev/brotli", + "bsdiff": "bsdiff/bsdiff", + "csv": "girgias/csv", + "dbus": "pecl/dbus", + "decimal": "php-decimal/ext-decimal", + "deepclone": "symfony/deepclone", + "dio": "pecl/dio", + "ds": "php-ds/ext-ds", + "ev": "osmanov/pecl-ev", + "event": "osmanov/pecl-event", + "excel": "iliaal/php-excel", + "excimer": "wikimedia/excimer", + "fast_uuid": "iliaal/fast_uuid", + "fastchart": "iliaal/fastchart", + "fastjson": "iliaal/fastjson", + "geospatial": "php-geospatial/geospatial", + "glfw": "mario-deluna/php-glfw", + "grpc": "grpc/grpc-php-ext", + "hdr_histogram": "beberlei/hdrhistogram", + "hdrhistogram": "beberlei/hdrhistogram", + "highlighter": "yiipress/highlighter", + "http": "m6w6/ext-http", + "igbinary": "igbinary/igbinary", + "imagick": "imagick/imagick", + "inotify": "arnaud-lb/inotify", + "ip2location": "ip2location/ip2location-pie", + "ip2proxy": "ip2location/ip2proxy-pie", + "json_post": "m6w6/ext-json_post", + "jsonpath": "supermetrics/jsonpath", + "judy": "orieg/judy", + "krb5": "pecl/krb5", + "lchash": "iliaal/lchash", + "luasandbox": "wikimedia/luasandbox", + "lz4": "kjdev/lz4", + "lzf": "pecl/lzf", + "mailparse": "pecl/mailparse", + "maxminddb": "maxmind-db/reader-ext", + "mcrypt": "pecl/mcrypt", + "mdparser": "iliaal/mdparser", + "memcached": "php-memcached/php-memcached", + "memprof": "arnaud-lb/memprof", + "mjml": "kcs/mjml", + "mongodb": "mongodb/mongodb-extension", + "msgpack": "msgpack/msgpack-php", + "mustache": "jbboehr/php-mustache", + "oci8": "oci8/oci8", + "openswoole": "openswoole/ext-openswoole", + "opentelemetry": "open-telemetry/ext-opentelemetry", + "operator": "jblo/operator", + "parallel": "pecl/parallel", + "pcov": "pecl/pcov", + "pdo_duckdb": "thomas-0816/pdo-duckdb-php", + "pdo_oci": "pecl/pdo_oci", + "pdo_sqlsrv": "microsoft/pdo_sqlsrv", + "pecl_http": "m6w6/ext-http", + "phalcon": "phalcon/cphalcon", + "phonetic": "iliaal/phonetic", + "phpser": "iliaal/phpser", + "phpstan_turbo": "phpstan/turbo", + "pkcs11": "gamringer/pkcs11", + "pq": "m6w6/ext-pq", + "quickhash": "derickr/quickhash", + "raphf": "m6w6/ext-raphf", + "rar": "cataphract/rar", + "rdkafka": "rdkafka/rdkafka", + "redis": "phpredis/phpredis", + "relay": "cachewerk/ext-relay", + "rpminfo": "remi/rpminfo", + "scrypt": "woohoolabs/php-scrypt", + "selinux": "pecl/selinux", + "snappy": "kjdev/snappy", + "solr": "pecl/solr", + "spx": "noisebynorthwest/php-spx", + "sqlsrv": "microsoft/sqlsrv", + "statgrab": "iliaal/statgrab", + "swoole": "swoole/swoole", + "swow": "swow/swow-extension", + "taint": "laruence/taint", + "tensor": "rubix/tensor_ext", + "timezonedb": "pecl/timezonedb", + "translit": "derickr/translit", + "uopz": "krakjoe/uopz", + "uuid": "pecl/uuid", + "uv": "amphp/uv", + "valkey_glide": "valkey-io/valkey-glide-php", + "vld": "derickr/vld", + "wikidiff2": "wikimedia/wikidiff2", + "win32service": "win32service/win32service", + "xattr": "pecl/xattr", + "xdebug": "xdebug/xdebug", + "xdiff": "pecl/xdiff", + "xlswriter": "viest/xlswriter", + "xmldiff": "pecl/xml-xmldiff", + "xpass": "remi/xpass", + "yac": "laruence/yac", + "yaconf": "laruence/yaconf", + "yaf": "laruence/yaf", + "yaml": "pecl/yaml", + "yar": "laruence/yar", + "yaz": "indexdata/yaz", + "zip": "pecl/zip", + "zstd": "kjdev/zstd" +} diff --git a/src/scripts/extensions/add_extensions.ps1 b/src/scripts/extensions/add_extensions.ps1 index 60cec0fe..b9dfb5c3 100644 --- a/src/scripts/extensions/add_extensions.ps1 +++ b/src/scripts/extensions/add_extensions.ps1 @@ -85,6 +85,75 @@ Function Add-ExtensionFromGithub { } } +# Function to install a resolved binary using the existing extension configuration. +Function Add-PieBinary { + Param ($release) + $ErrorActionPreference = 'Stop' + $extension = $release.extension + $download_dir = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $download_dir -Force >$null + try { + $archive = Join-Path $download_dir $release.name + Get-File -Url $release.url -OutFile $archive + if ($release.digest -and $release.digest -ne "sha256:$((Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant())") { + throw "Checksum verification failed for $extension" + } + $extract_dir = Join-Path $download_dir 'extension' + New-Item -ItemType Directory -Path $extract_dir -Force >$null + if ($archive.EndsWith('.dll')) { + Copy-Item $archive -Destination "$extract_dir\php_$extension.dll" + } elseif ($archive.EndsWith('.zip')) { + Expand-Archive -Path $archive -DestinationPath $extract_dir -Force + } else { + & tar -xzf $archive -C $extract_dir + if ($LASTEXITCODE -ne 0) { throw "Could not extract $extension" } + } + $dlls = @(Get-ChildItem $extract_dir -Recurse -File -Filter '*.dll') + $binary = @($dlls | Where-Object { $_.Name -eq "php_$extension.dll" -or $_.Name -like "php_$extension-*.dll" }) + if ($binary.Count -ne 1) { throw "Could not find the prebuilt DLL for $extension" } + $dlls | Where-Object { $_.FullName -ne $binary[0].FullName } | Copy-Item -Destination $php_dir -Force + Copy-Item $binary[0].FullName -Destination "$ext_dir\php_$extension.dll" -Force + $pdb = [System.IO.Path]::ChangeExtension($binary[0].FullName, '.pdb') + if (Test-Path $pdb) { Copy-Item $pdb -Destination "$ext_dir\php_$extension.pdb" -Force } + $extras_dir = Join-Path $php_dir "extras\$extension" + New-Item -ItemType Directory -Path $extras_dir -Force >$null + Copy-Item "$extract_dir\*" -Destination $extras_dir -Recurse -Force + Enable-ExtensionDependencies $extension + Enable-PhpExtension -Extension $extension -Path $php_dir + & "$php_dir\php.exe" -r 'exit(extension_loaded($argv[1]) ? 0 : 1);' -- $extension >$null 2>&1 + if ($LASTEXITCODE -ne 0) { throw "Could not load $extension after installing the prebuilt release" } + } finally { + Remove-Item $download_dir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# Function to install a mapped extension or vendor/package[:constraint]. +Function Add-PieExtension { + Param ( + [Parameter(Position = 0, Mandatory = $true)] + [ValidateNotNull()] + [string] + $extension, + [Parameter(Position = 1, Mandatory = $true)] + [ValidateNotNull()] + [string] + $package, + [Parameter(Position = 2, Mandatory = $false)] + [Alias('AllowSourceBuild')] + [bool] + $allow_source = $false + ) + $metadata = & php "$src\scripts\extensions\release.php" $extension $package 'none' "vs$($installed.VCVersion)" + if ($LASTEXITCODE -ne 0) { throw "Could not resolve $extension" } + $release = $metadata | ConvertFrom-Json + if ($release.kind -ne 'binary') { + if ($allow_source) { throw "No compatible binary for $extension $($release.version); source builds are not supported on Windows" } + throw "No compatible binary for $extension $($release.version); source builds are disabled" + } + Add-PieBinary $release + Add-Log $tick $extension 'Installed and enabled' +} + # Function to add PHP extensions. Function Add-Extension { Param ( diff --git a/src/scripts/extensions/add_extensions.sh b/src/scripts/extensions/add_extensions.sh index 963a9e74..efd8bb73 100644 --- a/src/scripts/extensions/add_extensions.sh +++ b/src/scripts/extensions/add_extensions.sh @@ -188,6 +188,73 @@ get_pecl_version() { echo "$response" | grep -Eio "$pattern" | head -n 1 | cut -d '>' -f 2 | cut -d '<' -f 1 } +# Install a resolved binary using the existing extension configuration. +pie_install_binary() ( + local metadata=$1 prefix=$2 extension archive binary digest + local download_dir + extension=$(echo "$metadata" | jq -r .extension) + download_dir=$(mktemp -d) || return 1 + trap 'sudo rm -rf "$download_dir"' EXIT + archive="$download_dir/$(echo "$metadata" | jq -r .name)" + [ "$(get -v -n "$archive" "$(echo "$metadata" | jq -r .url)")" = 200 ] || return 1 + digest=$(echo "$metadata" | jq -r '.digest // empty') + if [ -n "$digest" ]; then + verify_checksum "$archive" "$digest" || return 1 + fi + if [[ "$archive" = *.so ]]; then + binary="$archive" + else + if [[ "$archive" = *.zip ]]; then + unzip -qo "$archive" -d "$download_dir" || return 1 + else + tar -xzf "$archive" -C "$download_dir" || return 1 + fi + binary=$(find "$download_dir" -type f -name "$extension.so" -print) + fi + [ -f "$binary" ] || return 1 + sudo rm -f "${ext_dir:?}/$extension.so" + sudo cp "$binary" "$ext_dir/$extension.so" || return 1 + sudo rm -f /tmp/php"${version:?}"_extensions + enable_extension "$extension" "${prefix:-$(echo "$metadata" | jq -r .prefix)}" + check_extension "$extension" +) + +# Resolve a mapped extension or package[:version]; return 2 when source is required but disabled. +pie_install() { + local extension=$1 package=$2 allow_source=${3:-false} prefix=${4:-} + local metadata libc source_url reference repository origin organization build_path + libc=bsdlibc + if [ "$(uname -s)" = Linux ]; then + libc=glibc + ldd --version 2>&1 | grep -qi musl && libc=musl + fi + metadata=$(php "${src:?}/scripts/extensions/release.php" "$extension" "$package" "$libc") || return 1 + if [ "$(echo "$metadata" | jq -r .kind)" = binary ]; then + pie_install_binary "$metadata" "$prefix" + return $? + fi + [ "$allow_source" = true ] || return 2 + source_url=$(echo "$metadata" | jq -r '.source.url // empty') + reference=$(echo "$metadata" | jq -r '.source.reference // empty') + [[ "$(echo "$metadata" | jq -r '.source.type // empty')" = git && "$source_url" =~ ^https://[a-zA-Z0-9._~:/-]+$ && "$reference" =~ ^[a-fA-F0-9]{7,64}$ ]] || return 1 + source_url=${source_url%.git} + repository=${source_url##*/} + organization=${source_url#https://} + origin="https://${organization%%/*}" + organization=${organization#*/} + organization=${organization%/*} + extension=$(echo "$metadata" | jq -r .extension) + prefix=${prefix:-$(echo "$metadata" | jq -r .prefix)} + build_path=$(echo "$metadata" | jq -r .buildPath) + if [ -n "$build_path" ] && [ -z "$(parse_args "$extension" PATH)" ]; then + [[ "$build_path" != /* && ! "$build_path" =~ (^|/)\.\.(/|$) ]] || return 1 + local "${extension}_PATH=$build_path" + fi + add_extension_from_source "$extension" "$origin" "$organization" "$repository" "$reference" "$prefix" + sudo rm -f /tmp/php"${version:?}"_extensions + check_extension "$extension" +} + # Function to install PECL extensions and accept default options pecl_install() { local extension=$1 diff --git a/src/scripts/extensions/release.php b/src/scripts/extensions/release.php new file mode 100644 index 00000000..b7d9a6fc --- /dev/null +++ b/src/scripts/extensions/release.php @@ -0,0 +1,224 @@ + 'alpha', 'b' => 'beta', 'p' => 'pl', 'rc' => 'RC'); + $label = isset($match[2]) ? strtolower($match[2]) : 'stable'; + $label = isset($labels[$label]) ? $labels[$label] : $label; + $suffix = $label === 'stable' ? '' : '-' . $label . (isset($match[3]) ? $match[3] : ''); + return implode('.', array_pad(explode('.', $match[1]), 4, '0')) . $suffix; + } + + private static function bound($parts, $index) + { + $parts = array_pad($parts, 4, 0); + $parts[$index]++; + for ($i = $index + 1; $i < 4; $i++) { $parts[$i] = 0; } + return implode('.', $parts) . '-dev'; + } + + // Package PHP requirements use Composer's public range syntax, without its installer. + public static function satisfies($version, $constraint) + { + $version = self::version($version); + $constraint = preg_replace('/@(dev|alpha|beta|RC|stable)\b/i', '', trim($constraint)); + foreach (preg_split('/\s*\|\|?\s*/', $constraint) as $alternative) { + $alternative = preg_replace_callback('/\bv?(\d+(?:\.\d+){0,3})\s+-\s+v?(\d+(?:\.\d+){0,3})\b/', function ($match) { + $upper = explode('.', $match[2]); + return '>=' . $match[1] . ' ' . (count($upper) < 3 ? '<' . self::bound($upper, count($upper) - 1) : '<=' . $match[2]); + }, $alternative); + $alternative = preg_replace('/([<>!=~^]+)\s+(?=v?\d)/', '$1', trim($alternative)); + if ($alternative === '') { throw new RuntimeException('Empty version constraint'); } + $matches = true; + foreach (preg_split('/[\s,]+/', $alternative) as $term) { + if (!self::matchesTerm($version, $term)) { $matches = false; } + } + if ($matches) { return true; } + } + return false; + } + + private static function matchesTerm($version, $term) + { + if (!preg_match('/^(>=|<=|!=|<>|==|>|<|=|\^|~)?(.*)$/', $term, $match)) { + throw new RuntimeException('Invalid version constraint: ' . $term); + } + $operator = $match[1] ?: '=='; + $target = $match[2]; + if ($target === '*' || strtolower($target) === 'x') { return $operator !== '!=' && $operator !== '<>'; } + if (preg_match('/^v?(\d+(?:\.\d+)*)\.(?:\*|x)(?:\.(?:\*|x))*$/i', $target, $wildcard)) { + if (!in_array($operator, array('==', '=', '!=', '<>'), true)) { throw new RuntimeException('Invalid wildcard constraint: ' . $term); } + $parts = explode('.', $wildcard[1]); + $inside = version_compare($version, self::version($wildcard[1]) . '-dev', '>=') && version_compare($version, self::bound($parts, count($parts) - 1), '<'); + return $operator === '!=' || $operator === '<>' ? !$inside : $inside; + } + $normal = self::version($target); + $hasStability = (bool) preg_match('/^v?\d+(?:\.\d+){0,3}-?(?:dev|alpha|beta|rc|stable|pl|a|b|p)\.?\d*(?:\+.*)?$/i', $target); + if ($operator === '^' || $operator === '~') { + preg_match('/^v?(\d+(?:\.\d+){0,3})/', $target, $numeric); + $parts = explode('.', $numeric[1]); + $index = $operator === '~' ? max(0, count($parts) - 2) : 0; + if ($operator === '^') { + while ($index < count($parts) - 1 && (int) $parts[$index] === 0) { $index++; } + } + return version_compare($version, $normal . ($hasStability ? '' : '-dev'), '>=') && version_compare($version, self::bound($parts, $index), '<'); + } + if (!$hasStability && ($operator === '>=' || $operator === '<')) { $normal .= '-dev'; } + return version_compare($version, $normal, $operator); + } + + public static function expand($metadata, $name) + { + if (!isset($metadata['packages'][$name])) { throw new RuntimeException('No release metadata for ' . $name); } + if (!isset($metadata['minified'])) { return $metadata['packages'][$name]; } + if ($metadata['minified'] !== 'composer/2.0') { throw new RuntimeException('Unsupported package metadata format'); } + $previous = array(); + $packages = array(); + foreach ($metadata['packages'][$name] as $changes) { + foreach ($changes as $key => $value) { + if ($value === '__unset') { unset($previous[$key]); } else { $previous[$key] = $value; } + } + $packages[] = $previous; + } + return $packages; + } + + public static function selectPackage($packages, $requested, $platform) + { + $constraint = in_array($requested, array('', 'latest', 'stable'), true) ? '*' : $requested; + if (preg_match('/^v?\d+(?:\.\d+)?$/i', $constraint)) { $constraint .= '.*'; } + $stabilities = array('dev' => 0, 'alpha' => 1, 'a' => 1, 'beta' => 2, 'b' => 2, 'rc' => 3, 'stable' => 4, 'pl' => 4, 'p' => 4); + $minimum = 4; + if (preg_match_all('/(?:@|\d[.-]?)(dev|alpha|beta|rc|a|b)\d*\b/i', $constraint, $matches)) { + foreach ($matches[1] as $stability) { $minimum = min($minimum, $stabilities[strtolower($stability)]); } + } + usort($packages, function ($a, $b) { return version_compare(self::version($b['version']), self::version($a['version'])); }); + foreach ($packages as $package) { + $version = self::version($package['version']); + $stability = preg_match('/-(dev|alpha|beta|rc|a|b|pl|p)/i', $version, $match) ? $stabilities[strtolower($match[1])] : 4; + if ($stability < $minimum || !self::satisfies($version, $constraint)) { continue; } + $options = isset($package['php-ext']) ? $package['php-ext'] : array(); + if (!isset($package['type']) || !in_array($package['type'], array('php-ext', 'php-ext-zend'), true)) { continue; } + if (isset($options['os-families']) && !in_array($platform['os'], $options['os-families'], true)) { continue; } + if (isset($options['os-families-exclude']) && in_array($platform['os'], $options['os-families-exclude'], true)) { continue; } + $threadMode = $platform['zts'] ? 'support-zts' : 'support-nts'; + if (isset($options[$threadMode]) && !$options[$threadMode]) { continue; } + foreach (array('require', 'conflict') as $rule) { + foreach (isset($package[$rule]) ? $package[$rule] : array() as $name => $range) { + if (!in_array($name, array('php', 'php-64bit', 'php-zts', 'php-debug'), true)) { continue; } + $present = $name === 'php' || ($name === 'php-64bit' && $platform['bits'] === 64) || ($name === 'php-zts' && $platform['zts']) || ($name === 'php-debug' && $platform['debug']); + $matches = $present && self::satisfies($platform['version'], $range); + if (($rule === 'require' && !$matches) || ($rule === 'conflict' && $matches)) { continue 3; } + } + } + return $package; + } + throw new RuntimeException('No compatible release for ' . $requested); + } + + private static function architecture($arch) + { + $arch = strtolower($arch); + if (in_array($arch, array('amd64', 'x64', 'x86_64'), true)) { return 'x86_64'; } + if (in_array($arch, array('arm64', 'aarch64'), true)) { return 'arm64'; } + if (in_array($arch, array('x86', 'i386', 'i686'), true)) { return 'x86'; } + return $arch; + } + + // Decode the platform declared by an asset name; do not guess download URLs. + public static function decodeAsset($name, $extension) + { + $prefix = 'php_' . $extension . '-'; + if (stripos($name, $prefix) !== 0) { return null; } + $name = substr($name, strlen($prefix)); + $arch = '(x86_64|amd64|x64|arm64|aarch64|x86)'; + if (preg_match('/^(.+)_php(\d+\.\d+)-' . $arch . '-(linux|darwin|windows)-(glibc|musl|anylibc|bsdlibc)(-debug)?(?:-(zts|nts))?\.(zip|tgz|tar\.gz|so)$/i', $name, $match)) { + return array('version' => self::version($match[1]), 'php' => $match[2], 'arch' => self::architecture($match[3]), 'os' => strtolower($match[4]), 'libc' => strtolower($match[5]), 'debug' => !empty($match[6]), 'zts' => isset($match[7]) && strtolower($match[7]) === 'zts', 'format' => strtolower($match[8])); + } + foreach (array('(nts|ts)-(vs\d+|vc\d+)', '(vs\d+|vc\d+)-(nts|ts)') as $order => $build) { + if (preg_match('/^(.+)-(\d+\.\d+)-' . $build . '-' . $arch . '\.(zip|dll)$/i', $name, $match)) { + return array('version' => self::version($match[1]), 'php' => $match[2], 'arch' => self::architecture($match[5]), 'os' => 'windows', 'compiler' => strtolower($match[$order === 0 ? 4 : 3]), 'debug' => false, 'zts' => strtolower($match[$order === 0 ? 3 : 4]) === 'ts', 'format' => strtolower($match[6])); + } + } + return null; + } + + public static function selectAsset($assets, $extension, $version, $platform) + { + $matches = array(); + foreach ($assets as $asset) { + try { $abi = self::decodeAsset($asset['name'], $extension); } catch (RuntimeException $error) { continue; } + if (!$abi || version_compare($abi['version'], self::version($version), '!=') || $abi['php'] !== $platform['php'] || $abi['os'] !== $platform['os'] || $abi['arch'] !== $platform['arch'] || $abi['zts'] !== $platform['zts'] || $abi['debug'] !== $platform['debug']) { continue; } + if ($platform['os'] === 'windows') { + if (!isset($abi['compiler']) || $abi['compiler'] !== $platform['compiler']) { continue; } + } elseif ($abi['libc'] !== $platform['libc'] && !($platform['os'] === 'linux' && $abi['libc'] === 'anylibc')) { continue; } + $rank = (isset($abi['libc']) && $abi['libc'] === 'anylibc' ? 10 : 0) + array_search($abi['format'], array('zip', 'tgz', 'tar.gz', 'so', 'dll'), true); + $matches[$rank][] = $asset; + } + if (!$matches) { return null; } + ksort($matches); + $best = reset($matches); + if (count($best) !== 1) { throw new RuntimeException('Multiple release assets match the PHP platform'); } + return $best[0]; + } + + private static function request($url, $optional = false) + { + $headers = array('User-Agent: setup-php', 'Accept: application/json'); + $token = getenv('GITHUB_TOKEN'); + if ($token && parse_url($url, PHP_URL_HOST) === 'api.github.com') { $headers[] = 'Authorization: Bearer ' . $token; } + $curl = curl_init($url); + curl_setopt_array($curl, array(CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_ENCODING => '', CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30)); + $response = curl_exec($curl); + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + if ($optional && $status === 404) { return null; } + if ($response === false || $status !== 200) { throw new RuntimeException('Could not fetch release metadata (HTTP ' . $status . ')'); } + $data = json_decode($response, true); + if (!is_array($data)) { throw new RuntimeException('Invalid release metadata'); } + return $data; + } + + public static function resolve($extension, $requested, $libc, $compiler) + { + $extension = strtolower($extension); + $mapping = json_decode(file_get_contents(__DIR__ . '/../../configs/pie-extensions.json'), true); + if (strpos($requested, '/') === false) { + if (!isset($mapping[$extension])) { throw new RuntimeException('No package mapping for ' . $extension); } + $requested = $mapping[$extension] . ':' . ($requested ?: 'latest'); + } + $parts = explode(':', $requested, 2); + $parts[0] = strtolower($parts[0]); + if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}i', $parts[0])) { throw new RuntimeException('Invalid extension package'); } + $os = strtolower(PHP_OS_FAMILY); + $platform = array('version' => PHP_VERSION, 'php' => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, 'bits' => PHP_INT_SIZE * 8, 'os' => $os, 'arch' => PHP_INT_SIZE === 4 ? 'x86' : self::architecture(php_uname('m')), 'zts' => (bool) PHP_ZTS, 'debug' => (bool) PHP_DEBUG, 'libc' => $libc, 'compiler' => strtolower($compiler)); + $package = self::selectPackage(self::expand(self::request('https://repo.packagist.org/p2/' . $parts[0] . '.json'), $parts[0]), isset($parts[1]) ? $parts[1] : 'latest', $platform); + $extension = isset($package['php-ext']['extension-name']) ? $package['php-ext']['extension-name'] : $extension; + if (!preg_match('/^[a-z0-9_]+$/i', $extension)) { throw new RuntimeException('Invalid extension name'); } + $source = isset($package['source']) ? $package['source'] : array(); + $plan = array('kind' => 'source', 'extension' => $extension, 'version' => $package['version'], 'prefix' => $package['type'] === 'php-ext-zend' ? 'zend_extension' : 'extension', 'source' => $source, 'buildPath' => isset($package['php-ext']['build-path']) ? $package['php-ext']['build-path'] : '', 'reason' => 'No compatible binary in the selected release'); + $dist = isset($package['dist']['url']) ? $package['dist']['url'] : ''; + if (!preg_match('{^https://api\.github\.com/repos/([a-z0-9_.-]+/[a-z0-9_.-]+)/}i', $dist, $repository) + && !preg_match('{^https://github\.com/([a-z0-9_.-]+/[a-z0-9_.-]+?)(?:\.git)?$}i', isset($source['url']) ? $source['url'] : '', $repository)) { return $plan; } + $release = self::request('https://api.github.com/repos/' . $repository[1] . '/releases/tags/' . rawurlencode($package['version']), true); + if ($release !== null && (!isset($release['assets']) || !is_array($release['assets']))) { throw new RuntimeException('Invalid release asset metadata'); } + $asset = $release ? self::selectAsset($release['assets'], $extension, $package['version'], $platform) : null; + if (!$asset) { return $plan; } + unset($plan['reason']); + return array_merge($plan, array('kind' => 'binary', 'url' => $asset['browser_download_url'], 'name' => $asset['name'], 'digest' => isset($asset['digest']) ? $asset['digest'] : '')); + } +} + +if (realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) { + try { + echo json_encode(ExtensionRelease::resolve($argv[1], $argv[2], $argv[3], isset($argv[4]) ? $argv[4] : '')); + } catch (Exception $error) { + fwrite(STDERR, $error->getMessage() . PHP_EOL); + exit(1); + } +}