Hello...

Seeing that some packages in LFS require pkg-config to build, I wrote a
bash script that implements most of pkg-config's functionality, in order
to avoid setting the flags manually for each package that needs it. I
put it in /tools/bin and did a full build of LFS, with a few packages
from BLFS, without any problems. I also verified that it returns the
same results as the standard implementation for all calls to pkg-config
made during the build, except for differences in error messages and the
order of flags of independent libraries.

I thought some of you might find it useful too. Not sure if and how it
can be used in the book, though.

What do you think?
#!/bin/bash

# version of pkg-config
this_version=0.26

# List of "global" variables, defined for all packages. The list includes
# pre-defined variables such as "pc_top_builddir" (not yet implemented) and
# variables defined by the user with --define-variable.
declare -A global_vars

# Usage: cmp_versions VERSION-1 CMP VERSION-2
#
# CMP must be either "=", "!=", "<", ">", "<=", or ">=". The function returns 0
# if the comparison is true, 1 otherwise.
#
# This is how the standard pkg-config compares two version strings:
# 1. Both strings are split into distinct segments. Each segment contains
#    either only digits or only letters. Other characters between segments or
#    at the start or the end of the strings are ignored. The segments may be of
#    different lengths in the two strings.
# 2. If both strings have no segments left, they are compared equal; stop and
#    return this result. If only one of the strings has no segments left, it is
#    compared less than the other string; stop and return this result.
# 3. Examine the first segment of each string. If one of the segments contains
#    digits and the other segment contains letters, the former is compared
#    greater than the latter; stop and return this result.
# 4. Compare the first segment of the strings, either numerically (if both
#    contain only digits) or lexicographically (if both contain only letters).
#    If they are not compared equal, stop and return the result.
# 5. Move to the next segment of each string and go back to step 2.
function cmp_versions () {
        local str1="$1"
        local cmp="$2"
        local str2="$3"
        local first1 first2
        local seg1 seg2

        # remove non-alphanumeric characters from the beginning
        str1="$(expr "$str1" : '[^0-9A-Za-z]*\(.*\)')"
        str2="$(expr "$str2" : '[^0-9A-Za-z]*\(.*\)')"

        while [ -n "$str1" -a -n "$str2" ]
        do
                # extract the first segments and check that they have the same
                # type
                first1=${str1:0:1}
                first2=${str2:0:1}
                case "$first1$first2" in
                        [0-9][A-Za-z])
                                [ "$cmp" = "!=" -o "$cmp" = ">" -o "$cmp" = 
">=" ]
                                return
                                ;;
                        [A-Za-z][0-9])
                                [ "$cmp" = "!=" -o "$cmp" = "<" -o "$cmp" = 
"<=" ]
                                return
                                ;;
                        [0-9][0-9])
                                seg1="$(expr "$str1" : '\([0-9]*\)')"
                                seg2="$(expr "$str2" : '\([0-9]*\)')"
                                ;;
                        [A-Za-z][A-Za-z])
                                seg1="$(expr "$str1" : '\([A-Za-z]*\)')"
                                seg2="$(expr "$str2" : '\([A-Za-z]*\)')"
                                ;;
                esac

                # expr compares numerically or lexicographically
                if expr "$seg1" "!=" "$seg2" >/dev/null
                then
                        if expr "$seg1" ">" "$seg2" >/dev/null
                        then
                                [ "$cmp" = "!=" -o "$cmp" = ">" -o "$cmp" = 
">=" ]
                                return
                        else
                                [ "$cmp" = "!=" -o "$cmp" = "<" -o "$cmp" = 
"<=" ]
                                return
                        fi
                fi

                # remove the first segment and non-alphanumeric characters that
                # may follow it
                str1="${str1#$seg1}"
                str2="${str2#$seg2}"

                str1="$(expr "$str1" : '[^0-9A-Za-z]*\(.*\)')"
                str2="$(expr "$str2" : '[^0-9A-Za-z]*\(.*\)')"
        done

        # if only one is empty, it is compared less than the other
        if [ -n "$str1" ]
        then
                [ "$cmp" = "!=" -o "$cmp" = ">" -o "$cmp" = ">=" ]
                return
        fi
        if [ -n "$str2" ]
        then
                [ "$cmp" = "!=" -o "$cmp" = "<" -o "$cmp" = "<=" ]
                return
        fi

        # if both are empty, they are compared equal
        [ "$cmp" = "=" -o "$cmp" = "<=" -o "$cmp" = ">=" ]
        return
}

pc_path=/usr/lib/pkgconfig:/usr/share/pkgconfig

function pkg_pcfile () {
        local pkg="$1"
        local dir fullpath
        local oldifs

        oldifs=$IFS
        IFS=:
        for dir in $pc_path
        do
                fullpath="$dir/$pkg.pc"

                if [ -r "$fullpath" ]
                then
                        IFS=$oldifs
                        echo "$fullpath"
                        return 0
                fi
        done

        IFS=$oldifs
        return 1
}

# Usage: substitute_vars STRING
#
# Variable names and values are passed on stdin, one variable per line, in the
# form of "name=value". The function prints the given string with '${VAR}'
# expression substituted with the value of VAR.
function substitute_vars () {
        local string="$1"
        local -A vars
        local line
        local varname varvalue
        local result

        while read line
        do
                [ -n "$line" ] || continue
                varname="${line%%=*}"
                varvalue="${line#*=}"
                vars[$varname]="$varvalue"
        done

        while [ -n "$string" ]
        do
                case "$string" in
                        \$\$*)
                                result+='$'
                                string="${string:2}"
                                ;;
                        \${*}*)
                                varname="${string:2}"
                                varname="${varname%%\}*}"
                                if [ x${global_vars[$varname]+set} = xset ]
                                then
                                        result+="${global_vars[$varname]}"
                                elif [ x${vars[$varname]+set} = xset ]
                                then
                                        result+="${vars[$varname]}"
                                else
                                        result+="\${$varname}"
                                fi
                                string="${string#"\${$varname}"}"
                                ;;
                        *)
                                result+="${string:0:1}"
                                string="${string:1}"
                                ;;
                esac
        done

        echo "$result"
}

# Usage: pkg_get_keyword PKG KEYWORD
#
# Outputs the value of KEYWORD defined for PKG. The .pc file for PKG must
# exist.
function pkg_get_keyword () {
        local pkg="$1"
        local keyword="$2"
        local -A vars
        local line
        local varlines
        local varname varvalue
        local raw_value
        local result

        while read line
        do
                case "$line" in
                        *=*)
                                varname="${line%%=*}"
                                raw_value="${line#*=}"
                                varvalue="$(echo "$varlines" | substitute_vars 
"$raw_value")"
                                vars[$varname]="$varvalue"
                                if [ -n "$varlines" ]
                                then
                                        varlines+=$'\n'"$varname=$varvalue"
                                else
                                        varlines="$varname=$varvalue"
                                fi
                                ;;
                        "$keyword:"*)
                                raw_value="${line#"$keyword:"}"
                                result="$(echo "$varlines" | substitute_vars 
"$raw_value")"
                                ;;
                esac
        done < "$(pkg_pcfile "$pkg")"

        echo $result
}

# Usage: pkg_get_var PKG VAR
#
# Outputs the value of the variable named VAR defined for PKG. If there is a
# global variable named VAR, it takes precedence over any variables defined in
# the .pc file of the package. The .pc file for PKG must exist.
function pkg_get_var () {
        local pkg="$1"
        local var="$2"
        local -A vars
        local line
        local varlines
        local varname varvalue
        local raw_value

        if [ x${global_vars[$var]+set} = xset ]
        then
                echo "${global_vars[$var]}"
                return 0
        fi

        while read line
        do
                case "$line" in
                        *=*)
                                varname="${line%%=*}"
                                raw_value="${line#*=}"
                                varvalue="$(echo "$varlines" | substitute_vars 
"$raw_value")"
                                vars[$varname]="$varvalue"
                                if [ -n "$varlines" ]
                                then
                                        varlines+=$'\n'"$varname=$varvalue"
                                else
                                        varlines="$varname=$varvalue"
                                fi
                                ;;
                esac
        done < "$(pkg_pcfile "$pkg")"

        echo "${vars[$var]}"
}

# Usage: pkg_list_vars PKG
#
# Outputs the names of all variables defined for PKG. The .pc file for PKG must
# exist.
function pkg_list_vars () {
        local pkg="$1"
        local line

        while read line
        do
                case "$line" in
                        *=*) echo "${line%%=*}" ;;
                esac
        done < "$(pkg_pcfile "$pkg")"
}

function pkg_name () {
        pkg_get_keyword "$1" "Name"
}

function pkg_description () {
        pkg_get_keyword "$1" "Description"
}

function pkg_url () {
        pkg_get_keyword "$1" "URL"
}

function pkg_version () {
        pkg_get_keyword "$1" "Version"
}

function pkg_requires () {
        pkg_get_keyword "$1" "Requires"
}

function pkg_requires_private () {
        pkg_get_keyword "$1" "Requires.private"
}

function pkg_conflicts () {
        pkg_get_keyword "$1" "Conflicts"
}

function pkg_libs () {
        pkg_get_keyword "$1" "Libs"
}

function pkg_libs_private () {
        pkg_get_keyword "$1" "Libs.private"
}

function pkg_cflags () {
        pkg_get_keyword "$1" "Cflags"
}

function print_pkg_not_found () {
        local pkg="$1"

        echo "Package $pkg was not found in the pkg-config search path.
Perhaps you should add the directory containing \`$pkg.pc'
to the PKG_CONFIG_PATH environment variable
No package '$pkg' found"
}

function print_version_mismatch () {
        local pkg="$1"
        local cmp="$2"
        local version="$3"

        echo "Requested '$pkg $cmp $version' but version of $(pkg_name $pkg) is 
$(pkg_version $pkg)"
}

function print_cmp_without_version () {
        local pkg="$1"

        echo "Comparison operator but no version after package name '$pkg'"
}

function print_var_without_value () {
        echo "--define-variable argument does not have a value for the variable"
}

function print_var_already_defined () {
        local var="$1"

        echo "Variable '$var' defined twice globally"
}

# Usage: check_pkg_list PKG...
#
# Checks that all packages in the given list exist. Each package may be
# followed by a comparison sign and a version specification, in which case the
# function also checks that the package version matches this requirement.
# Packages are separated by commas and/or whitespace.
#
# If all packages are found and their versions match the given requirements, a
# list of packages (without the version requirements) is printed to stdout and
# the function returns 0. Otherwise, an error message is printed to stderr and
# the function returns 1.
function check_pkg_list () {
        local pkg cmp arg
        local list
        local oldifs=$IFS

        IFS=$IFS,
        for arg in $*
        do
                IFS=$oldifs

                if [ -z "$pkg" ]
                then
                        pkg=$arg
                elif [ -z "$cmp" ]
                then
                        case $arg in
                                "="|"!="|"<"|">"|"<="|">=") cmp=$arg ;;
                                *)
                                        # $pkg contains the name of the
                                        # package, and it has no version
                                        # requirements. Just check that the
                                        # package exists. $arg is the name of
                                        # the next package in the list.
                                        if ! pkg_pcfile $pkg >/dev/null
                                        then
                                                print_pkg_not_found "$pkg" >&2
                                                return 1
                                        fi

                                        list="$pkg $list"

                                        pkg=$arg
                                        ;;
                        esac
                else
                        # $pkg and $cmp are set, which means we had encountered
                        # a package name followed by a comparison operator.
                        # $arg is now the version string. Check that the
                        # package exists and compare its version to $arg.
                        if ! pkg_pcfile $pkg >/dev/null
                        then
                                print_pkg_not_found "$pkg" >&2
                                return 1
                        fi
                        if ! cmp_versions "$(pkg_version $pkg)" $cmp $arg
                        then
                                print_version_mismatch $pkg $cmp $arg >&2
                                return 1
                        fi

                        list="$pkg $list"

                        pkg=
                        cmp=
                fi
        done

        if [ -n "$cmp" ]
        then
                print_cmp_without_version $pkg >&2
                return 1
        fi

        # check the last package if any
        if [ -n "$pkg" ]
        then
                if ! pkg_pcfile $pkg >/dev/null
                then
                        print_pkg_not_found "$pkg" >&2
                        return 1
                fi

                list="$pkg $list"
        fi

        echo $list
}

# Usage: remove_dups_keep_first ARG...
#
# Prints all given arguments to stdout with any duplicates removed. Only the
# first occurence of each duplicate is printed.
function remove_dups_keep_first () {
         local arg
         local -A seen
         local result

         for arg
         do
                if [ x${seen[$arg]+set} != xset ]
                then
                        result="$result $arg"
                        seen[$arg]=1
                fi
         done

         echo $result
}

# Usage: remove_dups_keep_last ARG...
#
# Prints all given arguments to stdout with any duplicates removed. Only the
# last occurence of each duplicate is printed.
function remove_dups_keep_last () {
         local arg
         local reversed
         local -A seen
         local result

         # reverse the order of the arguments
         for arg
         do
                reversed="$arg $reversed"
         done

         # prepend each newly-seen argument to $result, thereby restoring the
         # original order of the arguments
         for arg in $reversed
         do
                if [ x${seen[$arg]+set} != xset ]
                then
                        result="$arg $result"
                        seen[$arg]=1
                fi
         done

         echo $result
}

# five different types of flags:
# I - cflags that start with -I
# o - cflags that do not start with -I
# l - libs that start with -l
# L - libs that start with -L
# O - libs that do not start with -[lL]
optflags_cflags_I=I
optflags_cflags_other=o
optflags_libs_l=l
optflags_libs_L=L
optflags_libs_other=O

nonoptions=
for arg
do
        # handle options that take an argument

        if [ x${optname+set} = xset ]
        then
                arg="$optname=$arg"
                unset optname
        fi

        case "$arg" in
                --variable | --define-variable | --atleast-pkgconfig-version)
                        optname=$arg
                        continue
                        ;;
        esac

        case "$arg" in
                --version)
                        echo "$this_version"
                        exit 0
                        ;;

                --atleast-pkgconfig-version=*)
                        cmp_versions $this_version ">=" "${arg#*=}"
                        exit
                        ;;

                --modversion) opt_modversion=1 ;;

                # don't care, but configure uses them
                --print-errors) ;;
                --silence-errors) ;;
                --short-errors) ;;
                --errors-to-stdout) ;;

                --cflags) opt_flags+=$optflags_cflags_I$optflags_cflags_other ;;
                --cflags-only-I) opt_flags+=$optflags_cflags_I ;;
                --libs) 
opt_flags+=$optflags_libs_l$optflags_libs_L$optflags_libs_other ;;
                --libs-only-L) opt_flags+=$optflags_libs_L ;;
                --libs-only-l) opt_flags+=$optflags_libs_l ;;

                --variable=*) opt_variable="${arg#*=}" ;;

                --define-variable=*)
                        optarg="${arg#*=}"
                        case "$optarg" in
                                *=*)
                                        varname="${optarg%%=*}"
                                        varvalue="${optarg#*=}"
                                        ;;
                                *)
                                        print_var_without_value >&2
                                        exit 1
                        esac

                        if [ x${global_vars[$varname]+set} = xset ]
                        then
                                print_var_already_defined "$varname" >&2
                                exit 1
                        fi

                        global_vars[$varname]="$varvalue"

                        ;;

                --print-variables) opt_print_variables=1 ;;

                # I don't quite understand what this option is for, pkg-config
                # seems to check for the existence of the given packages with
                # or without it. But configure scripts use it anyway, so it
                # must be recognized here.
                --exists) ;;

                --static) opt_static=1 ;;

                -*)
                        echo "$arg: unknown option" >&2
                        exit 1
                        ;;

                *) nonoptions="$nonoptions $arg" ;;
        esac
done

if [ x${optname+set} = xset ]
then
        echo "$optname: missing argument" >&2
        exit 1
fi

# check that all given packages exist and have the required versions
cmd_pkgs=$(check_pkg_list $nonoptions)
[ $? -eq 0 ] || exit 1

if [ x${opt_modversion+set} = xset ]
then
        for pkg in $cmd_pkgs
        do
                pkg_version $pkg
        done
fi

if [ x${opt_flags+set} = xset ]
then
        # Create a list of all packages required by $cmd_pkgs, and all their
        # required packages, recursively.

        # To implement recursion, use two lists. $pkg_list is the final result,
        # which contains all required packages. $add_list is a list of packages
        # which will be added to $pkg_list after they will be checked for their
        # own required packages.
        #
        # Newly seen packages are first added to $add_list. Then, the required
        # packages of all packages in $add_list are put in a temporary list.
        # Then, all packages in $add_list are added to $pkg_list and $add_list
        # is set to the temporary list. Thus, the required packages of all
        # packages which are listed in $pkg_list are guaranteed to be either in
        # $pkg_list (if they were already checked for their own dependencies)
        # or in $add_list (if they are about to be checked in the next
        # iteration). The process continues until there are no packages in
        # $add_list, which means that there are no more packages to check for
        # dependencies.

        pkg_list=
        add_list=$cmd_pkgs

        while [ -n "$add_list" ]
        do
                requires=
                for pkg in $add_list
                do
                        requires="$requires $(pkg_requires $pkg)"
                done

                tmp=$(check_pkg_list $requires)
                [ $? -eq 0 ] || exit 1

                pkg_list="$pkg_list $add_list"
                add_list=$tmp
        done

        # do the same, but this time add also packages listed in
        # "Requires.private" for each package

        pkg_list_with_private=
        add_list=$cmd_pkgs

        while [ -n "$add_list" ]
        do
                requires=
                for pkg in $add_list
                do
                        requires="$requires $(pkg_requires_private $pkg)"
                        requires="$requires $(pkg_requires $pkg)"
                done

                tmp=$(check_pkg_list $requires)
                [ $? -eq 0 ] || exit 1

                pkg_list_with_private="$pkg_list_with_private $add_list"
                add_list=$tmp
        done

        # As an optimization, remove duplicates from the package lists so that
        # each package will be checked only once. We must keep the last
        # occurence of each package in order to preserve dependency order.
        pkg_list=$(remove_dups_keep_last $pkg_list)
        pkg_list_with_private=$(remove_dups_keep_last $pkg_list_with_private)

        # get a list of flags for all required packages

        pkg_cflags=
        for pkg in $pkg_list_with_private
        do
                pkg_cflags="$pkg_cflags $(pkg_cflags $pkg)"
        done

        pkg_libs=
        if [ x${opt_static+set} = xset ]
        then
                for pkg in $pkg_list_with_private
                do
                        pkg_libs="$pkg_libs $(pkg_libs $pkg)"
                        pkg_libs="$pkg_libs $(pkg_libs_private $pkg)"
                done
        else
                for pkg in $pkg_list
                do
                        pkg_libs="$pkg_libs $(pkg_libs $pkg)"
                done
        fi

        # classify all flags and remove ignored flags

        pkg_cflags_I=
        pkg_cflags_other=
        pkg_libs_l=
        pkg_libs_L=
        pkg_libs_other=

        for flag in $pkg_cflags
        do
                case $flag in
                        -I/usr/include) ;;
                        -I*) cflags_I="$cflags_I $flag" ;;
                        *) cflags_other="$cflags_other $flag" ;;
                esac
        done

        for flag in $pkg_libs
        do
                case $flag in
                        -L/usr/lib) ;;
                        -L*) libs_L="$libs_L $flag" ;;
                        -l*) libs_l="$libs_l $flag" ;;
                        *) libs_other="$libs_other $flag" ;;
                esac
        done

        # If libx depends on liby, then -lx should appear before -ly on the
        # command-line. Therefore, it is important that the last occurence of
        # each duplicate, rather than the first, will be used when removing
        # duplicate flags in $libs_l.
        cflags_I=$(remove_dups_keep_first $cflags_I)
        cflags_other=$(remove_dups_keep_first $cflags_other)
        libs_L=$(remove_dups_keep_first $libs_L)
        libs_l=$(remove_dups_keep_last $libs_l)
        libs_other=$(remove_dups_keep_first $libs_other)

        # now print all flags requested (in the same order that the standard
        # pkg-config prints them)
        out=
        [[ $opt_flags == *${optflags_cflags_other}* ]] && out="$out 
$cflags_other"
        [[ $opt_flags == *${optflags_cflags_I}* ]] && out="$out $cflags_I"
        [[ $opt_flags == *${optflags_libs_other}* ]] && out="$out $libs_other"
        [[ $opt_flags == *${optflags_libs_L}* ]] && out="$out $libs_L"
        [[ $opt_flags == *${optflags_libs_l}* ]] && out="$out $libs_l"
        echo $out
fi

if [ x${opt_variable+set} = xset ]
then
        value=
        for pkg in $cmd_pkgs
        do
                value="$value $(pkg_get_var $pkg "$opt_variable")"
        done

        echo $value
fi

if [ x${opt_print_variables+set} = xset ]
then
        for pkg in $cmd_pkgs
        do
                pkg_list_vars $pkg
        done
fi

exit 0
-- 
http://linuxfromscratch.org/mailman/listinfo/lfs-dev
FAQ: http://www.linuxfromscratch.org/faq/
Unsubscribe: See the above information page

Reply via email to