Last active
August 19, 2021 13:56
-
-
Save boesing/40a74f77f93146f075166c197788c9b2 to your computer and use it in GitHub Desktop.
PHP binary by composer.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
SOURCE="${BASH_SOURCE[0]}" | |
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink | |
REALPATH_PHP_BINARY="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |
SOURCE="$(readlink "$SOURCE")" | |
[[ $SOURCE != /* ]] && SOURCE="$REALPATH_PHP_BINARY/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located | |
done | |
WORKING_DIRECTORY=$(pwd) | |
REALPATH_PHP_BINARY="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |
SEMVER_BINARY="$REALPATH_PHP_BINARY/tools/semver" | |
BREW_PHP_BINARY="/usr/local/bin/php" | |
CONFIGURATION_BY_VERSION_PATH="/usr/local/etc/php"; | |
CACHE_FILE_PATH_PREFIX="$TMPDIR/phpbinary"; | |
declare -a INSTALLED_MINOR_VERSIONS=() | |
for INSTALLED_MINOR_VERSION in $(find "$CONFIGURATION_BY_VERSION_PATH" -type d -maxdepth 1 -mindepth 1 -exec basename {} \; | sort); do | |
INSTALLED_MINOR_VERSIONS+=($INSTALLED_MINOR_VERSION); | |
done | |
if ! [ -x "$SEMVER_BINARY" ]; then | |
echo "Missing $SEMVER_BINARY or it is not executable!"; | |
exit 1; | |
fi | |
SYSTEM_PHP_BINARY=$BREW_PHP_BINARY; | |
if ! [ -f "composer.json" ] && [ -z "$FORCE_PHP_VERSION" ]; then | |
echo $SYSTEM_PHP_BINARY; | |
exit 0; | |
fi | |
JQ_BINARY=$(which jq); | |
if ! [ -x "$JQ_BINARY" ]; then | |
echo "Missing jq binary. This is needed for some features of this script"; | |
echo "Run: brew install jq" | |
exit 1; | |
fi | |
function php_binary_by_constraint { | |
local REQUIRED_CONSTRAINT="$1" | |
local REQUIRED_CONSTRAINT_PATH="/usr/local/opt/php@$REQUIRED_CONSTRAINT/bin/php" | |
if [ -x "$REQUIRED_CONSTRAINT_PATH" ]; then | |
echo $REQUIRED_CONSTRAINT_PATH; | |
return; | |
fi | |
for INSTALLED_MINOR in ${INSTALLED_MINOR_VERSIONS[@]}; do | |
local INSTALLED_MINOR_PATH="/usr/local/opt/php@$INSTALLED_MINOR/bin/php" | |
local INSTALLED_MINOR_VERSION="$($INSTALLED_MINOR_PATH -nr 'echo PHP_VERSION;' 2>/dev/null | cut -d '-' -f1)" | |
local SEMVER_RESULT=$($SEMVER_BINARY -r "$REQUIRED_CONSTRAINT" $INSTALLED_MINOR_VERSION) | |
if [ -z "$SEMVER_RESULT" ]; then | |
continue; | |
fi | |
echo $INSTALLED_MINOR_PATH; | |
break; | |
done | |
} | |
function php_binary_from_cache() | |
{ | |
local CACHE_IDENTIFIER=$1; | |
local CACHE_FILE="${CACHE_FILE_PATH_PREFIX}-${CACHE_IDENTIFIER}" | |
# If the cache file does not exist, early return | |
test -r $CACHE_FILE || return; | |
local CACHED_PHP_BINARY=$(cat $CACHE_FILE); | |
# Only return cached PHP binary if it is executable | |
test -x $CACHED_PHP_BINARY && echo $CACHED_PHP_BINARY; | |
} | |
# Use system PHP version for projects without PHP requirements | |
PHP_BINARY=$SYSTEM_PHP_BINARY | |
CACHE_IDENTIFIER="" | |
if [ ! -z "$FORCE_PHP_VERSION" ]; then | |
REQUIRED_PHP_VERSION="$FORCE_PHP_VERSION"; | |
elif [ -f "composer.json" ]; then | |
CACHE_IDENTIFIER="$(sha1sum composer.json | awk '{ print $1 }')" | |
CACHED_PHP_BINARY=$(php_binary_from_cache "$CACHE_IDENTIFIER") | |
if [ ! -z "$CACHED_PHP_BINARY" ]; then | |
echo "$CACHED_PHP_BINARY" | |
exit 0 | |
fi | |
REQUIRED_PHP_VERSION=$(jq -re ".config.platform.php // .require.php" composer.json 2>/dev/null); | |
fi | |
# Detect PHP Version based on composer.json requirements | |
if ! [ -z "$REQUIRED_PHP_VERSION" ]; then | |
PHP_BINARY=$(php_binary_by_constraint $REQUIRED_PHP_VERSION) | |
if [ -z "$PHP_BINARY" ]; then | |
echo "Could not detect a suitable PHP binary for required PHP version(s) $REQUIRED_PHP_VERSION." | |
echo "Please check your environment and probably install at least the required version to continue."; | |
exit 1; | |
fi | |
fi | |
if [ ! -z "$CACHE_IDENTIFIER" ]; then | |
echo $PHP_BINARY > "${CACHE_FILE_PATH_PREFIX}-${CACHE_IDENTIFIER}" | |
fi | |
echo $PHP_BINARY |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
### | |
# Initial source can be found here: https://github.com/qzb/sh-semver/blob/master/semver.sh | |
# Composer also supports single pipes between constraints like "^7|^8" and thus, I've added some normalization below | |
### | |
_num_part='([0-9]|[1-9][0-9]*)' | |
_lab_part='([0-9]|[1-9][0-9]*|[0-9]*[a-zA-Z-][a-zA-Z0-9-]*)' | |
_met_part='([0-9A-Za-z-]+)' | |
RE_NUM="$_num_part(\.$_num_part)*" | |
RE_LAB="$_lab_part(\.$_lab_part)*" | |
RE_MET="$_met_part(\.$_met_part)*" | |
RE_VER="[ \t]*$RE_NUM(-$RE_LAB)?(\+$RE_MET)?" | |
BRE_DIGIT='[0-9]\{1,\}' | |
BRE_ALNUM='[0-9a-zA-Z-]\{1,\}' | |
BRE_IDENT="$BRE_ALNUM\(\.$BRE_ALNUM\)*" | |
BRE_MAJOR="$BRE_DIGIT" | |
BRE_MINOR="\(\.$BRE_DIGIT\)\{0,1\}" | |
BRE_PATCH="\(\.$BRE_DIGIT\)\{0,1\}" | |
BRE_PRERE="\(-$BRE_IDENT\)\{0,1\}" | |
BRE_BUILD="\(+$BRE_IDENT\)\{0,1\}" | |
BRE_VERSION="${BRE_MAJOR}${BRE_MINOR}${BRE_PATCH}${BRE_PRERE}${BRE_BUILD}" | |
filter() | |
{ | |
local text="$1" | |
local regex="$2" | |
shift 2 | |
echo "$text" | grep -E "$@" "$regex" | |
} | |
# Gets number part from normalized version | |
get_number() | |
{ | |
echo "${1%%-*}" | |
} | |
# Gets prerelase part from normalized version | |
get_prerelease() | |
{ | |
local pre_and_meta=${1%+*} | |
local pre=${pre_and_meta#*-} | |
if [ "$pre" = "$1" ]; then | |
echo | |
else | |
echo "$pre" | |
fi | |
} | |
# Gets major number from normalized version | |
get_major() | |
{ | |
echo "${1%%.*}" | |
} | |
# Gets minor number from normalized version | |
get_minor() | |
{ | |
local minor_major_bug=${1%%-*} | |
local minor_major=${minor_major_bug%.*} | |
local minor=${minor_major#*.} | |
if [ "$minor" = "$minor_major" ]; then | |
echo | |
else | |
echo "$minor" | |
fi | |
} | |
get_bugfix() | |
{ | |
local minor_major_bug=${1%%-*} | |
local bugfix=${minor_major_bug##*.*.} | |
if [ "$bugfix" = "$minor_major_bug" ]; then | |
echo | |
else | |
echo "$bugfix" | |
fi | |
} | |
strip_metadata() | |
{ | |
echo "${1%+*}" | |
} | |
semver_eq() | |
{ | |
local ver1 ver2 part1 part2 | |
ver1=$(get_number "$1") | |
ver2=$(get_number "$2") | |
local count=1 | |
while true; do | |
part1=$(echo "$ver1"'.' | cut -d '.' -f $count) | |
part2=$(echo "$ver2"'.' | cut -d '.' -f $count) | |
if [ -z "$part1" ] || [ -z "$part2" ]; then | |
break | |
fi | |
if [ "$part1" != "$part2" ]; then | |
return 1 | |
fi | |
local count=$(( count + 1 )) | |
done | |
if [ "$(get_prerelease "$1")" = "$(get_prerelease "$2")" ]; then | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
semver_lt() | |
{ | |
local number_a number_b prerelease_a prerelease_b | |
number_a=$(get_number "$1") | |
number_b=$(get_number "$2") | |
prerelease_a=$(get_prerelease "$1") | |
prerelease_b=$(get_prerelease "$2") | |
local head_a='' | |
local head_b='' | |
local rest_a=$number_a. | |
local rest_b=$number_b. | |
while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do | |
head_a=${rest_a%%.*} | |
head_b=${rest_b%%.*} | |
rest_a=${rest_a#*.} | |
rest_b=${rest_b#*.} | |
if [ -z "$head_a" ] || [ -z "$head_b" ]; then | |
return 1 | |
fi | |
if [ "$head_a" -eq "$head_b" ]; then | |
continue | |
fi | |
if [ "$head_a" -lt "$head_b" ]; then | |
return 0 | |
else | |
return 1 | |
fi | |
done | |
if [ -n "$prerelease_a" ] && [ -z "$prerelease_b" ]; then | |
return 0 | |
elif [ -z "$prerelease_a" ] && [ -n "$prerelease_b" ]; then | |
return 1 | |
fi | |
local head_a='' | |
local head_b='' | |
local rest_a=$prerelease_a. | |
local rest_b=$prerelease_b. | |
while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do | |
head_a=${rest_a%%.*} | |
head_b=${rest_b%%.*} | |
rest_a=${rest_a#*.} | |
rest_b=${rest_b#*.} | |
if [ -z "$head_a" ] && [ -n "$head_b" ]; then | |
return 0 | |
elif [ -n "$head_a" ] && [ -z "$head_b" ]; then | |
return 1 | |
fi | |
if [ "$head_a" = "$head_b" ]; then | |
continue | |
fi | |
# If both are numbers then compare numerically | |
if [ "$head_a" = "${head_a%[!0-9]*}" ] && [ "$head_b" = "${head_b%[!0-9]*}" ]; then | |
[ "$head_a" -lt "$head_b" ] && return 0 || return 1 | |
# If only a is a number then return true (number has lower precedence than strings) | |
elif [ "$head_a" = "${head_a%[!0-9]*}" ]; then | |
return 0 | |
# If only b is a number then return false | |
elif [ "$head_b" = "${head_b%[!0-9]*}" ]; then | |
return 1 | |
# Finally if of identifiers is a number compare them lexically | |
else | |
test "$head_a" \< "$head_b" && return 0 || return 1 | |
fi | |
done | |
return 1 | |
} | |
semver_gt() | |
{ | |
if semver_lt "$1" "$2" || semver_eq "$1" "$2"; then | |
return 1 | |
else | |
return 0 | |
fi | |
} | |
semver_le() | |
{ | |
semver_gt "$1" "$2" && return 1 || return 0 | |
} | |
semver_ge() | |
{ | |
semver_lt "$1" "$2" && return 1 || return 0 | |
} | |
semver_sort() | |
{ | |
if [ $# -le 1 ]; then | |
echo "$1" | |
return | |
fi | |
local pivot=$1 | |
local args_a=() | |
local args_b=() | |
shift 1 | |
for ver in "$@"; do | |
if semver_le "$ver" "$pivot"; then | |
args_a=( "${args_a[@]}" "$ver" ) | |
else | |
args_b=( "$ver" "${args_b[@]}" ) | |
fi | |
done | |
args_a=( $(semver_sort "${args_a[@]}") ) | |
args_b=( $(semver_sort "${args_b[@]}") ) | |
echo "${args_a[@]}" "$pivot" "${args_b[@]}" | |
} | |
regex_match() | |
{ | |
local string="$1 " | |
local regexp="$2" | |
local match | |
match="$(eval "echo '$string' | grep -E -o '^[ \t]*($regexp)[ \t]+'")"; | |
for i in $(seq 0 9); do | |
unset "MATCHED_VER_$i" | |
unset "MATCHED_NUM_$i" | |
done | |
unset REST | |
if [ -z "$match" ]; then | |
return 1 | |
fi | |
local match_len=${#match} | |
REST="${string:$match_len}" | |
local part | |
local i=1 | |
for part in $string; do | |
local ver num | |
ver="$(eval "echo '$part' | grep -E -o '$RE_VER' | head -n 1 | sed 's/ \t//g'")"; | |
num=$(get_number "$ver") | |
if [ -n "$ver" ]; then | |
eval "MATCHED_VER_$i='$ver'" | |
eval "MATCHED_NUM_$i='$num'" | |
i=$(( i + 1 )) | |
fi | |
done | |
return 0 | |
} | |
# Normalizes rules string | |
# | |
# * replaces chains of whitespaces with single spaces | |
# * replaces whitespaces around hyphen operator with "_" | |
# * removes wildcards from version numbers (1.2.* -> 1.2) | |
# * replaces "x" with "*" | |
# * removes whitespace between operators and version numbers | |
# * removes leading "v" from version numbers | |
# * removes leading and trailing spaces | |
normalize_rules() | |
{ | |
echo " $1" \ | |
| sed 's/\\t/ /g' \ | |
| sed 's/ / /g' \ | |
| sed 's/ \{2,\}/ /g' \ | |
| sed 's/ - /_-_/g' \ | |
| sed 's/\([~^<>=]\) /\1/g' \ | |
| sed 's/\([ _~^<>=]\)v/\1/g' \ | |
| sed 's/\.[xX*]//g' \ | |
| sed 's/[xX]/*/g' \ | |
| sed 's/^ //g' \ | |
| sed 's/ $//g' | |
} | |
# Reads rule from provided string | |
resolve_rule() | |
{ | |
local rule operator operands | |
rule="$1" | |
operator="$( echo "$rule" | sed "s/$BRE_VERSION/#/g" )" | |
operands=( $( echo "$rule" | grep -o "$BRE_VERSION") ) | |
case "$operator" in | |
'*') echo "all" ;; | |
'#') echo "eq ${operands[0]}" ;; | |
'=#') echo "eq ${operands[0]}" ;; | |
'<#') echo "lt ${operands[0]}" ;; | |
'>#') echo "gt ${operands[0]}" ;; | |
'<=#') echo "le ${operands[0]}" ;; | |
'>=#') echo "ge ${operands[0]}" ;; | |
'#_-_#') echo "ge ${operands[0]}" | |
echo "le ${operands[1]}" ;; | |
'~#') echo "tilde ${operands[0]}" ;; | |
'^#') echo "caret ${operands[0]}" ;; | |
*) return 1 | |
esac | |
} | |
resolve_rules() | |
{ | |
local rules | |
rules="$(normalize_rules "$1")" | |
IFS=' ' read -ra rules <<< "${rules:-all}" | |
for rule in "${rules[@]}"; do | |
resolve_rule "$rule" | |
done | |
} | |
rule_eq() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
semver_eq "$tested_ver" "$rule_ver" && return 0 || return 1; | |
} | |
rule_le() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
semver_le "$tested_ver" "$rule_ver" && return 0 || return 1; | |
} | |
rule_lt() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
semver_lt "$tested_ver" "$rule_ver" && return 0 || return 1; | |
} | |
rule_ge() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
semver_ge "$tested_ver" "$rule_ver" && return 0 || return 1; | |
} | |
rule_gt() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
semver_gt "$tested_ver" "$rule_ver" && return 0 || return 1; | |
} | |
rule_tilde() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
if rule_ge "$rule_ver" "$tested_ver"; then | |
local rule_major rule_minor | |
rule_major=$(get_major "$rule_ver") | |
rule_minor=$(get_minor "$rule_ver") | |
if [ -n "$rule_minor" ] && rule_eq "$rule_major.$rule_minor" "$(get_number "$tested_ver")"; then | |
return 0 | |
fi | |
if [ -z "$rule_minor" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then | |
return 0 | |
fi | |
fi | |
return 1 | |
} | |
rule_caret() | |
{ | |
local rule_ver="$1" | |
local tested_ver="$2" | |
if rule_ge "$rule_ver" "$tested_ver"; then | |
local rule_major | |
rule_major="$(get_major "$rule_ver")" | |
if [ "$rule_major" != "0" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then | |
return 0 | |
fi | |
if [ "$rule_major" = "0" ] && rule_eq "$rule_ver" "$(get_number "$tested_ver")"; then | |
return 0 | |
fi | |
fi | |
return 1 | |
} | |
rule_all() | |
{ | |
return 0 | |
} | |
apply_rules() | |
{ | |
local rules_string="$1" | |
shift | |
local versions=( "$@" ) | |
# Loop over sets of rules (sets of rules are separated with ||) | |
for ver in "${versions[@]}"; do | |
rules_tail="$rules_string"; | |
while [ -n "$rules_tail" ]; do | |
head="${rules_tail%%||*}" | |
if [ "$head" = "$rules_tail" ]; then | |
rules_string="" | |
else | |
rules_tail="${rules_tail#*||}" | |
fi | |
#if [ -z "$head" ] || [ -n "$(echo "$head" | grep -E -x '[ \t]*')" ]; then | |
#group=$(( $group + 1 )) | |
#continue | |
#fi | |
rules="$(resolve_rules "$head")" | |
# If specified rule cannot be recognised - end with error | |
if [ $? -eq 1 ]; then | |
exit 1 | |
fi | |
if ! echo "$ver" | grep -q -E -x "[v=]?[ \t]*$RE_VER"; then | |
continue | |
fi | |
ver=$(echo "$ver" | grep -E -x "$RE_VER") | |
success=true | |
allow_prerel=false | |
if $FORCE_ALLOW_PREREL; then | |
allow_prerel=true | |
fi | |
while read -r rule; do | |
comparator="${rule%% *}" | |
operand="${rule#* }" | |
if [ -n "$(get_prerelease "$operand")" ] && semver_eq "$(get_number "$operand")" "$(get_number "$ver")" || [ "$rule" = "all" ]; then | |
allow_prerel=true | |
fi | |
"rule_$comparator" "$operand" "$ver" | |
if [ $? -eq 1 ]; then | |
success=false | |
break | |
fi | |
done <<< "$rules" | |
if $success; then | |
if [ -z "$(get_prerelease "$ver")" ] || $allow_prerel; then | |
echo "$ver" | |
break; | |
fi | |
fi | |
done | |
group=$(( group + 1 )) | |
done | |
} | |
FORCE_ALLOW_PREREL=false | |
USAGE="Usage: $0 [-r <rule>] [<version>... ] | |
Omitting <version>s reads them from STDIN. | |
Omitting -r <rule> simply sorts the versions according to semver ordering." | |
while getopts ar:h o; do | |
case "$o" in | |
a) FORCE_ALLOW_PREREL=true ;; | |
r) RULES_STRING="$OPTARG||";; | |
h) echo "$USAGE" && exit ;; | |
?) echo "$USAGE" && exit 1;; | |
esac | |
done | |
shift $(( OPTIND-1 )) | |
VERSIONS=( ${@:-$(cat -)} ) | |
# Sort versions | |
VERSIONS=( $(semver_sort "${VERSIONS[@]}") ) | |
if [ -z "$RULES_STRING" ]; then | |
printf '%s\n' "${VERSIONS[@]}" | |
else | |
# Ensure that constraints are separated by double pipes | |
NORMALIZED_RULES_STRING="$(echo "$RULES_STRING" | tr '|' '\n' | egrep -v '^$' | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/||/g')" | |
if ! [ -z "$NORMALIZED_RULES_STRING" ]; then | |
RULES_STRING="${NORMALIZED_RULES_STRING}||" | |
fi | |
apply_rules "$RULES_STRING" "${VERSIONS[@]}" | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This supposed to work OOTB when using on MacOS along with shivammathur PHP over Homebrew.
Other distros need some PATH changes.