# package.tcl --
#
# utility procs formerly in init.tcl which can be loaded on demand
# for package management.
#
# RCS: @(#) $Id: package.tcl,v 1.2 2004/10/28 21:58:13 cgregg Exp $
#
# Copyright (c) 1991-1993 The Regents of the University of California.
# Copyright (c) 1994-1998 Sun Microsystems, Inc.
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
#

# Create the package namespace
namespace eval ::pkg {
}

# pkg_compareExtension --
#
#  Used internally by pkg_mkIndex to compare the extension of a file to
#  a given extension. On Windows, it uses a case-insensitive comparison
#  because the file system can be file insensitive.
#
# Arguments:
#  fileName name of a file whose extension is compared
#  ext      (optional) The extension to compare against; you must
#       provide the starting dot.
#       Defaults to [info sharedlibextension]
#
# Results:
#  Returns 1 if the extension matches, 0 otherwise

proc pkg_compareExtension { fileName {ext {}} } {
    global tcl_platform
    if {![string length $ext]} {set ext [info sharedlibextension]}
    if {[string equal $tcl_platform(platform) "windows"]} {
        return [string equal -nocase [file extension $fileName] $ext]
    } else {
        # Some unices add trailing numbers after the .so, so
        # we could have something like '.so.1.2'.
        set root $fileName
        while {1} {
            set currExt [file extension $root]
            if {[string equal $currExt $ext]} {
                return 1
            } 

        # The current extension does not match; if it is not a numeric
        # value, quit, as we are only looking to ignore version number
        # extensions.  Otherwise we might return 1 in this case:
        #       pkg_compareExtension foo.so.bar .so
        # which should not match.

        if { ![string is integer -strict [string range $currExt 1 end]] } {
        return 0
        }
            set root [file rootname $root]
    }
    }
}

# pkg_mkIndex --
# This procedure creates a package index in a given directory.  The
# package index consists of a "pkgIndex.tcl" file whose contents are
# a Tcl script that sets up package information with "package require"
# commands.  The commands describe all of the packages defined by the
# files given as arguments.
#
# Arguments:
# -direct       (optional) If this flag is present, the generated
#           code in pkgMkIndex.tcl will cause the package to be
#           loaded when "package require" is executed, rather
#           than lazily when the first reference to an exported
#           procedure in the package is made.
# -verbose      (optional) Verbose output; the name of each file that
#           was successfully rocessed is printed out. Additionally,
#           if processing of a file failed a message is printed.
# -load pat     (optional) Preload any packages whose names match
#           the pattern.  Used to handle DLLs that depend on
#           other packages during their Init procedure.
# dir -         Name of the directory in which to create the index.
# args -        Any number of additional arguments, each giving
#           a glob pattern that matches the names of one or
#           more shared libraries or Tcl script files in
#           dir.

proc pkg_mkIndex {args} {
    global errorCode errorInfo
    set usage {"pkg_mkIndex ?-direct? ?-lazy? ?-load pattern? ?-verbose? ?--? dir ?pattern ...?"};

    set argCount [llength $args]
    if {$argCount < 1} {
    return -code error "wrong # args: should be\n$usage"
    }

    set more ""
    set direct 1
    set doVerbose 0
    set loadPat ""
    for {set idx 0} {$idx < $argCount} {incr idx} {
    set flag [lindex $args $idx]
    switch -glob -- $flag {
        -- {
        # done with the flags
        incr idx
        break
        }
        -verbose {
        set doVerbose 1
        }
        -lazy {
        set direct 0
        append more " -lazy"
        }
        -direct {
        append more " -direct"
        }
        -load {
        incr idx
        set loadPat [lindex $args $idx]
        append more " -load $loadPat"
        }
        -* {
        return -code error "unknown flag $flag: should be\n$usage"
        }
        default {
        # done with the flags
        break
        }
    }
    }

    set dir [lindex $args $idx]
    set patternList [lrange $args [expr {$idx + 1}] end]
    if {[llength $patternList] == 0} {
    set patternList [list "*.tcl" "*[info sharedlibextension]"]
    }

    set oldDir [pwd]
    cd $dir

    if {[catch {eval glob $patternList} fileList]} {
    global errorCode errorInfo
    cd $oldDir
    return -code error -errorcode $errorCode -errorinfo $errorInfo $fileList
    }
    foreach file $fileList {
    # For each file, figure out what commands and packages it provides.
    # To do this, create a child interpreter, load the file into the
    # interpreter, and get a list of the new commands and packages
    # that are defined.

    if {[string equal $file "pkgIndex.tcl"]} {
        continue
    }

    # Changed back to the original directory before initializing the
    # slave in case TCL_LIBRARY is a relative path (e.g. in the test
    # suite). 

    cd $oldDir
    set c [interp create]

    # Load into the child any packages currently loaded in the parent
    # interpreter that match the -load pattern.

    if {[string length $loadPat]} {
        if {$doVerbose} {
        tclLog "currently loaded packages: '[info loaded]'"
        tclLog "trying to load all packages matching $loadPat"
        }
        if {![llength [info loaded]]} {
        tclLog "warning: no packages are currently loaded, nothing"
        tclLog "can possibly match '$loadPat'"
        }
    }
    foreach pkg [info loaded] {
        if {! [string match -nocase $loadPat [lindex $pkg 1]]} {
        continue
        }
        if {$doVerbose} {
        tclLog "package [lindex $pkg 1] matches '$loadPat'"
        }
        if {[catch {
        load [lindex $pkg 0] [lindex $pkg 1] $c
        } err]} {
        if {$doVerbose} {
            tclLog "warning: load [lindex $pkg 0] [lindex $pkg 1]\nfailed with: $err"
        }
        } elseif {$doVerbose} {
        tclLog "loaded [lindex $pkg 0] [lindex $pkg 1]"
        }
        if {[string equal [lindex $pkg 1] "Tk"]} {
        # Withdraw . if Tk was loaded, to avoid showing a window.
        $c eval [list wm withdraw .]
        }
    }
    cd $dir

    $c eval {
        # Stub out the package command so packages can
        # require other packages.

        rename package __package_orig
        proc package {what args} {
        switch -- $what {
            require { return ; # ignore transitive requires }
            default { eval __package_orig {$what} $args }
        }
        }
        proc tclPkgUnknown args {}
        package unknown tclPkgUnknown

        # Stub out the unknown command so package can call
        # into each other during their initialilzation.

        proc unknown {args} {}

        # Stub out the auto_import mechanism

        proc auto_import {args} {}

        # reserve the ::tcl namespace for support procs
        # and temporary variables.  This might make it awkward
        # to generate a pkgIndex.tcl file for the ::tcl namespace.

        namespace eval ::tcl {
        variable file       ;# Current file being processed
        variable direct     ;# -direct flag value
        variable x      ;# Loop variable
        variable debug      ;# For debugging
        variable type       ;# "load" or "source", for -direct
        variable namespaces ;# Existing namespaces (e.g., ::tcl)
        variable packages   ;# Existing packages (e.g., Tcl)
        variable origCmds   ;# Existing commands
        variable newCmds    ;# Newly created commands
        variable newPkgs {} ;# Newly created packages
        }
    }

    $c eval [list set ::tcl::file $file]
    $c eval [list set ::tcl::direct $direct]

    # Download needed procedures into the slave because we've
    # just deleted the unknown procedure.  This doesn't handle
    # procedures with default arguments.

    foreach p {pkg_compareExtension} {
        $c eval [list proc $p [info args $p] [info body $p]]
    }

    if {[catch {
        $c eval {
        set ::tcl::debug "loading or sourcing"

        # we need to track command defined by each package even in
        # the -direct case, because they are needed internally by
        # the "partial pkgIndex.tcl" step above.

        proc ::tcl::GetAllNamespaces {{root ::}} {
            set list $root
            foreach ns [namespace children $root] {
            eval lappend list [::tcl::GetAllNamespaces $ns]
            }
            return $list
        }

        # init the list of existing namespaces, packages, commands

        foreach ::tcl::x [::tcl::GetAllNamespaces] {
            set ::tcl::namespaces($::tcl::x) 1
        }
        foreach ::tcl::x [package names] {
            if {[string compare [package provide $::tcl::x] ""]} {
            set ::tcl::packages($::tcl::x) 1
            }
        }
        set ::tcl::origCmds [info commands]

        # Try to load the file if it has the shared library
        # extension, otherwise source it.  It's important not to
        # try to load files that aren't shared libraries, because
        # on some systems (like SunOS) the loader will abort the
        # whole application when it gets an error.

        if {[pkg_compareExtension $::tcl::file [info sharedlibextension]]} {
            # The "file join ." command below is necessary.
            # Without it, if the file name has no \'s and we're
            # on UNIX, the load command will invoke the
            # LD_LIBRARY_PATH search mechanism, which could cause
            # the wrong file to be used.

            set ::tcl::debug loading
            load [file join . $::tcl::file]
            set ::tcl::type load
        } else {
            set ::tcl::debug sourcing
            source $::tcl::file
            set ::tcl::type source
        }

        # As a performance optimization, if we are creating 
        # direct load packages, don't bother figuring out the 
        # set of commands created by the new packages.  We 
        # only need that list for setting up the autoloading 
        # used in the non-direct case.
        if { !$::tcl::direct } {
            # See what new namespaces appeared, and import commands
            # from them.  Only exported commands go into the index.
            
            foreach ::tcl::x [::tcl::GetAllNamespaces] {
            if {! [info exists ::tcl::namespaces($::tcl::x)]} {
                namespace import -force ${::tcl::x}::*
            }

            # Figure out what commands appeared
            
            foreach ::tcl::x [info commands] {
                set ::tcl::newCmds($::tcl::x) 1
            }
            foreach ::tcl::x $::tcl::origCmds {
                catch {unset ::tcl::newCmds($::tcl::x)}
            }
            foreach ::tcl::x [array names ::tcl::newCmds] {
                # determine which namespace a command comes from
                
                set ::tcl::abs [namespace origin $::tcl::x]
                
                # special case so that global names have no leading
                # ::, this is required by the unknown command
                
                set ::tcl::abs \
                    [lindex [auto_qualify $::tcl::abs ::] 0]
                
                if {[string compare $::tcl::x $::tcl::abs]} {
                # Name changed during qualification
                
                set ::tcl::newCmds($::tcl::abs) 1
                unset ::tcl::newCmds($::tcl::x)
                }
            }
            }
        }

        # Look through the packages that appeared, and if there is
        # a version provided, then record it

        foreach ::tcl::x [package names] {
            if {[string compare [package provide $::tcl::x] ""] \
                && ![info exists ::tcl::packages($::tcl::x)]} {
            lappend ::tcl::newPkgs \
                [list $::tcl::x [package provide $::tcl::x]]
            }
        }
        }
    } msg] == 1} {
        set what [$c eval set ::tcl::debug]
        if {$doVerbose} {
        tclLog "warning: error while $what $file: $msg"
        }
    } else {
        set what [$c eval set ::tcl::debug]
        if {$doVerbose} {
        tclLog "successful $what of $file"
        }
        set type [$c eval set ::tcl::type]
        set cmds [lsort [$c eval array names ::tcl::newCmds]]
        set pkgs [$c eval set ::tcl::newPkgs]
        if {$doVerbose} {
        if { !$direct } {
            tclLog "commands provided were $cmds"
        }
        tclLog "packages provided were $pkgs"
        }
        if {[llength $pkgs] > 1} {
        tclLog "warning: \"$file\" provides more than one package ($pkgs)"
        }
        foreach pkg $pkgs {
        # cmds is empty/not used in the direct case
        lappend files($pkg) [list $file $type $cmds]
        }

        if {$doVerbose} {
        tclLog "processed $file"
        }
    }
    interp delete $c
    }

    append index "# Tcl package index file, version 1.1\n"
    append index "# This file is generated by the \"pkg_mkIndex$more\" command\n"
    append index "# and sourced either when an application starts up or\n"
    append index "# by a \"package unknown\" script.  It invokes the\n"
    append index "# \"package ifneeded\" command to set up package-related\n"
    append index "# information so that packages will be loaded automatically\n"
    append index "# in response to \"package require\" commands.  When this\n"
    append index "# script is sourced, the variable \$dir must contain the\n"
    append index "# full path name of this file's directory.\n"

    foreach pkg [lsort [array names files]] {
    set cmd {}
    foreach {name version} $pkg {
        break
    }
    lappend cmd ::pkg::create -name $name -version $version
    foreach spec $files($pkg) {
        foreach {file type procs} $spec {
        if { $direct } {
            set procs {}
        }
        lappend cmd "-$type" [list $file $procs]
        }
    }
    append index "\n[eval $cmd]"
    }

    set f [open pkgIndex.tcl w]
    puts $f $index
    close $f
    cd $oldDir
}

# tclPkgSetup --
# This is a utility procedure use by pkgIndex.tcl files.  It is invoked
# as part of a "package ifneeded" script.  It calls "package provide"
# to indicate that a package is available, then sets entries in the
# auto_index array so that the package's files will be auto-loaded when
# the commands are used.
#
# Arguments:
# dir -         Directory containing all the files for this package.
# pkg -         Name of the package (no version number).
# version -     Version number for the package, such as 2.1.3.
# files -       List of files that constitute the package.  Each
#           element is a sub-list with three elements.  The first
#           is the name of a file relative to $dir, the second is
#           "load" or "source", indicating whether the file is a
#           loadable binary or a script to source, and the third
#           is a list of commands defined by this file.

proc tclPkgSetup {dir pkg version files} {
    global auto_index

    package provide $pkg $version
    foreach fileInfo $files {
    set f [lindex $fileInfo 0]
    set type [lindex $fileInfo 1]
    foreach cmd [lindex $fileInfo 2] {
        if {[string equal $type "load"]} {
        set auto_index($cmd) [list load [file join $dir $f] $pkg]
        } else {
        set auto_index($cmd) [list source [file join $dir $f]]
        } 
    }
    }
}

# tclPkgUnknown --
# This procedure provides the default for the "package unknown" function.
# It is invoked when a package that's needed can't be found.  It scans
# the auto_path directories and their immediate children looking for
# pkgIndex.tcl files and sources any such files that are found to setup
# the package database.  (On the Macintosh we also search for pkgIndex
# TEXT resources in all files.)  As it searches, it will recognize changes
# to the auto_path and scan any new directories.
#
# Arguments:
# name -        Name of desired package.  Not used.
# version -     Version of desired package.  Not used.
# exact -       Either "-exact" or omitted.  Not used.

proc tclPkgUnknown {name version {exact {}}} {
    global auto_path env

    if {![info exists auto_path]} {
    return
    }
    # Cache the auto_path, because it may change while we run through
    # the first set of pkgIndex.tcl files
    set old_path [set use_path $auto_path]
    while {[llength $use_path]} {
    set dir [lindex $use_path end]
    
    # Make sure we only scan each directory one time.
    if {[info exists tclSeenPath($dir)]} {
        set use_path [lrange $use_path 0 end-1]
        continue
    }
    set tclSeenPath($dir) 1

    # we can't use glob in safe interps, so enclose the following
    # in a catch statement, where we get the pkgIndex files out
    # of the subdirectories
    catch {
        foreach file [glob -directory $dir -join -nocomplain \
            * pkgIndex.tcl] {
        set dir [file dirname $file]
        if {![info exists procdDirs($dir)] && [file readable $file]} {
            if {[catch {source $file} msg]} {
            tclLog "error reading package index file $file: $msg"
            } else {
            set procdDirs($dir) 1
            }
        }
        }
    }
    set dir [lindex $use_path end]
    if {![info exists procdDirs($dir)]} {
        set file [file join $dir pkgIndex.tcl]
        # safe interps usually don't have "file readable", 
        # nor stderr channel
        if {([interp issafe] || [file readable $file])} {
        if {[catch {source $file} msg] && ![interp issafe]}  {
            tclLog "error reading package index file $file: $msg"
        } else {
            set procdDirs($dir) 1
        }
        }
    }

    set use_path [lrange $use_path 0 end-1]

    # Check whether any of the index scripts we [source]d above
    # set a new value for $::auto_path.  If so, then find any
    # new directories on the $::auto_path, and lappend them to
    # the $use_path we are working from.  This gives index scripts
    # the (arguably unwise) power to expand the index script search
    # path while the search is in progress.
    set index 0
    if {[llength $old_path] == [llength $auto_path]} {
        foreach dir $auto_path old $old_path {
        if {$dir ne $old} {
            # This entry in $::auto_path has changed.
            break
        }
        incr index
        }
    }

    # $index now points to the first element of $auto_path that
    # has changed, or the beginning if $auto_path has changed length
    # Scan the new elements of $auto_path for directories to add to
    # $use_path.  Don't add directories we've already seen, or ones
    # already on the $use_path.
    foreach dir [lrange $auto_path $index end] {
        if {![info exists tclSeenPath($dir)] 
            && ([lsearch -exact $use_path $dir] == -1) } {
        lappend use_path $dir
        }
    }
    set old_path $auto_path
    }
}

# tcl::MacOSXPkgUnknown --
# This procedure extends the "package unknown" function for MacOSX.
# It scans the Resources/Scripts directories of the immediate children
# of the auto_path directories for pkgIndex files.
# Only installed in interps that are not safe so we don't check
# for [interp issafe] as in tclPkgUnknown.
#
# Arguments:
# original -        original [package unknown] procedure
# name -        Name of desired package.  Not used.
# version -     Version of desired package.  Not used.
# exact -       Either "-exact" or omitted.  Not used.

proc tcl::MacOSXPkgUnknown {original name version {exact {}}} {

    #  First do the cross-platform default search
    uplevel 1 $original [list $name $version $exact]

    # Now do MacOSX specific searching
    global auto_path

    if {![info exists auto_path]} {
    return
    }
    # Cache the auto_path, because it may change while we run through
    # the first set of pkgIndex.tcl files
    set old_path [set use_path $auto_path]
    while {[llength $use_path]} {
    set dir [lindex $use_path end]
    # get the pkgIndex files out of the subdirectories
    foreach file [glob -directory $dir -join -nocomplain \
        * Resources Scripts pkgIndex.tcl] {
        set dir [file dirname $file]
        if {[file readable $file] && ![info exists procdDirs($dir)]} {
        if {[catch {source $file} msg]} {
            tclLog "error reading package index file $file: $msg"
        } else {
            set procdDirs($dir) 1
        }
        }
    }
    set use_path [lrange $use_path 0 end-1]
    if {[string compare $old_path $auto_path]} {
        foreach dir $auto_path {
        lappend use_path $dir
        }
        set old_path $auto_path
    }
    }
}

# tcl::MacPkgUnknown --
# This procedure extends the "package unknown" function for Mac.
# It searches for pkgIndex TEXT resources in all files
# Only installed in interps that are not safe so we don't check
# for [interp issafe] as in tclPkgUnknown.
#
# Arguments:
# original -        original [package unknown] procedure
# name -        Name of desired package.  Not used.
# version -     Version of desired package.  Not used.
# exact -       Either "-exact" or omitted.  Not used.

proc tcl::MacPkgUnknown {original name version {exact {}}} {

    #  First do the cross-platform default search
    uplevel 1 $original [list $name $version $exact]

    # Now do Mac specific searching
    global auto_path

    if {![info exists auto_path]} {
    return
    }
    # Cache the auto_path, because it may change while we run through
    # the first set of pkgIndex.tcl files
    set old_path [set use_path $auto_path]
    while {[llength $use_path]} {
    # We look for pkgIndex TEXT resources in the resource fork of shared libraries
    set dir [lindex $use_path end]
    foreach x [concat [list $dir] [glob -directory $dir -nocomplain *] ] {
        if {[file isdirectory $x] && ![info exists procdDirs($x)]} {
        set dir $x
        foreach x [glob -directory $dir -nocomplain *.shlb] {
            if {[file isfile $x]} {
            set res [resource open $x]
            foreach y [resource list TEXT $res] {
                if {[string equal $y "pkgIndex"]} {source -rsrc pkgIndex}
            }
            catch {resource close $res}
            }
        }
        set procdDirs($dir) 1
        }
    }
    set use_path [lrange $use_path 0 end-1]
    if {[string compare $old_path $auto_path]} {
        foreach dir $auto_path {
        lappend use_path $dir
        }
        set old_path $auto_path
    }
    }
}

# ::pkg::create --
#
#   Given a package specification generate a "package ifneeded" statement
#   for the package, suitable for inclusion in a pkgIndex.tcl file.
#
# Arguments:
#   args        arguments used by the create function:
#           -name       packageName
#           -version    packageVersion
#           -load       {filename ?{procs}?}
#           ...
#           -source     {filename ?{procs}?}
#           ...
#
#           Any number of -load and -source parameters may be
#           specified, so long as there is at least one -load or
#           -source parameter.  If the procs component of a 
#           module specifier is left off, that module will be
#           set up for direct loading; otherwise, it will be
#           set up for lazy loading.  If both -source and -load
#           are specified, the -load'ed files will be loaded 
#           first, followed by the -source'd files.
#
# Results:
#   An appropriate "package ifneeded" statement for the package.

proc ::pkg::create {args} {
    append err(usage) "[lindex [info level 0] 0] "
    append err(usage) "-name packageName -version packageVersion"
    append err(usage) "?-load {filename ?{procs}?}? ... "
    append err(usage) "?-source {filename ?{procs}?}? ..."

    set err(wrongNumArgs) "wrong # args: should be \"$err(usage)\""
    set err(valueMissing) "value for \"%s\" missing: should be \"$err(usage)\""
    set err(unknownOpt)   "unknown option \"%s\": should be \"$err(usage)\""
    set err(noLoadOrSource) "at least one of -load and -source must be given"

    # process arguments
    set len [llength $args]
    if { $len < 6 } {
    error $err(wrongNumArgs)
    }
    
    # Initialize parameters
    set opts(-name)     {}
    set opts(-version)      {}
    set opts(-source)       {}
    set opts(-load)     {}

    # process parameters
    for {set i 0} {$i < $len} {incr i} {
    set flag [lindex $args $i]
    incr i
    switch -glob -- $flag {
        "-name"     -
        "-version"      {
        if { $i >= $len } {
            error [format $err(valueMissing) $flag]
        }
        set opts($flag) [lindex $args $i]
        }
        "-source"       -
        "-load"     {
        if { $i >= $len } {
            error [format $err(valueMissing) $flag]
        }
        lappend opts($flag) [lindex $args $i]
        }
        default {
        error [format $err(unknownOpt) [lindex $args $i]]
        }
    }
    }

    # Validate the parameters
    if { [llength $opts(-name)] == 0 } {
    error [format $err(valueMissing) "-name"]
    }
    if { [llength $opts(-version)] == 0 } {
    error [format $err(valueMissing) "-version"]
    }
    
    if { [llength $opts(-source)] == 0 && [llength $opts(-load)] == 0 } {
    error $err(noLoadOrSource)
    }

    # OK, now everything is good.  Generate the package ifneeded statment.
    set cmdline "package ifneeded $opts(-name) $opts(-version) "
    
    set cmdList {}
    set lazyFileList {}

    # Handle -load and -source specs
    foreach key {load source} {
    foreach filespec $opts(-$key) {
        foreach {filename proclist} {{} {}} {
        break
        }
        foreach {filename proclist} $filespec {
        break
        }
        
        if { [llength $proclist] == 0 } {
        set cmd "\[list $key \[file join \$dir [list $filename]\]\]"
        lappend cmdList $cmd
        } else {
        lappend lazyFileList [list $filename $key $proclist]
        }
    }
    }

    if { [llength $lazyFileList] > 0 } {
    lappend cmdList "\[list tclPkgSetup \$dir $opts(-name)\
        $opts(-version) [list $lazyFileList]\]"
    }
    append cmdline [join $cmdList "\\n"]
    return $cmdline
}

