Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <repo>.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
Expand Down
113 changes: 92 additions & 21 deletions phpvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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 <repo>.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:"
Expand All @@ -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:"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
154 changes: 154 additions & 0 deletions tests/05_dnf.bats
Original file line number Diff line number Diff line change
@@ -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" ]
}
Loading