--- /usr/bin/euse 2009-06-15 16:15:32.000000000 -0500 +++ /usr/bin/euse 2009-08-08 23:39:09.000000000 -0500 @@ -13,6 +13,7 @@ MAKE_GLOBALS_PATH=/etc/make.globals MAKE_PROFILE_PATH=/etc/make.profile MAKE_CONF_BACKUP_PATH=/etc/make.conf.euse_backup +PACKAGE_USE_PATH=/etc/portage/package.use [ -z "${MODE}" ] && MODE="showhelp" # available operation modes: showhelp, showversion, showdesc, showflags, modify @@ -32,13 +33,14 @@ -E | --enable) MODE="modify"; ACTION="add";; -D | --disable) MODE="modify"; ACTION="remove";; -P | --prune) MODE="modify"; ACTION="prune";; + -p | --package) MODE="modify"; shift; PACKAGE=${1};; -*) echo "ERROR: unknown option ${1} specified." echo MODE="showhelp" ;; "%active") - get_useflags + get_portageuseflags ARGUMENTS="${ARGUMENTS} ${ACTIVE_FLAGS[9]}" ;; *) @@ -55,6 +57,10 @@ exit 1 } +warn() { + echo "WARNING: ${1}" +} + get_real_path() { set -P cd "$1" @@ -82,6 +88,7 @@ done # [ ! -r "$(get_make_defaults)" ] && error "$(get_make_defaults) is not readable" [ "${MODE}" == "modify" -a ! -w "${MAKE_CONF_PATH}" ] && error ""${MAKE_CONF_PATH}" is not writable" + [ "${MODE}" == "modify" -a -s "${PACKAGE_USE_PATH}" -a ! -w "${PACKAGE_USE_PATH}" ] && error ""${PACKAGE_USE_PATH}" is not writable" } showhelp() { @@ -101,13 +108,17 @@ -E, --enable - enable the given useflags -D, --disable - disable the given useflags -P, --prune - remove all references to the given flags from - make.conf to revert to default settings + make.conf and package.use to revert to default + settings + -p, --package - used with -E, -D, to apply to a specific + package only Notes: ${PROGRAM_NAME} currently only works for global flags defined - in make.globals, make.defaults or make.conf, it doesn't handle - use.defaults, use.mask or package.use yet (see portage(5) for details on - these files). It also might have issues with cascaded profiles. - If multiple options are specified only the last one will be used. + in make.globals, make.defaults or make.conf, and local flags defined + in package.use and individual package ebuilds, it doesn't handle + use.defaults or use.mask yet (see portage(5) for details on these + files). It also might have issues with cascaded profiles. If + multiple options are specified only the last one will be used. HELP } @@ -147,13 +158,44 @@ print ' '.join(r)" } +# Similar to reduce_incrementals except converts lines from package atoms +# in /etc/portage/package.use files to lines of "pkg {[-]flag}*" +reduce_package_use() { + echo "${@}" | python -c "import sys,re +h={}; atom=re.compile(r'[<>]?=?([a-z][\w/-]+[a-z])(-[\dpr._*-]+)?', re.I); +getflags=re.compile(r'(-?[\w*-]+)') +for x in sys.stdin.read().split('\n'): + if not x: continue + parts = x.lstrip().split(' ',1) + if len(parts)==1: continue + try: + pkg = atom.match(parts[0]).group(1) + except: + continue + flags = getflags.findall(parts[1]) + if not pkg in h: h[pkg]=[] + r=h[pkg] + for x in flags: + if x[0] == '-' and x[1:] in r: + r.remove(x[1:]) + r.append(x) + elif x[0] != '-' and '-'+x in r: + r.remove('-'+x) + r.append(x) + elif x == '-*': + r = h[pkg] = ['-*'] + elif x not in r: + r.append(x) +print '\n'.join([' %s %s ' % (pkg,' '.join(flgs)) for pkg,flgs in h.iteritems() if len(flgs)])" +} + # the following function creates a bash array ACTIVE_FLAGS that contains the # global use flags, indexed by origin: 0: environment, 1: make.conf, -# 2: make.defaults, 3: make.globals +# 2: make.defaults, 3: make.globals, 4: package.use, 5: ebuild IUSE get_useflags() { - # only calculate once as calling emerge is painfully slow + # only calculate once [ -n "${USE_FLAGS_CALCULATED}" ] && return - + # backup portdir so get_portdir() doesn't give false results later portdir_backup="${PORTDIR}" @@ -174,10 +216,38 @@ USE="${ACTIVE_FLAGS[0]}" PORTDIR="${portdir_backup}" + # Parse through /etc/portage/package.use + if [[ -d ${PACKAGE_USE_PATH} ]]; then + ACTIVE_FLAGS[4]="$( cat ${PACKAGE_USE_PATH}/* \ + | sed -re "s/#.*$//" )" + elif [[ -e ${PACKAGE_USE_PATH} ]]; then + ACTIVE_FLAGS[4]="$(sed -re "/#.*$/ d" < ${PACKAGE_USE_PATH})" + fi + # Simplify ACTIVE_FLAGS[4] to be lines of pkg {[-]flag}* + ACTIVE_FLAGS[4]="$(reduce_package_use "${ACTIVE_FLAGS[4]}")" + # + # ACTIVE_FLAGS[5] reserved for USE flags defined in ebuilds and + # is generated/maintained in the get_useflaglist_ebuild() function + # + # Traverse through use.mask and use.force (0.5s) + #ACTIVE_FLAGS[6]=$(reduce_incrementals \ + # $(cat $(traverse_profile "use.mask") \ + # | sed -re "/^#.*$/ {d}")) + #ACTIVE_FLAGS[7]=$(reduce_incrementals \ + # $(cat $(traverse_profile "use.force") \ + # | sed -re "/^#.*$/ {d}")) + + USE_FLAGS_CALCULATED=1 +} + +# Fetch USE flags reported active by Portage +get_portageuseflags() { + # only calculate once as calling emerge is painfully slow + [ -n "${_PORTAGE_USE_FLAGS_CALCULATED}" ] && return # get the currently active USE flags as seen by portage, this has to be after # restoring USE or portage won't see the original environment ACTIVE_FLAGS[9]="$(emerge --info | grep 'USE=' | cut -b 5- | sed -e 's:"::g')" #' - USE_FLAGS_CALCULATED=1 + _PORTAGE_USE_FLAGS_CALCULATED=1 } # get the list of all known USE flags by reading use.desc and/or use.local.desc @@ -194,26 +264,67 @@ egrep "^[^# :]+:[^ ]+ +-" "${descdir}/use.local.desc" | cut -d: -f 2 | cut -d\ -f 1 fi } +get_useflaglist_ebuild() { + # $1 package atom (app-editor/vim) + # $2 version (7.2) + # + # TODO: Scrap ${2} input. Get a folder listing of the ebuilds, + # derive version from each file name and create use flag list + # from each file's contents. Store by package name and + # version. + # + local known=$(echo "${ACTIVE_FLAGS[5]}" | egrep "^${1};${2}") + if [[ -n $known ]]; then + # Isolate the flags part + echo $known | cut -d\; -f4 | cut -d\" -f2 + return + fi + local pkg=$(echo ${1} | cut -d/ -f2) + # Open the ebuild file and retrieve defined USE flags + [[ ! -d "$(get_portdir)/${1}" ]] && return 1 + for file in $(get_portdir)/${1}/*.ebuild; do + # Crazy sed script matches IUSE variable if it spans + # multiple lines + local flags=$(sed -nr -e '/IUSE="/{;:loop;/"$/{;s/\n//g;p;q;};N;b loop;}' ${file}) + local slot=$(grep "^SLOT=" ${file}) + local version=$(basename ${file} | sed -re "s:${pkg}-(.+)\.ebuild:\1:") + ACTIVE_FLAGS[5]="${ACTIVE_FLAGS[5]}"$'\n'"${1};${version};${slot};${flags}" + done +} -# get all make.defaults by traversing the cascaded profile directories -get_all_make_defaults() { +# General method of collecting the contents of a profile +# component by traversing through the cascading profile +# +# Arguments: +# $1 - Filename (make.profile) +# [$2] - Current directory (unspecified means to start at the top) +traverse_profile() { local curdir local parent local rvalue - curdir="${1:-$(get_real_path ${MAKE_PROFILE_PATH})}" + curdir="${2:-$(get_real_path ${MAKE_PROFILE_PATH})}" - [ -f "${curdir}/make.defaults" ] && rvalue="${curdir}/make.defaults ${rvalue}" + [ -f "${curdir}/${1}" ] && rvalue="${curdir}/${1} ${rvalue}" if [ -f "${curdir}/parent" ]; then for parent in $(egrep -v '(^#|^ *$)' ${curdir}/parent); do pdir="$(get_real_path ${curdir}/${parent})" - rvalue="$(get_all_make_defaults ${pdir}) ${rvalue}" + rvalue="$(traverse_profile ${1} ${pdir}) ${rvalue}" done fi echo "${rvalue}" } +# get all make.defaults by traversing the cascaded profile directories +get_all_make_defaults() { + if [[ -n $MAKE_DEFAULTS ]]; then + echo $MAKE_DEFAULTS + return + fi + traverse_profile "make.defaults" +} + # get the path to make.defaults by traversing the cascaded profile directories get_make_defaults() { local curdir @@ -238,29 +349,129 @@ # 3: echo value for positive (and as lowercase for negative) test result, # 4 (optional): echo value for "missing" test result, defaults to blank get_flagstatus_helper() { - if echo " ${ACTIVE_FLAGS[${2}]} " | grep " ${1} " > /dev/null; then - echo -n "${3}" - elif echo " ${ACTIVE_FLAGS[${2}]} " | grep " -${1} " > /dev/null; then - echo -n "$(echo ${3} | tr [[:upper:]] [[:lower:]])" + if [[ -z ${flags} ]]; then + local flags=${ACTIVE_FLAGS[${2}]} + fi + local flag=$(echo " $flags " | grep -Eo " [+-]?${1} ") + if [[ ${flag:1:1} == "-" ]]; then + echo -e -n "${3}" | tr [:upper:]+ [:lower:]- + elif [[ -n ${flag} ]]; then + echo -e -n "${3}" else echo -n "${4:- }" fi } +get_flagstatus_helper_pkg() { + if [[ -z ${flags} ]]; then + local flags=$(echo " ${ACTIVE_FLAGS[${2}]} " | grep "${5} "); + fi + if [[ -z ${5} || -z ${flags} ]]; then + echo -e -n "${4:- }" + return + fi + get_flagstatus_helper "$@" +} +get_flagstatus_helper_ebuild() { + if [[ -z ${5} ]]; then + echo -n "${4:- }" + return + fi + #local info=$( get_useflaglist_ebuild "${5}" "${6}") + local flags=$(echo $2 | cut -d\" -f2) + local flag=$(echo " $flags " | grep -Eo " [+-]?$1 ") + if [[ ${flag:1:1} == "+" ]]; then + echo -en "${3}" + elif [[ ${flag:1:1} == "-" ]]; then + echo -en "${3}" | tr [:upper:]+ [:lower:]- + elif [[ -z $flag ]]; then + echo -en "!" + else + echo -en "${4:- }" + fi +} # prints a status string for the given flag, each column indicating the presence # for portage, in the environment, in make.conf, in make.defaults and in make.globals. -# full positive value would be "[+ECDG]", full negative value would be [-ecdg], +# full positive value would be "[+ECDG]", full negative value would be [-ecdgpb], # full missing value would be "[- ]" (portage only sees present or not present) get_flagstatus() { get_useflags - echo -n '[' - get_flagstatus_helper "${1}" 9 "+" "-" - get_flagstatus_helper "${1}" 0 "E" - get_flagstatus_helper "${1}" 1 "C" - get_flagstatus_helper "${1}" 2 "D" - get_flagstatus_helper "${1}" 3 "G" - echo -n '] ' + local E=$(get_flagstatus_helper "${1}" 0 "E") + local C=$(get_flagstatus_helper "${1}" 1 "C") + local D=$(get_flagstatus_helper "${1}" 2 "D") + local G=$(get_flagstatus_helper "${1}" 3 "G") + # + # Use flag precedence is defined (at least) at: + # http://www.gentoo.org/doc/en/handbook/handbook-x86.xml?part=2&chap=2 + for location in "E" "C" "D" "G"; do + if [[ ${!location} == $location ]]; then + local ACTIVE="+" + break + elif [[ ${!location} != " " ]]; then + local ACTIVE="-" + break + fi + done + echo -n "[${ACTIVE:--}$E$C$D$G] " + if [[ $ACTIVE == "+" ]]; then + return 0 + else + return 1 + fi +} + +get_flagstatus_pkg() { + # Arguments: + # $1 - flag (gtk) + # $2 - package atom (www-client/elinks) + # $3 - +/- whether flag is enabled by global configuration files + # + # Pre-cache the use flags declared in ebuilds first. + # This is required as calling it inside a $() seems to + # prevent caching of results into $ACTIVE_FLAGS array + get_useflaglist_ebuild ${2} > /dev/null + # + # Fetch list of package ebuilds matching the query from equery. + # Since determining the current version of a package requires + # scanning for available ones, inpecting the KEYWORDS variable + # and then checking in a heap of other places for exceptions, + # equery is a simple alternative + # + echo "${ACTIVE_FLAGS[5]}" | grep $2 | while read pkgline; do + # This is a bit crazy, but faster than calling equery + OIFS=$IFS; IFS=";"; INFO=($pkgline); IFS=$OIFS; + local version=${INFO[1]} + local slot=${INFO[2]} + local iuse=${INFO[3]} + if [[ $slot == 'SLOT="0"' || $slot == "" ]]; then + slot=""; + else + slot="($(echo ${slot} | cut -d\" -f2)) " + fi + # + # Fetch enabled status for this version + local P=$(get_flagstatus_helper_pkg "${1}" 4 "P" "" "${2}" "${version}") + local B=$(get_flagstatus_helper_ebuild "${1}" "${iuse}" "B" "" "${2}" "${version}") + ACTIVE=; UNUSED= + for location in "P" "B"; do + if [[ ${!location} == $location ]]; then + ACTIVE="+" + break + elif [[ ${!location} == "!" ]]; then + UNUSED=1 + elif [[ ${!location} != " " ]]; then + ACTIVE="-" + break + fi + done + if [[ $UNUSED ]]; then + echo " - ${slot}${version}" + else + echo " [${ACTIVE:-${3:- }}$P$B] ${slot}${version}" + fi + done + echo } # faster replacement to `portageq portdir` @@ -285,6 +496,7 @@ local found_one local args + set -f args="${*:-*}" if [ -z "${SCOPE}" ]; then @@ -299,7 +511,7 @@ [ "${SCOPE}" == "global" ] && echo "global use flags (searching: ${args})" [ "${SCOPE}" == "local" ] && echo "local use flags (searching: ${args})" echo "************************************************************" - + set +f if [ "${args}" == "*" ]; then args="$(get_useflaglist | sort -u)" fi @@ -327,9 +539,15 @@ pkg="$(echo $line | cut -d\| -f 1)" flag="$(echo $line | cut -d\| -f 2)" desc="$(echo $line | cut -d\| -f 3)" - get_flagstatus "${flag}" - printf "%s (%s):\n%s\n\n" "${flag}" "${pkg}" "${desc}" + if get_flagstatus "${flag}"; then + ACTIVE="+" + else + ACTIVE="-" + fi + printf "%s\n %s: %s\n" "${flag}" "${pkg}" "${desc}" + get_flagstatus_pkg "${flag}" "${pkg}" "${ACTIVE}" done + echo fi shift done @@ -351,8 +569,8 @@ args=("${@:-*}") case "${SCOPE}" in - "global") echo "global use flags (searching: ${args})";; - "local") echo "local use flags (searching: ${args})";; + "global") echo "global use flags (searching: ${args[@]})";; + "local") echo "local use flags (searching: ${args[@]})";; *) SCOPE="global" showinstdesc "${args[@]}" echo SCOPE="local" showinstdesc "${args[@]}" @@ -366,7 +584,7 @@ args="$(get_useflaglist | sort -u)" fi - set "${args[@]}" + set ${args[@]} while [ -n "${1}" ]; do case "${SCOPE}" in @@ -394,10 +612,10 @@ # print name only if package is installed # NOTE: If we implement bug #114086 's enhancement we can just use the # exit status of equery instead of a subshell and pipe to wc -l - if [ $(equery -q -C list -i -e "${pkg}" | wc -l) -gt 0 ]; then + if [ $(equery -q -C list -i "${pkg}" | wc -l) -gt 0 ]; then foundone=1 IFS="$OIFS" - get_flagstatus "${flag}" + get_flagstatus "${flag}" "${pkg}" IFS=': ' printf "%s (%s):\n%s\n\n" "${flag}" "${pkg}" "${desc#- }" fi @@ -428,11 +646,20 @@ set ${args} while [ -n "${1}" ]; do + get_portageuseflags if echo " ${ACTIVE_FLAGS[9]} " | grep " ${1} " > /dev/null; then printf "%-20s" ${1} get_flagstatus ${1} echo fi + if echo " ${ACTIVE_FLAGS[4]} " | egrep -e " -?${1} " > /dev/null; then + for pkg in $( echo "${ACTIVE_FLAGS[4]}" | \ + egrep " -?${1} " | cut -d " " -f 2); do + printf "%-20s" ${1} + SCOPE="local" get_flagstatus ${1} "${pkg}" + printf "(%s)\n" ${pkg} + done; + fi shift done } @@ -446,9 +673,163 @@ NEW_MAKE_CONF_USE="${NEW_MAKE_CONF_USE// ${1} / }" } +# Simple utility to remove empty files from package.use +clean_package_use() { +if [[ -d ${PACKAGE_USE_PATH} ]]; then + for f in $(find ${PACKAGE_USE_PATH} -size 0); do + echo "Removing empty file ""${f}""" + rm ${f} + done; +fi +} + +# Utility to remove a use flag from a file in package.use[/] +# Args: (1) File, (2) Use flag +scrub_use_flag() { + local atom_re="^[<>]?=?([a-z][\da-z/-]+[a-z])(-[0-9pr._*-]+)?" + local filename=${1} + # Ignore leading - on flag + local flag=${2#*-} + local pkg=$(echo "${PACKAGE}" | sed -re "s/${atom_re}/\1/") + local pkg_re="[<>]?=?${pkg}(-[\dpr._*-]+)?" + + while read line; do + # Skip (preserve) comments on their own lines + if [[ -z $(echo "${line}" | sed -re "s/^ *#.*$//") ]]; then + echo "${line}" + # Detect if requested package is defined on this line + elif [[ -n ${PACKAGE} ]]; then + if [[ -n $(echo ${line} | grep -Ee "[^#]*${pkg_re}") ]]; then + # If this is the only (remaining) use flag defined + # for this package, then remove the whole line + if [[ -z $(echo ${line} | \ + grep -Pe "^[^#]*${pkg_re}[ ]*-?${flag}[ ]$") ]]; then + # Remove flag from this line + echo "${line}" | sed -re "s/-?\b${flag}\b//" + fi + else + # Passthru + echo "${line}" + fi + # If line only has this use flag, let it be removed + # (used if PACKAGE is not defined -- from pruning) + elif [[ -z $(echo ${line} | \ + grep -re "[^#]*${atom_re}.*-?${flag}") ]]; then + # Remove flag from this line + echo "${line}" | sed -re "s/-?\b${flag}\b//" + else + # Passthru + echo "${line}" + fi; + done < <(cat "${filename}") > "${filename}"; +} + +modify_package() { + get_useflags + + local atom_re="^[<>]?=?([a-z][\da-z/-]+[a-z])(-[0-9pr._*-]+)?" + local pkg=$(echo "${PACKAGE}" | sed -re "s/${atom_re}/\1/") + local pkg_re="[<>]?=?${pkg}(-[\dpr._*-]+)?" + + while [[ -n ${1} ]]; do + local flag=${1} + # + # --- Sanity checks + # (1) make sure ${pkg} exists in portdir + if [[ ! -d "$(get_portdir)/${pkg}" ]]; then + error "Package ""${pkg}"" does not exist" + # + # (2) make sure ${flag} is defined in get_useflaglist + elif [[ ! -n $(echo " $(get_useflaglist) " | grep "${flag}") ]]; then + warn "USE flag ""${flag}"" does not exist" + # Don't bail just because of this, just warn + fi; + # If flag is enabled in portage USE flags (emerge --info), + # then "remove"ing the flag should be replaced with adding + # the negative flag instead + if [[ "${ACTION}" == "remove" && + -z $( echo " ${ACTIVE_FLAGS[4]} " | egrep -e " ${PACKAGE}.*${flag}\b" ) && + $( get_flagstatus ${flag} ${pkg} | grep "+" ) ]]; then + flag="-${flag}" + ACTION="add" + # If flag is currently disabled for the package requested + # to be enabled in, then "remove" the negative + elif [[ "${ACTION}" == "add" && + -n $( echo " ${ACTIVE_FLAGS[4]} " | egrep -e " ${PACKAGE}.*-${flag}\b" ) ]]; then + flag="-${flag}" + ACTION="remove" + fi; + case "${ACTION}" in + "add") + local filename + if [[ -d ${PACKAGE_USE_PATH} ]]; then + # Use naming convention of package.use/package + filename="${PACKAGE_USE_PATH}/${pkg#*/}" + if [[ ! -s "${filename}" ]]; then + # Create new file to contain flag + echo "${PACKAGE} ${flag}" > "${filename}" + echo "Adding ""${PACKAGE}:${flag}"" use flag to new file ""${filename}""" + return + fi; + else + # Add to package.use file instead + filename=${PACKAGE_USE_PATH} + # Create as necessary + touch "${filename}" + fi; + # Walk through the file and add the flag manually + echo "Adding ""${PACKAGE}:${flag}"" use flag in ""${filename}""" + local added=0 + while read line; do + if [[ -n $(echo "${line}" | egrep -e "^[^#]*${pkg_re}") ]]; then + echo "$(reduce_package_use "${line} ${flag}" | cut -d " " -f 2-)" + added=1 + else + # Passthru + echo "${line}" + fi; + done < <(cat "${filename}") > ${filename} + if [[ ${added} == 0 ]]; then + echo "${PACKAGE} ${flag}" >> "${filename}" + fi + ;; + "remove") + local filename + if [[ -d ${PACKAGE_USE_PATH} ]]; then + # Scan for file containing named package and use flag + filename=$( egrep -rle "${pkg_re}.*[^-]${flag}( |$)" "${PACKAGE_USE_PATH}") + if [[ -z "${filename}" ]]; then + error ""${flag}" is not defined for package "${PACKAGE}"" + return + fi; + else + # Remove from package.use instead + filename=${PACKAGE_USE_PATH} + # Create as necessary + touch "${filename}" + fi; + # Scrub use flag from matched files + for f in ${filename}; do + # Remove current flags in file + echo "Removing ""${PACKAGE}:${flag}"" use flag in ""${f}""" + scrub_use_flag ${f} ${flag} + done; + # Remove empty files + clean_package_use + ;; + esac; + shift; + done; +} + # USE flag modification function. Mainly a loop with calls to add_flag and # remove_flag to create a new USE string which is then inserted into make.conf. modify() { + if [[ -n ${PACKAGE} ]]; then + modify_package ${*} + return; + fi; + if [ -z "${*}" ]; then if [ "${ACTION}" != "prune" ]; then echo "WARNING: no USE flags listed for modification, do you really" @@ -488,6 +869,22 @@ elif echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then remove_flag "-${1}" fi + # Locate use flag in package.use + local filename + if [[ -d ${PACKAGE_USE_PATH} ]]; then + filename=$( egrep -rle "-?\b${1}\b" "${PACKAGE_USE_PATH}") + else + # Scrub from package.use file + filename=${PACKAGE_USE_PATH} + fi + # Scrub use flag from matched files + for f in ${filename}; do + # Remove current flags in file + echo "Disabling ""${1}"" use flag in ""${f}""" + scrub_use_flag ${f} ${1} + done; + # Remove empty files from package.use + clean_package_use shift fi done @@ -545,6 +942,7 @@ set -f parse_arguments "$@" check_sanity +set +f eval ${MODE} ${ARGUMENTS} -set +f +