diff --git a/bin/zopen-install b/bin/zopen-install index 7443ebfbd..622e24727 100755 --- a/bin/zopen-install +++ b/bin/zopen-install @@ -184,18 +184,69 @@ installDependencies() handlePackageInstall() { - fullname="$1" isRuntimeDependency=$2 if [ -z "$isRuntimeDependency" ]; then isRuntimeDependency=false fi - printVerbose "Name to install: ${fullname}, parsing any version ('=') or tag ('%') has been specified" - name=$(echo "${fullname}" | sed -e 's#[=%].*##') + + operator="" + versioned="" + tagged="" + + # Support: name=ver, name>=ver, name>ver, name<=ver, name<%].*//') repo="${name}" - versioned=$(echo "${fullname}" | cut -s -d '=' -f 2) + + # Get everything after the name + suffix="${fullname#$name}" + + # Extract operator (can be >=, <=, >, <, =) from the beginning of suffix + operator=$(echo "${suffix}" | sed -E -n 's/^([>=<]{1,2}).*/\1/p') + + if [ -n "${operator}" ]; then + # Validate operator + case "${operator}" in + "="|">"|">="|"<"|"<=") ;; + *) printError "Invalid operator '${operator}' in package specification. Supported operators are: =, >, >=, <, <=" ;; + esac + + # Extract version: take everything after operator and before % + rest="${suffix#$operator}" + versioned="${rest%%%*}" + + if [ -z "${versioned}" ]; then + printError "A version must be provided when using an operator (e.g., ${name}${operator}1.0)." + fi + else + # Fallback to older '=' parsing if no explicit operator found but versioned exists + # (Checking if suffix starts with '=' and taking everything before '%') + case "${suffix}" in + =*) + operator="=" + rest="${suffix#=}" + versioned="${rest%%%*}" + ;; + esac + fi + + # Validate that the version string contains valid characters (alphanumeric, dots, hyphens) + if [ -n "${versioned}" ]; then + # Strip leading 'v' if present for validation + v_to_check="${versioned#v}" + # Must contain at least one digit and only valid chars + if [ -z "${v_to_check}" ] || ! echo "${v_to_check}" | grep -q "[0-9]" || echo "${v_to_check}" | grep -q "[^0-9a-zA-Z.-]" || echo "${v_to_check}" | grep -q "^\." || echo "${v_to_check}" | grep -q "\.$" || echo "${v_to_check}" | grep -q "\.\."; then + printError "Invalid version string '${versioned}'. Versions must be numeric segments separated by dots (e.g., 1.2.3) and can include alphanumeric suffixes (e.g., 1.2.3-rc1)." + fi + fi + + # Extract tag tagged=$(echo "${fullname}" | cut -s -d '%' -f 2) - printDebug "Name:${name};version:${versioned};tag:${tagged};repo:${repo}" + + if [ -n "${versioned}" ] && [ -n "${tagged}" ]; then + printError "Ambiguous package specification '${fullname}'. Provide either a version constraint or a tag, but not both." + fi + printDebug "Name:${name};operator:${operator};version:${versioned};tag:${tagged};repo:${repo}" printInfo "${NC}${HEADERCOLOR}${BOLD}Processing package: ${name}${NC}" nameSansPort=$(echo "${name}" | sed -e 's#\(.*\)port$#\1#') @@ -242,14 +293,35 @@ handlePackageInstall() # Options where the user explicitly sets a version/tag/releaseline currently ignore any configured release-line, # either for a previous package install or system default if [ -n "${versioned}" ]; then - printVerbose "Specific version ${versioned} requested - checking existence and URL." + printVerbose "Specific version ${versioned} requested with operator '${operator}' - checking existence and URL." requestedMajor=$(echo "${versioned}" | awk -F'.' '{print $1}') - requestedMinor=$(echo "${versioned}" | awk -F'.' '{print $2}') - requestedPatch=$(echo "${versioned}" | awk -F'.' '{print $3}') - requestedSubrelease=$(echo "${versioned}" | awk -F'.' '{print $4}') - requestedVersion="${requestedMajor}\\\.${requestedMinor}\\\.${requestedPatch}\\\.${requestedSubrelease}" - printVerbose "Finding URL for latest release matching version prefix: requestedVersion: ${requestedVersion}" - releaseMetadata=$(/bin/printf "%s" "${releases}" | jq -e -r '. | map(select(.assets[].name | test("'${requestedVersion}'")))[0]') + if [ -z "${requestedMajor}" ]; then + printError "A major version must be provided when specifying a version (e.g., ${name}=1)." + fi + + # Convert requested version string to a JSON array of numbers for jq + req_v_json=$(echo "${versioned#v}" | tr '.' '\n' | jq -R 'tonumber' | jq -s -c .) + + printVerbose "Finding latest release matching ${operator} ${versioned}" + # Unified selection logic using jq: + # 1. to_v: Converts a dot-delimited version string to a numeric array, padded with 0s to length 8 + # 2. match_v: Handles prefix match for '=' and numeric comparison for others + releaseMetadata=$(/bin/printf "%s" "${releases}" | jq -e -r --arg op "${operator}" --argjson req_v "${req_v_json}" ' + def to_v: if . == null then [0] else split(".") | map(tonumber? // 0) end | . + [range(0; 8 - length) | 0]; + def match_v(rv; op): + to_v as $av | + (rv + [range(0; 8 - (rv | length)) | 0]) as $nrv | + if op == "=" then $av[0:(rv | length)] == rv + elif op == ">=" then $av >= $nrv + elif op == ">" then $av > $nrv + elif op == "<=" then $av <= $nrv + elif op == "<" then $av < $nrv + else false end; + map(select(.assets[0].version != null and (.assets[0].version | match_v($req_v; $op)))) | + sort_by([(.assets[0].version | to_v), .date, .tag_name]) | reverse | .[0]') + if [ -z "${releaseMetadata}" ] || [ "${releaseMetadata}" = "null" ]; then + printError "Could not find a release of '${name}' matching '${operator}${versioned}'" + fi elif [ -n "${tagged}" ]; then printVerbose "Explicit tagged version '${tagged}' specified. Checking for match." releaseMetadata=$(/bin/printf "%s" "${releases}" | jq -e -r '.[] | select(.tag_name == "'${tagged}'")') @@ -861,18 +933,26 @@ if ${all}; then done installArray=$(strtrim "${installArray}") else + chosenRepos=$(echo "${chosenRepos}" | tr ',' ' ' | tr -s ' ') chosenRepos=$(strtrim "${chosenRepos}") invalidlist="" - for chosenRepo in $(echo "${chosenRepos}" | tr ',' ' ' | tr -s ' '); do + for chosenRepo in ${chosenRepos}; do printVerbose "Processing repo: ${chosenRepo}" - printVerbose "Stripping any version (%), tag (#) or port suffixes" - toolrepo=$(echo "${chosenRepo}" | sed -e 's#%.*##' -e 's#=.*##') + printVerbose "Stripping any version operator (>, >=, <, <=, =), tag (%) or port suffixes" + toolrepo=$(echo "${chosenRepo}" | sed -E 's/[=><%].*//') toolfound=$(echo "${repo_results}" | awk -vtoolrepo="${toolrepo}" '$0 == toolrepo {print}') if [ "${toolfound}" = "${toolrepo}" ]; then printVerbose "Adding '${chosenRepo}' to the install queue." installArray="${installArray} ${chosenRepo}" printVerbose "Removing valid port from input list." - chosenRepos=$(echo "${chosenRepos}" | sed -e "s#^${chosenRepo}\$##") + # Safely remove the word from the space-separated list + newChosenRepos="" + for r in ${chosenRepos}; do + if [ "$r" != "${chosenRepo}" ]; then + newChosenRepos="${newChosenRepos} $r" + fi + done + chosenRepos=$(strtrim "${newChosenRepos}") else invalidlist=$(/bin/printf "%s %s" "${invalidlist}" "${chosenRepo}") fi