# -*-shell-script-*-

# Dependancy management code

###
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright 2002-2003 Mike Hearn (mike@theoretic.com)
#
###


# places dependancy information in the $1 info directory
# for now dependancies are stored in a text file - this may change in future
# $1 will depend on $2
function forgeDependancy() {
    trace $1
    # ensure we have an entry in the db
    if ! [[ -e "$autopackage_db_location/$1" ]]; then _mkdirs "$autopackage_db_location/$1" >/dev/null; fi;
    trace writing dependency on `justRootName $2`
    echo "$2" >> "$autopackage_db_location/$1/dependancies"
    # now link $2 to $1 so we can show the user warnings
    if ! [[ -e "$autopackage_db_location/$2" ]]; then _mkdirs "$autopackage_db_location/$2" >/dev/null; fi;
    trace writing supports entry
    echo "$1" >> "$autopackage_db_location/$2/supports"
}

# removes a dependancy from a package. $1 is the package, $2 is the dependancy
function unforgeDependancy() {
    trace $1, $2
    # Remove the dependancy
    removeLine "$autopackage_db_location/$1/dependancies" "$2"
    # now remove the supports entry
    removeLine "$autopackage_db_location/$2/supports" "$1"
}

# eliminates all links to dependancies of package $1 (usually in preparation for uninstall)
function unforgeDependancies() {
    trace $1
    if [ ! -e "$autopackage_db_location/$1/dependancies" ]; then trace "no dependencies file found"; return 0; fi
    local deps=`cat "$autopackage_db_location/$1/dependancies" | sort | uniq`
    if [[ "$deps" == "" ]]; then return 0; fi;
    local c=`getLineCount "$deps"`
    local i=1
    while (( i <= c )); do
	local dep=`getLine "$deps" $i`
	unforgeDependancy "$1" "$dep"
	(( i++ ));
    done
}


##
# _locateSkeleton <ROOTNAME>
# Returns: 0 on success
# Returns: 1 if the skeleton was not found
# Returns: 2 if found but not an ASCII text file
#
# Given a root name, will search through the skeleton path until the highest numbered skeleton for that name is found.
# It will then output the absolute filename.
#
# Example:
#   _locateSkeleton "@gnome.org/libxml" => /usr/local/share/autopackage/skeletons/\@gnome.org/libxml/skeleton.2
function _locateSkeleton() {
    # first things first, locate the highest matching directory for the given root name
    # at present you need to have an entry (or symlink) for every version. At some point we may add fuzzy matching.
    local s;
    trace locating $1
    for s in "$meta_dir" $_skel_dirs; do
	if [ -e "$s/$1" ]; then
	    trace located at $s/`cat "$s/$1/skeleton_ref"`
	    if ! file "$s"/`cat "$s/$1/skeleton_ref"` | awk -F": " ' { print $2 } ' | grep ASCII >/dev/null; then
		return 2;
	    fi
	    echo $s/`cat "$s/$1/skeleton_ref"`
	    return 0;
	fi
    done
    return 1;
}

##
# checkForPackage <ROOTNAME> [... arguments to skeleton file test section]
# 
# Calls the test section of a skeleton file for the given root name. The results
# are available in the $INTERFACE_VERSIONS and $SOFTWARE_VERSIONS variables. If the
# package wasn't found, these variables are empty, otherwise they contain a space
# delimited list of version numbers that were detected. The ones you're probably 
# interested in are the interface numbers, these identify a logical software interface.
#
# Optional arguments given after the root name are passed to the skeleton test script
# in $@, so can be accessed using the standard $1, $2, $3 etc variables
function checkForPackage() {
    trace checking for $1
    local _skelpath=` _locateSkeleton $1 `
    local _test=$( getSection "$_skelpath" Test )
    shift;
    unset INTERFACE_VERSIONS
    unset SOFTWARE_VERSIONS
    # process the [Test] script
    eval "$_test";
    # code after this point may not be executed
}

##
# retrieve <ROOTNAME> [<MAJOR>[.REVISION]]
# Returns: 0 on success
# Returns: non-zero on failure
#
# Will attempt to locate and install a package either from the local computer or a
# remote network given either a versioned rootname (for example @foo.org/bar:6.4b)
# or an unversioned root name with an interface number.
#
# This function is primarily used by require(), however it's available for your own
# package scripts as well.
#
# If you specify a versioned root name (should not contain a release number) then
# you should not give an interface number, if you do it will be ignored.
#
# If you specify an unversioned root name, then the system will select the best
# package that satisfies the given interface. For example, requesting an interface
# 1.2 will allow a match against any package that advertises fulfilment of interface
# 1.2, 1.3, 1.4, 1.5 and so on (but not 0.x nor 2.x)
#
# If a package is succesfully located, the return code is the exit code of the package
# installation. Note that when invoked from inside other installers, the package will
# be prepared, but not fully installed. Instead, the payload will be registered, and
# later all the install sections of the packages that were prepared will be run at once.
#
# Example:
# retrieve @foo.org/bar 2.2   => will install any package that can fulfil interface 2.2
# retrieve @bar.org/foo:2003  => will attempt to install Foo 2003, no other version will do
function retrieve() {

    local __pkg_name # we alter this to the real location as resolution proceeds

    # search packages in the same directory that the package was started with
    local f;
    local r;
    local oIFS="$IFS"
    IFS=$'\n'
    for f in `ls -1 "$executed_from_directory" | grep '\.package$'`; do
	trace scanning $f
	r=$( head -n 20 "$executed_from_directory/$f" | grep ROOTNAME | sed 's/# ROOTNAME //' )
	r=$( justRootName "$r" ) # strip any version info
	trace root name is \"$r\"
	if [[ "$1" == "$r" ]]; then
	    # ok, the root name matches. did we specify a versioned rootname?
	    trace .. matched
	    if [[ $( versionFromRootName "$1" ) != "" ]]; then
		# yes, so we want to match exactly, as only the given version is good enough
		local v=$( head -n 20 "$executed_from_directory/$f" | grep SOFTWARE-VERSION | sed 's/# SOFTWARE-VERSION //' )
		if [[ "$v" == $( versionFromRootName "$1" ) ]]; then
		    __pkg_name="$executed_from_directory/$f"
		    trace ... and found
		    break;
		fi
		trace but the version was not correct
	    else
		# we need to match against the interface version
		
		if [[ `getMinor $2` == "" ]]; then requiredver="$2.0"; else requiredver="$2"; fi # normalise $2 into A.B form
		trace we need $requiredver
		local if_version=$( head -n 20 "$executed_from_directory/$f" | grep INTERFACE-VERSION | sed 's/# INTERFACE-VERSION //' )
		if_version=$( countDownVersions $if_version )
		trace interfaces this package fulfils is $if_version
		local v
		local found
		found="false"
		IFS=" "
		for v in $if_version; do
		    trace v is \"$v\"
		    if [[ "$v" == "$requiredver" ]]; then __pkg_name="$executed_from_directory/$f"; trace matched; found="true"; break; fi
		done
		IFS=$'\n'		
		if $found; then break; fi;
		
		trace required interface is not implemented by this package, keep looking....
	    fi
	fi    
    done
    IFS="$oIFS"

    # test to see if the package was found, if not error back
    if [[ "$__pkg_name" == "" ]]; then
	err "No network support yet, sorry!"
	warn no package found;
	return 1;
    fi

    # at this point, __pkg_name points to a local file

    # what is the versioned root name of this package?
    RETRIEVAL_VERSIONED_ROOTNAME=$( head $__pkg_name | grep "ROOTNAME" | sed 's/# ROOTNAME //' )
    trace versioned rootname is $RETRIEVAL_VERSIONED_ROOTNAME
    export RETRIEVAL_VERSIONED_ROOTNAME
    
    # TODO: verification that this is an autopackage package
    # here we go!
    trace executing $__pkg_name
    $__pkg_name -w "$working_dir"
    trace execution of $__pkg_name complete

    # that script will not have actually run the [Script-Install] section, it's delayed for various reasons.
    registerPayload "$RETRIEVAL_VERSIONED_ROOTNAME" # ... so register it for later installation
    
    return $?;
}

# places the given skeleton file into the database, and places shortname symlinks
# this is used for verifying packages later
# $1 the root name
function addSkeletonFile() {
    # copy skeleton to db here
    _mkdirs "$autopackage_db_location/$1" >/dev/null
    local skeldirs_backup="$skel_dirs" # i really need to write a stack for bash
    skeldirs="$autopackage_db_location"
    local skelfile=`_locateSkeleton $1`
    cp "$skelfile" "$autopackage_db_location/$1"
    # add shortname symlinks
    # note we use _ variables here to make them private, to avoid conflicting with global vars for this package
    local _meta=`getSection $skelfile Meta`
    local _shortname=`getKey "$_meta" ShortName`
    rm "$autopackage_db_location/${_shortname}" 2>/dev/null;
    ln -s -f "$autopackage_db_location/$1" "$autopackage_db_location/${_shortname}"
    skeldirs="$skeldirs_backup";
}


# registers a root name for later installation, part of building the "installation tree"
function registerPayload() {
    trace "Registering $1 as a payload in $meta_dir/payloads"
    echo "$1" >>"$meta_dir/payloads"
}

##
# require <root name> <major>[minor] [extra arguments]
# 
# Require allows you to depend upon an implementation of a particular interface being present on the system.
# The interface you wish to depend on is specified by a combination of the root name and the major/minor code.
# The root name should not have a version number in it - it identifies a particular skeleton to use, not a package.
# The best package to implement that interface will automatically be located and retrieved if it's not found.
#
# Optional arguments can be added to the end in any format, it's up to the skeleton file to figure out what to do with
# them, so refer to the notes for that skeleton.
# 
# Examples:
#   require "@gnome.org/libxml" 0
#   require "@gnome.org/libxml" 1.2 --with-xslt      (skeleton specific arguments)
#
function require() {
    pushOptE; set +e;
    # output the fact that we are doing a dependancy check

    if [[ $1 == "" ]] || [[ $2 == "" ]]; then
	local e=`out "$intl_BAD_CALL" "$intl_BAD_CALL_REQUIRE1"`
	outputFail "$e"
	popOptE
	return 1
    fi

    if [[ `getMinor $2` == "" ]]; then requiredver="$2.0"; else requiredver="$2"; fi # normalise $2 into A.B form
    local skelfile=`_locateSkeleton $1`
    trace skelfile is $skelfile
    local meta=`getSection $metadata_dir/$skelfile Meta`
    local displayname=`getKey "$meta" DisplayName`
    local found="n"
    outputTest "$displayname"
    checkForPackage "$@";
    trace INTERFACE_VERSIONS after checking is $INTERFACE_VERSIONS
    # check for versions
    INTERFACE_VERSIONS=$( countDownVersions $INTERFACE_VERSIONS | stripDupedItems );
    trace "INTERFACE_VERSIONS (after stripping)=$INTERFACE_VERSIONS"
    trace "requiredver is $requiredver"
    for v in $INTERFACE_VERSIONS; do
	[[ "$v" == "$requiredver" ]] && found="y";
	[[ `getMajor $v` == "$requiredver" ]] && found="y";
    done

    if [[ "$found" == "y" ]]; then
	outputTestPass;
	# have we already got the skeleton info
	if [[ ! -e "$autopackage_global_db_location/$1" ]] && [[ ! -e "$autopackage_user_db_location/$1" ]]; then
	    addSkeletonFile "$1"
	fi
	forgeDependancy "$ROOTNAME" "$1" # this will make a useless supports file, but oh well, FIXME later
	popOptE;
	return 0;
    else
	fail_code=$?
	outputTestFail;
	# if we fail, then call retrieve (ie fail for now)
	local _a
	if [[ "$fail_code" == "$TEST_FAIL" ]]; then
	    _a=`out "$intl_FUNCLIB_PACKAGE_REQUIRED" "$displayname"`
	elif [[ "$fail_code" == "$TEST_FAIL_WRONG_VERSION" ]]; then
	    _a=`out "$intl_FUNCLIB_PACKAGE_BAD_VERSION" "$displayname"`
	fi
	outputStatus "$_a"
	# at this point, attempt to locate and install the dependancy.
	# this can be from the net, or from the same directory (or from the net cache)
	# for now, we always fail the download

	# get the [retrieval] section of the skeleton
	retrieval="function __retrieval() {
		     `getSection \"$skelfile\" Retrieval`
		   }"

	local __pkg_versioned_rootname
	eval "$retrieval"
	INTERFACE_VERSION=$requiredver	
	if __retrieval ; then
	    outputStatus "$intl_FUNCLIB_RETRIEVAL_OK"
	    addSkeletonFile "$1"
	    forgeDependancy "$ROOTNAME" "$RETRIEVAL_VERSIONED_ROOTNAME"

	    unset INTERFACE_VERSION
	    unset RETRIEVAL_VERSIONED_ROOTNAME
	    
	    popOptE;
	    return 0;
	else
	    outputFail `out "$intl_FUNCLIB_RETRIEVAL_FAILED" "$1"`;
	    outputStatus "WRITE ME! :)"
	    unset INTERFACE_VERSION
	    
	    popOptE;
	    exit 1; # quit the forked install script
	fi
   fi
   popOptE
}

function recommend() {
    # output the fact that we are doing a dependancy check
    meta=`getSection $1/$skeleton_filename Meta`
    displayname=`getKey "$meta" DisplayName`
    outputTest "$displayname"

    if checkForPackage $1; then
	outputTestPass;
	return 0;
    else
	outputRecommend `out "$intl_FUNCLIB_RECOMMEND" "$2"`
	return 1;
    fi
}

##
# requireFile <file-path>
# file-path: An absolute path to the file that is required
#
# This function should be called from prep scripts when you depend on a file
# existing in a well known location. Examples of where this is useful would be
# for scripts that have an absolute path in their shebang line.
#
# This function outputs the test, and returns 1 if the file is not present. Otherwise it returns 0.
#
# Example:
# requireFile /bin/bash
# requireFile /usr/bin/python
function requireFile() {
    pushOptE; set +e;
    outputTest "existance of $1"
    if [ -e $1 ]; then 
       outputTestPass;
       popOptE;
       return 0;
    else
       outputTestFail;
       popOptE;
       return 1;
    fi
}
