From db99084663ac30306e78b58cf2878e8e80f85833 Mon Sep 17 00:00:00 2001 From: Jerome Thayananthajothy Date: Sat, 27 Jun 2026 17:43:48 +0530 Subject: [PATCH] fix: resolve DNF5 module stream naming and invalid recovery instructions (closes #20) - Add dnf_resolve_php_stream() to detect real stream name from dnf module list php (handles both Remi remi-X.Y and RHEL AppStream X.Y naming) - Add dnf_stream_enabled() to skip destructive reset/enable when the correct stream is already active - Add dnf_major_version() to emit correct config-manager syntax for DNF4 vs DNF5 (--set-enabled removed in DNF5/Fedora 41+) - Fix install_php_dnf() and use_php_version() to use resolved stream name instead of hardcoded php:X.Y (which does not exist on Remi/Fedora) - Fix pkg_search_php() to detect un-enabled remi-X.Y streams as available - Fix suggest_repository_setup() Fedora instructions: replace non-existent remi-php82 repo with dnf module enable php:remi-X.Y, use version-aware config-manager syntax - Add tests/05_dnf.bats with 8 unit tests using PATH-based dnf stubs for Remi (DNF5), enabled-stream, and AppStream (DNF4) fixtures - Bump version to 1.12.2 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 +++ phpvm.sh | 113 +++++++++++++++++++++++++++------- tests/05_dnf.bats | 154 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 21 deletions(-) create mode 100644 tests/05_dnf.bats diff --git a/CHANGELOG.md b/CHANGELOG.md index b76532b..bb73dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +## [v1.12.2](https://github.com/Thavarshan/phpvm/compare/v1.12.1...v1.12.2) - 2026-06-27 + +### Fixed + +- **DNF5 / Fedora 41+ module stream naming (issue #20):** `phpvm install` and `phpvm use` now resolve the actual php module stream name from `dnf module list php` instead of hardcoding `php:X.Y`. Remi streams (`remi-8.2`, `remi-8.3`, …) and RHEL AppStream streams (`8.2`) are both detected automatically. +- **Destructive unconditional `dnf module reset` (issue #20):** The reset is now skipped when the correct stream is already enabled, preventing phpvm from clobbering a stream the user configured manually. +- **Invalid DNF5 recovery instructions (issue #20):** `suggest_repository_setup` now emits `dnf config-manager setopt .enabled=1` on DNF5 (Fedora 41+) instead of the removed `--set-enabled` flag. Fedora instructions no longer reference the non-existent `remi-php82` repo; instead they direct users to enable the module stream (`dnf module enable php:remi-X.Y -y`). +- **False "not found" for un-enabled Remi streams (issue #20):** `pkg_search_php` now uses stream resolution to detect `remi-X.Y` streams even before they are enabled, so phpvm correctly reports a version as installable rather than emitting the misleading "other PHP versions are available" message. + ## [v1.12.1](https://github.com/Thavarshan/phpvm/compare/v1.12.0...v1.12.1) - 2026-05-24 ### Added diff --git a/phpvm.sh b/phpvm.sh index d93e467..0fce344 100755 --- a/phpvm.sh +++ b/phpvm.sh @@ -11,7 +11,7 @@ # shellcheck disable=SC2155 # Allow declare and assign on same line for better readability -PHPVM_VERSION="1.12.1" +PHPVM_VERSION="1.12.2" # Test mode flag PHPVM_TEST_MODE="${PHPVM_TEST_MODE:-false}" @@ -931,11 +931,11 @@ pkg_search_php() { fi ;; dnf) - # Check for module stream availability (preferred for versioned PHP) - if dnf module list php 2> /dev/null | command grep -Eq "php[[:space:]]+${version}[[:space:]]|${version}[[:space:]]+\["; then + # Use stream resolution to handle both Remi (remi-X.Y) and AppStream (X.Y) naming + if [ -n "$(dnf_resolve_php_stream "$version")" ]; then return 0 fi - # If any php module exists, signal "some versions exist" + # If any php module exists, signal "some versions exist but not this one" if dnf module list php 2> /dev/null | command grep -q "^php"; then return 2 fi @@ -1204,6 +1204,39 @@ check_remi_repository() { fi } +# Return the DNF major version (4 or 5), or 0 if dnf not found +dnf_major_version() { + command dnf --version 2> /dev/null | command awk 'NR==1{split($1,a,"."); print a[1]+0}' +} + +# Resolve the actual dnf module stream name for a PHP X.Y version. +# Remi uses "remi-X.Y", RHEL AppStream uses "X.Y". +# Prints the stream name (e.g. "remi-8.2") or nothing if not found. +dnf_resolve_php_stream() { + local version="$1" + command dnf module list php 2> /dev/null | command awk -v ver="$version" ' + $1 == "php" { + stream = $2 + # Exact match (AppStream: "8.2") or suffix match (Remi: "remi-8.2") + if (stream == ver || substr(stream, length(stream) - length(ver)) == "-" ver) { + # Prefer remi- streams when available; collect all and pick last-or-remi + if (substr(stream, 1, 5) == "remi-") { remi = stream } + else { plain = stream } + } + } + END { if (remi != "") print remi; else if (plain != "") print plain } + ' +} + +# Return 0 if the given php module stream is currently enabled, 1 otherwise. +dnf_stream_enabled() { + local stream="$1" + command dnf module list php 2> /dev/null | command awk -v s="$stream" ' + $1 == "php" && $2 == s { if (index($3, "[e]") || index($4, "[e]") || index($0, "[e]")) found=1 } + END { exit (found ? 0 : 1) } + ' +} + # Detect if PHP packages are available in current repositories detect_php_availability() { local version="$1" @@ -1218,9 +1251,23 @@ suggest_repository_setup() { local version="$1" local major_minor local major_version + local dnf_ver + + # Emit the correct "dnf config-manager" syntax for the installed dnf version. + # DNF5 (Fedora 41+) removed --set-enabled; use "setopt .enabled=1" instead. + _dnf_enable_repo_cmd() { + local repo="$1" + dnf_ver=$(dnf_major_version) + if [ "${dnf_ver:-0}" -ge 5 ]; then + printf ' sudo dnf config-manager setopt %s.enabled=1\n' "$repo" + else + printf ' sudo dnf config-manager --set-enabled %s\n' "$repo" + fi + } if [ "$PKG_MANAGER" = "dnf" ] || [ "$PKG_MANAGER" = "yum" ]; then if [ "$PHPVM_LINUX_DISTRO" = "fedora" ]; then + major_minor=$(echo "$version" | cut -d. -f1,2) phpvm_echo "" phpvm_echo "PHP packages not found in default Fedora repositories." phpvm_echo "To install PHP $version, you need to enable Remi's repository:" @@ -1232,16 +1279,16 @@ suggest_repository_setup() { phpvm_echo " sudo dnf install https://rpms.remirepo.net/fedora/remi-release-42.rpm" fi phpvm_echo "" - phpvm_echo " # Enable the repository" - phpvm_echo " sudo dnf config-manager --set-enabled remi" + phpvm_echo " # Enable the Remi repository" + _dnf_enable_repo_cmd "remi" phpvm_echo "" - phpvm_echo " # Enable specific PHP version repository" - major_minor=$(echo "$version" | cut -d. -f1,2 | tr -d '.') - phpvm_echo " sudo dnf config-manager --set-enabled remi-php$major_minor" + phpvm_echo " # Enable the PHP module stream for version $major_minor" + phpvm_echo " sudo dnf module enable php:remi-${major_minor} -y" phpvm_echo "" phpvm_echo "After setting up the repositories, try: phpvm install $version" elif [ "$PHPVM_LINUX_DISTRO" = "rhel" ] || [ "$PHPVM_LINUX_DISTRO" = "rocky" ] || [ "$PHPVM_LINUX_DISTRO" = "almalinux" ] || [ "$PHPVM_LINUX_DISTRO" = "centos" ]; then + major_minor=$(echo "$version" | cut -d. -f1,2 | tr -d '.') phpvm_echo "" phpvm_echo "PHP packages not found in default RHEL/CentOS repositories." phpvm_echo "To install PHP $version, you need to enable EPEL and Remi repositories:" @@ -1258,9 +1305,8 @@ suggest_repository_setup() { fi phpvm_echo "" phpvm_echo " # Enable the repositories" - phpvm_echo " sudo dnf config-manager --set-enabled remi" - major_minor=$(echo "$version" | cut -d. -f1,2 | tr -d '.') - phpvm_echo " sudo dnf config-manager --set-enabled remi-php$major_minor" + _dnf_enable_repo_cmd "remi" + _dnf_enable_repo_cmd "remi-php${major_minor}" phpvm_echo "" phpvm_echo "After setting up the repositories, try: phpvm install $version" fi @@ -1612,16 +1658,28 @@ install_php_dnf() { # Try to enable the module stream first if command_exists dnf && dnf module list php > /dev/null 2>&1; then - phpvm_echo "Enabling PHP:${normalized_version} module stream..." - if ! run_with_sudo dnf module reset php -y 2> /dev/null; then - phpvm_warn "Failed to reset PHP module" - fi - if ! run_with_sudo dnf module enable php:"${normalized_version}" -y; then - phpvm_warn "Failed to enable PHP:${normalized_version} module stream" + local php_stream + php_stream=$(dnf_resolve_php_stream "$normalized_version") + if [ -z "$php_stream" ]; then + phpvm_warn "No php module stream found for PHP ${normalized_version}" phpvm_warn "You may need Remi repository for this PHP version" suggest_repository_setup "$version" return "$PHPVM_EXIT_ERROR" fi + phpvm_echo "Enabling php:${php_stream} module stream..." + if dnf_stream_enabled "$php_stream"; then + phpvm_debug "Module stream php:${php_stream} is already enabled — skipping reset/enable" + else + if ! run_with_sudo dnf module reset php -y 2> /dev/null; then + phpvm_warn "Failed to reset PHP module" + fi + if ! run_with_sudo dnf module enable "php:${php_stream}" -y; then + phpvm_warn "Failed to enable php:${php_stream} module stream" + phpvm_warn "You may need Remi repository for this PHP version" + suggest_repository_setup "$version" + return "$PHPVM_EXIT_ERROR" + fi + fi fi # Install the base PHP package @@ -2018,9 +2076,22 @@ switch_to_version_php() { elif [ "$PKG_MANAGER" = "dnf" ]; then # Try dnf module approach for RHEL/Fedora if command_exists dnf && dnf module list php > /dev/null 2>&1; then - phpvm_debug "Attempting to enable PHP $normalized_version module" - if ! run_with_sudo dnf module enable php:"$normalized_version" -y; then - phpvm_warn "Failed to enable PHP $normalized_version module." + local use_stream + use_stream=$(dnf_resolve_php_stream "$normalized_version") + if [ -n "$use_stream" ]; then + phpvm_debug "Attempting to enable php:${use_stream} module" + if dnf_stream_enabled "$use_stream"; then + phpvm_debug "Module stream php:${use_stream} already enabled" + else + if ! run_with_sudo dnf module reset php -y 2> /dev/null; then + phpvm_warn "Failed to reset PHP module" + fi + if ! run_with_sudo dnf module enable "php:${use_stream}" -y; then + phpvm_warn "Failed to enable php:${use_stream} module." + fi + fi + else + phpvm_warn "No php module stream found for PHP $normalized_version" fi fi # After module enable, check if binary exists diff --git a/tests/05_dnf.bats b/tests/05_dnf.bats new file mode 100644 index 0000000..eadd7a3 --- /dev/null +++ b/tests/05_dnf.bats @@ -0,0 +1,154 @@ +#!/usr/bin/env bats +# BATS test suite for phpvm - DNF module stream helpers (issue #20) +# These tests use a PATH-based dnf stub to simulate "dnf module list php" output +# without requiring a real Fedora/RHEL system. + +load test_helper + +# ── helpers ────────────────────────────────────────────────────────────────── + +# Write a stub script that emulates "dnf module list php" for Remi streams. +# The stub is placed first on PATH so phpvm.sh's dnf calls hit it. +setup_dnf_stub_remi() { + local stub_dir="$TEST_DIR/stub-remi" + mkdir -p "$stub_dir" + cat > "$stub_dir/dnf" <<'STUB' +#!/bin/bash +# Minimal dnf stub — only handles the subcommands phpvm uses +case "$*" in + "--version") + echo "5.2.0" + ;; + "module list php"|"module list php 2>/dev/null") + cat <<'TABLE' +php remi-7.4 common [d], devel, minimal PHP scripting language +php remi-8.0 common [d], devel, minimal PHP scripting language +php remi-8.1 common [d], devel, minimal PHP scripting language +php remi-8.2 common [d], devel, minimal PHP scripting language +php remi-8.3 common [d], devel, minimal PHP scripting language +php remi-8.4 common [d], devel, minimal PHP scripting language +php remi-8.5 common [d], devel, minimal PHP scripting language +TABLE + ;; + *) + exit 1 + ;; +esac +STUB + chmod +x "$stub_dir/dnf" + export PATH="$stub_dir:$PATH" +} + +# Remi stub with remi-8.2 already enabled ([e] in profile column) +setup_dnf_stub_remi_enabled() { + local stub_dir="$TEST_DIR/stub-remi-enabled" + mkdir -p "$stub_dir" + cat > "$stub_dir/dnf" <<'STUB' +#!/bin/bash +case "$*" in + "--version") + echo "5.2.0" + ;; + "module list php"|"module list php 2>/dev/null") + cat <<'TABLE' +php remi-8.1 common [d], devel, minimal PHP scripting language +php remi-8.2 [e] common [d], devel, minimal PHP scripting language +php remi-8.3 common [d], devel, minimal PHP scripting language +TABLE + ;; + *) + exit 1 + ;; +esac +STUB + chmod +x "$stub_dir/dnf" + export PATH="$stub_dir:$PATH" +} + +# AppStream stub (plain X.Y stream names, DNF4) +setup_dnf_stub_appstream() { + local stub_dir="$TEST_DIR/stub-appstream" + mkdir -p "$stub_dir" + cat > "$stub_dir/dnf" <<'STUB' +#!/bin/bash +case "$*" in + "--version") + echo "4.14.0" + ;; + "module list php"|"module list php 2>/dev/null") + cat <<'TABLE' +php 8.0 common [d], devel, minimal PHP scripting language +php 8.1 common [d], devel, minimal PHP scripting language +php 8.2 common [d], devel, minimal PHP scripting language +TABLE + ;; + *) + exit 1 + ;; +esac +STUB + chmod +x "$stub_dir/dnf" + export PATH="$stub_dir:$PATH" +} + +# ── dnf_resolve_php_stream ──────────────────────────────────────────────────── + +@test "dnf_resolve_php_stream returns remi-8.2 for version 8.2 on Remi Fedora" { + setup_dnf_stub_remi + source "$BATS_TEST_DIRNAME/../phpvm.sh" + result=$(dnf_resolve_php_stream "8.2") + [ "$result" = "remi-8.2" ] +} + +@test "dnf_resolve_php_stream returns remi-8.1 for version 8.1 on Remi Fedora" { + setup_dnf_stub_remi + source "$BATS_TEST_DIRNAME/../phpvm.sh" + result=$(dnf_resolve_php_stream "8.1") + [ "$result" = "remi-8.1" ] +} + +@test "dnf_resolve_php_stream returns empty string for unknown version on Remi" { + setup_dnf_stub_remi + source "$BATS_TEST_DIRNAME/../phpvm.sh" + result=$(dnf_resolve_php_stream "9.9") + [ -z "$result" ] +} + +@test "dnf_resolve_php_stream returns 8.2 for version 8.2 on AppStream (DNF4)" { + setup_dnf_stub_appstream + source "$BATS_TEST_DIRNAME/../phpvm.sh" + result=$(dnf_resolve_php_stream "8.2") + [ "$result" = "8.2" ] +} + +# ── dnf_stream_enabled ──────────────────────────────────────────────────────── + +@test "dnf_stream_enabled returns true when remi-8.2 is enabled" { + setup_dnf_stub_remi_enabled + source "$BATS_TEST_DIRNAME/../phpvm.sh" + run dnf_stream_enabled "remi-8.2" + [ "$status" -eq 0 ] +} + +@test "dnf_stream_enabled returns false for non-enabled stream remi-8.1" { + setup_dnf_stub_remi_enabled + source "$BATS_TEST_DIRNAME/../phpvm.sh" + run dnf_stream_enabled "remi-8.1" + [ "$status" -ne 0 ] +} + +# ── dnf_major_version ───────────────────────────────────────────────────────── + +@test "dnf_major_version returns 5 for DNF5 stub" { + setup_dnf_stub_remi + source "$BATS_TEST_DIRNAME/../phpvm.sh" + result=$(dnf_major_version) + [ "$result" = "5" ] +} + +@test "dnf_major_version returns 4 for DNF4 stub" { + setup_dnf_stub_appstream + source "$BATS_TEST_DIRNAME/../phpvm.sh" + result=$(dnf_major_version) + [ "$result" = "4" ] +}