From fd92d52ba984413eed6438d80f329b5e02e4a10a Mon Sep 17 00:00:00 2001 From: aooohan Date: Sat, 12 Sep 2026 14:56:09 +0800 Subject: [PATCH 1/4] fix: restore PHP release discovery and platform installation Query PHP's JSON source index directly and combine current/archive Windows binaries with semantic ordering, architecture filtering, rebuild handling and explicit fetch errors. Preserve TS/NTS selection without introducing a scheduled manifest service. Apply the absolute Windows extension_dir fix from PR #23. Select Homebrew OpenSSL 3 for PHP 8.1+ and the older dependency for legacy versions, with clear missing-dependency errors and preservation of explicit configure options. Add hook and shell regressions and update installation CI entry points. Validated upstream discovery with vfox Lua modules on macOS and simulated Windows architectures; full SDK installation remains a CI check. Fixes #12 Fixes #17 Fixes #19 Fixes #8 Co-authored-by: JamBalaya56562 --- .github/workflows/hooks-test.yml | 27 +++++++ .github/workflows/test-linux.yaml | 4 +- .github/workflows/test-macos.yaml | 6 +- .github/workflows/test-windows.yaml | 6 +- README.md | 15 +++- bin/install | 45 +++++++++-- hooks/available.lua | 81 +------------------ hooks/post_install.lua | 18 ++++- hooks/pre_install.lua | 81 ++++--------------- lib/releases.lua | 116 ++++++++++++++++++++++++++++ tests/hooks_test.lua | 103 ++++++++++++++++++++++++ tests/openssl_test.sh | 36 +++++++++ tests/windows_test.lua | 33 ++++++++ 13 files changed, 410 insertions(+), 161 deletions(-) create mode 100644 .github/workflows/hooks-test.yml create mode 100644 lib/releases.lua create mode 100644 tests/hooks_test.lua create mode 100644 tests/openssl_test.sh create mode 100644 tests/windows_test.lua diff --git a/.github/workflows/hooks-test.yml b/.github/workflows/hooks-test.yml new file mode 100644 index 0000000..a6a63f1 --- /dev/null +++ b/.github/workflows/hooks-test.yml @@ -0,0 +1,27 @@ +name: Test Plugin + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version: '1.24.0' + cache: false + - name: Test plugin hooks with vfox's Lua interpreter + run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/hooks_test.lua + - name: Test Windows configuration generation + run: go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/windows_test.lua + - name: Test macOS OpenSSL selection + run: bash tests/openssl_test.sh diff --git a/.github/workflows/test-linux.yaml b/.github/workflows/test-linux.yaml index a109539..e562d41 100644 --- a/.github/workflows/test-linux.yaml +++ b/.github/workflows/test-linux.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: install vfox (Linux) run: | echo "deb [trusted=yes] https://apt.fury.io/versionfox/ /" | sudo tee /etc/apt/sources.list.d/versionfox.list @@ -33,7 +33,7 @@ jobs: run: | vfox add -s php.zip vfox install php@latest - vfox use -p php@$(vfox list php | sed -n 's/-> v//p') + vfox use -p php@latest eval "$(vfox activate bash)" php -v php -m diff --git a/.github/workflows/test-macos.yaml b/.github/workflows/test-macos.yaml index c1e9b3a..3d37ca3 100644 --- a/.github/workflows/test-macos.yaml +++ b/.github/workflows/test-macos.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: install vfox (MacOS) run: | brew tap version-fox/tap @@ -26,14 +26,14 @@ jobs: zip -r php.zip ./ - name: Install packages - run: brew install autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libxslt libzip pkg-config re2c zlib + run: brew install autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libxslt libzip openssl@3 pkg-config re2c zlib - name: Test shell: bash run: | vfox add -s php.zip vfox install php@latest - vfox use -p php@$(vfox list php | sed -n 's/-> v//p') + vfox use -p php@latest eval "$(vfox activate bash)" php -v php -m diff --git a/.github/workflows/test-windows.yaml b/.github/workflows/test-windows.yaml index bf2661e..937d5b6 100644 --- a/.github/workflows/test-windows.yaml +++ b/.github/workflows/test-windows.yaml @@ -15,11 +15,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: install vfox and test (Windows) shell: pwsh run: | + $PSNativeCommandUseErrorActionPreference = $true Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression scoop install vfox @@ -27,8 +28,9 @@ jobs: vfox -v vfox add -s php.zip vfox install php@latest - vfox use -p php@$(vfox list php | sed -n 's/-> v//p') + vfox use -p php@latest Invoke-Expression "$(vfox activate pwsh)" php -v php -m + if (-not ((php -m) -contains "openssl")) { throw "OpenSSL extension was not loaded" } composer -V diff --git a/README.md b/README.md index 28bd6fe..bea0988 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ vfox install php@8.4.5 vfox install php@8.4.5-nts ``` +The version list combines current and archived Windows binaries, sorted newest +first. Plain Windows versions select thread-safe (TS) builds; `-nts` selects NTS. +Linux and macOS source versions come directly from PHP's official JSON API. +Network failures report the upstream URL instead of returning an empty list. + ## Prerequirements PHP installation requires some dependencies. Please install the dependencies based on the error messages, or refer to [.github/workflows/test-\*.yaml](https://github.com/version-fox/vfox-php/tree/main/.github/workflows) for guidance. @@ -26,9 +31,17 @@ PHP installation requires some dependencies. Please install the dependencies bas To install PHP on macOS, you'll need a set of packages installed via homebrew. ```shell -brew install autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib +brew install autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip openssl@3 pkg-config re2c zlib ``` +PHP 8.1 and newer use `openssl@3` on macOS. Older PHP versions require a +compatible older OpenSSL installation; the build reports a missing dependency +instead of silently selecting OpenSSL 3 or omitting HTTPS support. Homebrew no +longer supplies `openssl@1.1` through its normal supported formulae. See the +[PHP OpenSSL compatibility requirements](https://www.php.net/manual/en/openssl.requirements.php). +If you supply `PHP_CONFIGURE_OPTIONS`, you remain responsible for configuring +the dependency paths; automatic macOS dependency selection is bypassed. + There's also a set of optional packages which enable additional extensions to be enabled: ```shell diff --git a/bin/install b/bin/install index be9a46d..84b400b 100755 --- a/bin/install +++ b/bin/install @@ -7,7 +7,8 @@ install_php() { local install_path=$1 local source_path="${install_path}_source" - local configure_options="$(construct_configure_options $install_path)" + local configure_options + configure_options="$(construct_configure_options "$install_path")" || return 1 local operating_system=$(uname -a) @@ -20,7 +21,11 @@ install_php() { local libedit_path=$(homebrew_package_path libedit) local libxml2_path=$(homebrew_package_path libxml2) local libxslt_path=$(homebrew_package_path libxslt) - local openssl_path=$(homebrew_package_path openssl@1.1) + local openssl_path + # Explicit configure options own their dependency paths, as before. + if [ -z "${PHP_CONFIGURE_OPTIONS:-}" ]; then + openssl_path=$(homebrew_openssl_path) || return 1 + fi if [ -n "$bison_path" ]; then export "PATH=${bison_path}/bin:${PATH}" @@ -133,7 +138,9 @@ construct_configure_options() { --without-snmp" if [ "$PHP_CONFIGURE_OPTIONS" = "" ]; then - local configure_options="$(os_based_configure_options) $global_config" + local platform_options + platform_options=$(os_based_configure_options) || return 1 + local configure_options="$platform_options $global_config" else local configure_options="$PHP_CONFIGURE_OPTIONS $global_config" fi @@ -163,6 +170,28 @@ homebrew_package_path() { fi } +homebrew_openssl_path() { + # PHP 7.1-8.0 requires OpenSSL < 3.0; PHP 8.1+ supports OpenSSL 3. + # See https://www.php.net/manual/en/openssl.requirements.php + local major minor formula path + if [[ ! "${PHP_VERSION:-}" =~ ^([0-9]+)\.([0-9]+)\. ]]; then + echo "ERROR: Cannot determine PHP version for OpenSSL selection" >&2 + return 1 + fi + major=${BASH_REMATCH[1]} + minor=${BASH_REMATCH[2]} + formula=openssl@1.1 + if (( major > 8 || (major == 8 && minor >= 1) )); then + formula=openssl@3 + fi + path=$(homebrew_package_path "$formula") + if [ -z "$path" ]; then + echo "ERROR: PHP $PHP_VERSION requires $formula; install a compatible dependency before building." >&2 + return 1 + fi + echo "$path" +} + exit_if_homebrew_not_installed() { if [ "$(brew --version 2>/dev/null)" = "" ]; then echo "ERROR: Please install homebrew for OSX" @@ -190,7 +219,8 @@ os_based_configure_options() { local libxml2_path=$(homebrew_package_path libxml2) local libxslt_path=$(homebrew_package_path libxslt) local libzip_path=$(homebrew_package_path libzip) - local openssl_path=$(homebrew_package_path openssl@1.1) + local openssl_path + openssl_path=$(homebrew_openssl_path) || return 1 local readline_path=$(homebrew_package_path readline) local webp_path=$(homebrew_package_path webp) local zlib_path=$(homebrew_package_path zlib) @@ -298,5 +328,8 @@ os_based_configure_options() { echo $configure_options } -install_php $1 -install_composer $1 +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + PHP_VERSION=${2:-$(sed -n 's/^#define PHP_VERSION "\([^"]*\)"/\1/p' "$1/main/php_version.h")} + install_php "$1" + install_composer "$1" +fi diff --git a/hooks/available.lua b/hooks/available.lua index 59e5027..f03aef2 100644 --- a/hooks/available.lua +++ b/hooks/available.lua @@ -1,82 +1,5 @@ -local http = require('http') -local html = require('html') -local util = require('util') -require('constants') +local releases = require("releases") ---- Return all available versions provided by this plugin ---- @param ctx table Empty table used as context, for future extension ---- @return table Descriptions of available versions and accompanying tool descriptions function PLUGIN:Available(ctx) - if RUNTIME.osType == 'windows' then - return GetReleaseListForWindows() - else - return GetReleaseListForLinux() - end -end - -function GetReleaseListForWindows() - local result = {} - local urls = { WIN_RELEASES_URL, WIN_RELEASES_URL_LTS } - - for _, url in ipairs(urls) do - local resp, err = http.get({ url = url }) - - if resp then - local doc = html.parse(resp.body) - local versions = {} - doc:find('a'):each(function(i, selection) - local versionStr = selection:text() - table.insert(versions, versionStr) - end) - -- TODO like this because for some reason sorting it at the end resets is_from_lts to false - table.sort(versions, function(a, b) - return util.compare_versions(a, b) > 0 - end) - for _, versionStr in ipairs(versions) do - if util.filter_windows_version(versionStr) then - local versions = util.split_string(versionStr, '-') - if util.compare_versions(versions[2], "5.3.2") >= 0 then - local entry = { - version = (versions[3] ~= "nts") and versions[2] or versions[2] .. "-nts", - name = versionStr - } - - entry.is_from_lts = (url == WIN_RELEASES_URL_LTS) - table.insert(result, entry) - end - end - end - end - end - - return result -end - -function GetReleaseListForLinux() - local result = {} - local urls = { RELEASES_URL, RELEASES_URL_LTS } - - for _, url in ipairs(urls) do - local resp, err = http.get({ url = url }) - local is_from_lts = (url == RELEASES_URL_LTS) - - if resp then - local doc = html.parse(resp.body) - local query = "#layout-content " .. (is_from_lts and "h3" or "h2") - doc:find(query):each(function(i, selection) - local versionStr = is_from_lts and selection:attr("id") or selection:text() - versionStr = versionStr:gsub("^v", "") - if util.compare_versions(versionStr, "5.3.2") >= 0 then - table.insert(result, { - version = versionStr, - }) - end - end) - end - end - - table.sort(result, function(a, b) - return util.compare_versions(a.version, b.version) > 0 - end) - return result + return releases.available() end diff --git a/hooks/post_install.lua b/hooks/post_install.lua index 9e11736..ee269c8 100644 --- a/hooks/post_install.lua +++ b/hooks/post_install.lua @@ -10,7 +10,7 @@ function PLUGIN:PostInstall(ctx) if RUNTIME.osType == 'windows' then InstallComposerForWin(path) else - CompileInstallPHP(path) + CompileInstallPHP(path, sdkInfo.version) end end @@ -19,7 +19,14 @@ function InstallComposerForWin(path) if err ~= nil then error(err) end - content = content:gsub(';%s*extension_dir = "ext"', 'extension_dir = "./ext"') + -- Use an absolute extension_dir so php.exe can locate php_openssl.dll + -- regardless of which CWD it was launched from. The composer-setup + -- step below invokes php.exe from the user's shell CWD, so a relative + -- "./ext" path resolves to the wrong directory and fails to load + -- openssl, breaking the HTTPS download Composer needs. + local ext_dir = (path .. "\\ext"):gsub("\\", "/") + content = content:gsub(';%s*extension_dir = "ext"', + function() return 'extension_dir = "' .. ext_dir .. '"' end) content = content:gsub(';extension=openssl', 'extension=openssl') content = content:gsub(';extension=php_openssl.dll', 'extension=php_openssl.dll') _, err = util.write_file(path .. '\\php.ini', content) @@ -68,9 +75,12 @@ function InstallComposerForWin(path) util.write_file(path .. '\\composer.bat', '@php "%~dp0composer.phar" %*') end -function CompileInstallPHP(path) +function CompileInstallPHP(path, version) + if not version:match("^%d+%.%d+%.%d+$") then + error("Invalid PHP source version: " .. tostring(version)) + end os.execute('chmod +x ' .. RUNTIME.pluginDirPath .. '/bin/install') - local code = os.execute(RUNTIME.pluginDirPath .. '/bin/install ' .. path) + local code = os.execute(RUNTIME.pluginDirPath .. '/bin/install ' .. path .. ' ' .. version) if code ~= 0 then error('Compilation Failure.') end diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index f9fa16e..dbfbd8b 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -1,74 +1,27 @@ -local http = require('http') -local json = require('json') -local util = require('util') -require('constants') +local util = require("util") ---- Returns some pre-installed information, such as version number, download address, local files, etc. ---- If checksum is provided, vfox will automatically check it for you. ---- @param ctx table ---- @field ctx.version string User-input version ---- @return table Version information function PLUGIN:PreInstall(ctx) local version = ctx.version - local lists = self:Available({}) - if version == 'latest' or version == '' then - version = lists[1].version - end - - local versions = {} - for _, value in pairs(lists) do - if util.starts_with(value.version, version .. '.') then - versions = value - end - if value.version == version then - versions = value - end - if next(versions) ~= nil then - break - end + local versions = self:Available({}) + if #versions == 0 then + error("No PHP releases available") end - if next(versions) == nil then - error('version not found for provided version ' .. version) + if version == "latest" or version == "" then + return versions[1] end - - if RUNTIME.osType == 'windows' then - return GetReleaseForWindows(versions) - else - return GetReleaseForLinux(versions) + if type(version) ~= "string" then + error("PHP version must be provided") end -end - -function GetReleaseForWindows(versions) - url = WIN_RELEASES_URL .. versions.name - - if (versions.is_from_lts) then - url = WIN_RELEASES_URL_LTS .. versions.name + -- Exact matches must win over prefixes, especially for Windows TS/NTS. + for _, release in ipairs(versions) do + if release.version == version then + return release + end end - return { - version = versions.version, - url = url, - } -end - -function GetReleaseForLinux(versions) - local resp, err = http.get({ - url = URL .. "/releases/index.php?json&version=" .. versions.version - }) - local data = json.decode(resp.body) - - local filename, md5, sha256 = "", "", "" - for _, s in pairs(data["source"]) do - if util.ends_with(s.filename, ".tar.gz") then - filename = s.filename - md5 = s.md5 - sha256 = s.sha256 - break + for _, release in ipairs(versions) do + if util.starts_with(release.version, version .. ".") then + return release end end - return { - version = versions.version, - url = URL .. "/distributions/" .. filename, - sha256 = sha256, - md5 = md5 - } + error("PHP version not found: " .. version) end diff --git a/lib/releases.lua b/lib/releases.lua new file mode 100644 index 0000000..2c26833 --- /dev/null +++ b/lib/releases.lua @@ -0,0 +1,116 @@ +local http = require("http") +local json = require("json") +local html = require("html") +local util = require("util") +local releases = {} + +local function fetch(url) + local response, err = http.get({ url = url }) + if err ~= nil or response == nil then + error("Failed to fetch PHP releases from " .. url .. ": " .. tostring(err or "empty response")) + end + if response.status_code ~= 200 then + error("Failed to fetch PHP releases from " .. url .. ": HTTP " .. tostring(response.status_code)) + end + return response.body +end + +local function sort(result) + table.sort(result, function(a, b) + local order = util.compare_versions(a.version:match("^[%d.]+"), b.version:match("^[%d.]+")) + if order ~= 0 then + return order > 0 + end + -- Plain versions are thread-safe on Windows; prefer them for latest. + return a.version < b.version + end) + if #result == 0 then + error("No PHP releases found for " .. RUNTIME.osType .. "/" .. RUNTIME.archType) + end + return result +end + +local function windows() + local arch = ({ amd64 = "x64", ["386"] = "x86", arm64 = "arm64" })[RUNTIME.archType] + if not arch then + error("Unsupported PHP architecture: " .. tostring(RUNTIME.archType)) + end + local result, seen = {}, {} + -- Current releases win when the same version also exists in the archive. + for _, base in ipairs({ + "https://windows.php.net/downloads/releases/", + "https://windows.php.net/downloads/releases/archives/", + }) do + html.parse(fetch(base)):find("a"):each(function(_, selection) + local filename = (selection:attr("href") or ""):match("([^/]+)$") or "" + local version, variant, fileArch = + filename:match("^php%-(%d+%.%d+%.%d+)(.-)%-Win32%-[Vv][CcSs]%d+%-(%w+)%.zip$") + if not version or fileArch ~= arch then + return + end + local rebuild = variant:match("^%-(%d+)$") or variant:match("^%-nts%-(%d+)$") + if variant ~= "" and variant ~= "-nts" and not rebuild then + return + end + local nts = variant:match("^%-nts") ~= nil + rebuild = tonumber(rebuild) or 0 + if util.compare_versions(version, "5.3.2") < 0 then + return + end + version = version .. (nts and "-nts" or "") + if not seen[version] then + local entry = { version = version, url = base .. filename, note = nts and "NTS" or "TS" } + seen[version] = { entry = entry, base = base, rebuild = rebuild } + table.insert(result, entry) + elseif seen[version].base == base and rebuild > seen[version].rebuild then + seen[version].entry.url = base .. filename + seen[version].rebuild = rebuild + end + end) + end + return sort(result) +end + +local function source() + local result, seen = {}, {} + for _, major in ipairs({ 8, 7, 5 }) do + local url = "https://www.php.net/releases/index.php?json&max=10000&version=" .. major + local decoded = json.decode(fetch(url)) + if type(decoded) ~= "table" then + error("Invalid PHP release index from " .. url) + end + for version, release in pairs(decoded) do + if + type(version) == "string" + and version:match("^%d+%.%d+%.%d+$") + and util.compare_versions(version, "5.3.2") >= 0 + and type(release) == "table" + and type(release.source) == "table" + and not seen[version] + then + for _, file in ipairs(release.source) do + if file.filename == "php-" .. version .. ".tar.gz" then + seen[version] = true + table.insert(result, { + version = version, + url = "https://www.php.net/distributions/" .. file.filename, + sha256 = file.sha256, + md5 = file.md5, + }) + break + end + end + end + end + end + return sort(result) +end + +function releases.available() + if RUNTIME.osType == "windows" then + return windows() + end + return source() +end + +return releases diff --git a/tests/hooks_test.lua b/tests/hooks_test.lua new file mode 100644 index 0000000..2f547e3 --- /dev/null +++ b/tests/hooks_test.lua @@ -0,0 +1,103 @@ +package.path = "./lib/?.lua;" .. package.path +PLUGIN = {} +RUNTIME = { osType = "windows", archType = "amd64" } +local responses, state = {}, {} +package.preload.http = function() + return { + get = function(args) + if state.failure then + return state.failure.response, state.failure.error + end + return { status_code = 200, body = assert(responses[args.url], "unexpected URL " .. args.url) } + end, + } +end +package.preload.json = function() + return { + decode = function(body) + return body + end, + } +end +package.preload.html = function() + return { + parse = function(body) + return { + find = function() + return { + each = function(_, fn) + for i, filename in ipairs(body) do + fn(i, { + attr = function() + return filename + end, + text = function() + return filename + end, + }) + end + end, + } + end, + } + end, + } +end +local current = "https://windows.php.net/downloads/releases/" +local archives = current .. "archives/" +responses[current] = { + "/downloads/releases/php-8.4.2-nts-Win32-vs17-x64.zip", + "/downloads/releases/php-8.4.2-Win32-vs17-x64.zip", + "/downloads/releases/php-8.4.2-Win32-vs17-x86.zip", + "/downloads/releases/php-8.4.2-debug-pack-Win32-vs17-x64.zip", +} +responses[archives] = { + "php-5.6.16-nts-Win32-VC11-x64.zip", + "php-8.4.2-Win32-vs17-x64.zip", + "php-8.3.15-Win32-vs16-x64.zip", + "php-8.4.1-nts-Win32-vs17-x64.zip", + "php-7.2.33-1-Win32-VC15-x64.zip", + "php-7.2.33-Win32-VC15-x64.zip", +} +for _, major in ipairs({ 5, 7, 8 }) do + responses["https://www.php.net/releases/index.php?json&max=10000&version=" .. major] = {} +end +responses["https://www.php.net/releases/index.php?json&max=10000&version=8"] = { + ["8.4.2"] = { source = { { filename = "php-8.4.2.tar.gz", sha256 = string.rep("a", 64) } } }, + ["8.3.15"] = { source = { { filename = "php-8.3.15.tar.gz", sha256 = string.rep("b", 64) } } }, + ["8.5.0RC1"] = { source = { { filename = "php-8.5.0RC1.tar.gz" } } }, +} +dofile("hooks/available.lua") +dofile("hooks/pre_install.lua") +local list = PLUGIN:Available({}) +assert(list[1].version == "8.4.2", "latest must be current TS, got " .. tostring(list[1].version)) +assert(#list == 6, #list) +assert(PLUGIN:PreInstall({ version = "7.2.33" }).url == archives .. "php-7.2.33-1-Win32-VC15-x64.zip") +assert(PLUGIN:PreInstall({ version = "latest" }).url == current .. "php-8.4.2-Win32-vs17-x64.zip") +assert(PLUGIN:PreInstall({ version = "8.4.2-nts" }).version == "8.4.2-nts") +assert(PLUGIN:PreInstall({ version = "8.3" }).url == archives .. "php-8.3.15-Win32-vs16-x64.zip") +RUNTIME.archType = "386" +assert(#PLUGIN:Available({}) == 1) +RUNTIME.osType = "linux" +list = PLUGIN:Available({}) +assert(#list == 2 and list[1].version == "8.4.2") +local install = PLUGIN:PreInstall({ version = "8.4" }) +assert(install.url == "https://www.php.net/distributions/php-8.4.2.tar.gz") +assert(install.sha256 == string.rep("a", 64)) +local function fails(fn, expected) + local ok, err = pcall(fn) + assert(not ok and tostring(err):find(expected, 1, true), tostring(err)) +end +state.failure = { error = "timeout" } +fails(function() + PLUGIN:Available({}) +end, "timeout") +state.failure = { response = { status_code = 503 } } +fails(function() + PLUGIN:PreInstall({ version = "latest" }) +end, "503") +state.failure = nil +fails(function() + PLUGIN:PreInstall({ version = "99.0.0" }) +end, "not found") +print("PHP release discovery, ordering, architecture and failure cases passed") diff --git a/tests/openssl_test.sh b/tests/openssl_test.sh new file mode 100644 index 0000000..f4186c0 --- /dev/null +++ b/tests/openssl_test.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail +source bin/install + +installed_three=yes +installed_one=yes +homebrew_package_path() { + if [[ "$1" == openssl@3 && "$installed_three" == yes ]] || [[ "$1" == openssl@1.1 && "$installed_one" == yes ]]; then + echo "/fake/$1" + fi +} +for version in 8.1.0 8.4.2 8.5.0; do + PHP_VERSION=$version + [[ "$(homebrew_openssl_path)" == /fake/openssl@3 ]] +done +for version in 7.4.33 8.0.30; do + PHP_VERSION=$version + [[ "$(homebrew_openssl_path)" == /fake/openssl@1.1 ]] +done +PHP_VERSION=7.4.33 +installed_one=no +if homebrew_openssl_path 2>/dev/null; then + echo "Old PHP must not silently use OpenSSL 3" >&2; exit 1 +fi +PHP_VERSION=8.4.2 +installed_three=no +if homebrew_openssl_path 2>/dev/null; then + echo "Missing OpenSSL must report a dependency error" >&2; exit 1 +fi +echo "OpenSSL compatibility and missing dependency cases passed" + +# Custom configure options must still work without a Homebrew OpenSSL formula. +PHP_CONFIGURE_OPTIONS=--with-openssl=/custom/openssl +options=$(construct_configure_options /tmp/php-test) +[[ "$options" == *--with-openssl=/custom/openssl* ]] +echo "Explicit configure options preserved" diff --git a/tests/windows_test.lua b/tests/windows_test.lua new file mode 100644 index 0000000..0aaf948 --- /dev/null +++ b/tests/windows_test.lua @@ -0,0 +1,33 @@ +package.path = "./lib/?.lua;" .. package.path +PLUGIN = {} +RUNTIME = { osType = "windows" } +local state = { files = {}, commands = {} } +local util = require("util") +util.read_file = function() + return ';extension_dir = "ext"\n;extension=openssl\n;extension=php_openssl.dll\n' +end +util.write_file = function(path, content) + state.files[path] = content + return true +end +package.preload.http = function() + return { + get = function() + return { status_code = 200, body = "composer installer" } + end, + } +end +os.execute = function(command) + state.commands[#state.commands + 1] = command + return 0 +end +os.remove = function() end +dofile("hooks/post_install.lua") +local path = "C:\\SDKs\\100% PHP" +PLUGIN:PostInstall({ sdkInfo = { php = { path = path, version = "8.4.2" } } }) +local ini = assert(state.files[path .. "\\php.ini"]) +assert(ini:find('extension_dir = "C:/SDKs/100% PHP/ext"', 1, true), ini) +assert(ini:find("\nextension=openssl\n", 1, true), ini) +assert(ini:find("\nextension=php_openssl.dll\n", 1, true), ini) +assert(#state.commands == 1) +print("Windows PHP extension path substitution passed") From ab653989734435295f615eaa6a592dacc07d7883 Mon Sep 17 00:00:00 2001 From: aooohan Date: Sat, 12 Sep 2026 14:58:41 +0800 Subject: [PATCH 2/4] ci: enable manual PHP installation checks Restrict push runs to main to avoid duplicate PR installations. Restore workflows disabled by inactivity so platform checks run for the fixes. --- .github/workflows/test-linux.yaml | 2 ++ .github/workflows/test-macos.yaml | 2 ++ .github/workflows/test-windows.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/test-linux.yaml b/.github/workflows/test-linux.yaml index e562d41..904ef76 100644 --- a/.github/workflows/test-linux.yaml +++ b/.github/workflows/test-linux.yaml @@ -3,6 +3,8 @@ name: Test linux workflow on: pull_request: push: + branches: [main] + workflow_dispatch: schedule: - cron: 0 0 * * 5 diff --git a/.github/workflows/test-macos.yaml b/.github/workflows/test-macos.yaml index 3d37ca3..02651fa 100644 --- a/.github/workflows/test-macos.yaml +++ b/.github/workflows/test-macos.yaml @@ -3,6 +3,8 @@ name: Test macos workflow on: pull_request: push: + branches: [main] + workflow_dispatch: schedule: - cron: 0 0 * * 5 diff --git a/.github/workflows/test-windows.yaml b/.github/workflows/test-windows.yaml index 937d5b6..60255b6 100644 --- a/.github/workflows/test-windows.yaml +++ b/.github/workflows/test-windows.yaml @@ -3,6 +3,8 @@ name: Test windows workflow on: pull_request: push: + branches: [main] + workflow_dispatch: schedule: - cron: 0 0 * * 5 From ba61471b6859bbddaf7c7f0148e9ab67ca6f1794 Mon Sep 17 00:00:00 2001 From: aooohan Date: Sat, 12 Sep 2026 15:06:46 +0800 Subject: [PATCH 3/4] fix(ci): initialize vfox before selecting PHP --- .github/workflows/test-linux.yaml | 3 ++- .github/workflows/test-macos.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-linux.yaml b/.github/workflows/test-linux.yaml index 904ef76..faf8f27 100644 --- a/.github/workflows/test-linux.yaml +++ b/.github/workflows/test-linux.yaml @@ -35,8 +35,9 @@ jobs: run: | vfox add -s php.zip vfox install php@latest - vfox use -p php@latest eval "$(vfox activate bash)" + vfox use -p php@latest + eval "$(vfox env --shell bash)" php -v php -m composer -V diff --git a/.github/workflows/test-macos.yaml b/.github/workflows/test-macos.yaml index 02651fa..ae5abc6 100644 --- a/.github/workflows/test-macos.yaml +++ b/.github/workflows/test-macos.yaml @@ -35,8 +35,9 @@ jobs: run: | vfox add -s php.zip vfox install php@latest - vfox use -p php@latest eval "$(vfox activate bash)" + vfox use -p php@latest + eval "$(vfox env --shell bash)" php -v php -m composer -V From 7e0471d2afe426f242fa893a0a9abd20896e2bca Mon Sep 17 00:00:00 2001 From: aooohan Date: Sat, 12 Sep 2026 15:21:48 +0800 Subject: [PATCH 4/4] fix(ci): remove empty legacy config and verify managed PHP --- .github/workflows/test-linux.yaml | 4 ++++ .github/workflows/test-macos.yaml | 4 ++++ .tool-versions | 0 3 files changed, 8 insertions(+) delete mode 100644 .tool-versions diff --git a/.github/workflows/test-linux.yaml b/.github/workflows/test-linux.yaml index faf8f27..d83dd92 100644 --- a/.github/workflows/test-linux.yaml +++ b/.github/workflows/test-linux.yaml @@ -32,12 +32,16 @@ jobs: zip -r php.zip ./ - name: Test + env: + MAKEFLAGS: -j2 run: | vfox add -s php.zip vfox install php@latest eval "$(vfox activate bash)" vfox use -p php@latest eval "$(vfox env --shell bash)" + php_path=$(command -v php) + [[ "$php_path" == "$(pwd -P)/.vfox/sdks/php/bin/php" ]] php -v php -m composer -V diff --git a/.github/workflows/test-macos.yaml b/.github/workflows/test-macos.yaml index ae5abc6..fbc8186 100644 --- a/.github/workflows/test-macos.yaml +++ b/.github/workflows/test-macos.yaml @@ -31,6 +31,8 @@ jobs: run: brew install autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libxslt libzip openssl@3 pkg-config re2c zlib - name: Test + env: + MAKEFLAGS: -j2 shell: bash run: | vfox add -s php.zip @@ -38,6 +40,8 @@ jobs: eval "$(vfox activate bash)" vfox use -p php@latest eval "$(vfox env --shell bash)" + php_path=$(command -v php) + [[ "$php_path" == "$(pwd -P)/.vfox/sdks/php/bin/php" ]] php -v php -m composer -V diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index e69de29..0000000