blob: 0be7756423844cf5f531310a581c9762f116ddd0 [file] [log] [blame]
#!/bin/sh
## Begin dltk.tcl \
exec tclsh "$0" ${1+"$@"}
# We are not using namespace for this package so that it should
# work with tcl versions older than 8.0 (not tested yet).
# This script relies on few proc renames, so instruct the tclchecker
# to ignore proc renames.
#checker -scope global exclude warnRedefine
proc hasCommand cmdName {
return [expr [llength [info commands $cmdName]] > 0];
}
## Begin renaming of core tcl cmds
rename package package-org
proc package {subcmd args} {
global pkg_stack pkg_reqs_tmp path_tmp
switch -exact -- $subcmd {
"require" {
set exact 0
set name [lindex $args 0]
set vers [lrange $args 1 end]
if {[string equal $name "-exact"]} {
set exact 1
set name [lindex $args 1]
set vers [lrange $args 2 end]
}
# Track package require dependencies but
# skip if pkg name is null
set pname [lindex $pkg_stack 0]
if {![string equal $pname ""]} {
lappend pkg_reqs_tmp($pname) [list $name $vers $exact]
}
set pkg_stack [linsert $pkg_stack 0 $name]
set retCode [catch {uplevel 1 "::package-org $subcmd $args"} vers]
set pkg_stack [lrange $pkg_stack 1 end]
if {$retCode} {
# Unsuccessful pkg load
add-pkg-srcs-info $name ""
} else {
# Successful pkg load
add-pkg-srcs-info $name $vers
}
return -code $retCode $vers
}
"ifneeded" {
set name [lindex $args 0]
set vers [lindex $args 1]
#set body [lindex $args 2]
upvar 1 use_path lcl_use_path
upvar 1 dir lcl_dir
set path $path_tmp
if {[info exists lcl_use_path]} {
set path [lindex $lcl_use_path end]
set path [get-canonical-path $path]
set path_tmp $path
}
set d ""
if {[info exists lcl_dir]} {
set d $lcl_dir
}
add-pkg-info $name $vers $path $d
return [uplevel 1 "::package-org $subcmd $args"]
}
default {}
}
return [uplevel 1 "::package-org $subcmd $args"]
} ;# End of proc package
rename source source-org
proc source {args} {
global pkg_srcs_tmp pkg_stack
global file_srcs_tmp
global auto_index
log::debug "source $args"
# Ignore "-rsrc*" option when its optional filename
# which should be present as 3rd argument is not specified
if {[llength $args] != "2"} {
# If the file is already sourced, don't source it again
set fname [get-canonical-path [lindex $args end]]
if {[info exists file_srcs_tmp($fname)]} {
return
}
set file_srcs_tmp($fname) 1
log::debug "source file: $fname"
# Skip pkgIndex.tcl or tclIndex file or if pkg name is null
set pname [lindex $pkg_stack 0]
set ftail [file tail $fname]
if {![string equal $pname ""] && [file exists $fname] &&
![string equal $ftail "pkgIndex.tcl"] &&
![string equal $ftail "tclIndex"]} {
lappend pkg_srcs_tmp($pname) $fname
} ;# End of if stmt
} ;# End of if stmt
return [uplevel 1 "::source-org $args"]
} ;# End of proc source
if [hasCommand load] {
rename load load-org
}
proc load {args} {
global pkg_load_tmp pkg_stack
global file_load_tmp
log::debug "load $args"
# If the file is already loaded, don't load it again
set fname [get-canonical-path [lindex $args 0]]
if {[info exists file_load_tmp($fname)]} {
return
}
set file_load_tmp($fname) 1
log::debug "load file: $fname"
# Skip if pkg name is null
set pname [lindex $pkg_stack 0]
if {![string equal $pname ""] && [file exists $fname]} {
lappend pkg_load_tmp($pname) $fname
}
return [uplevel 1 "::load-org $args"]
} ;# End of proc load
# Some pkgs rename exit proc when it is loaded or they may explicitly
# terminate this script by explicitly calling exit. We prevent this
# by renaming exit cmd as per below.
rename exit exit-org
proc exit args {}
## End renaming of core tcl cmds
## Log routines: can be easily replaced with tcllib's logger pkg
##
namespace eval ::log {
variable level warn
}
proc ::log::noop {args} {}
proc ::log::print {lvl msg} {
# Print log level and msg
puts stdout "$lvl - $msg"
} ;# End of proc ::log::print
proc ::log::levellist {} {
return [list error warn notice info debug]
} ;# End of proc ::log::levellist
proc ::log::getlevel {} {
variable level
return $level
} ;# End of proc ::log::getlevel
proc ::log::setlevel {lvl} {
variable level
# Validate specified level
if {[lsearch [log::levellist] $lvl] == "-1"} {
log::warn "Invalid log level: $lvl ... ignoring"
return
} ;# End of switch stmt
set log_alias ::log::print
foreach elm [log::levellist] {
interp alias {} ::log::$elm {} $log_alias [string toupper $elm]
if {[string equal $lvl $elm]} {
set log_alias ::log::noop
} ;# End of if stmt
} ;# End of foreach loop
return $lvl
} ;# End of proc ::log::setlevel
## Local procs below
##
proc get-canonical-path {path} {
global tcl_version
set retVal $path
# [file normalize] cmd was introduced in tcl8.4 version
if {$tcl_version >= 8.4} {
set retVal [file normalize $path]
} elseif {[string equal [file pathtype $path] "relative"]} {
set retVal [file join [pwd] $path]
} ;# End of if stmt
return $retVal
} ;# End of proc get-canonical-path
proc trace-auto_index {var idx op} {
global auto_index
# Return immediately if it is not a write operation on
# auto_index array element
if {![string equal $var "auto_index"] || ![string equal $op "w"]} {
return
} ;# End of if stmt
# If the auto_index element is updated by tclIndex file
# of a package, the corresponding file should be sourced
if {[info exists auto_index($idx)]} {
catch {uplevel #0 $auto_index($idx)} err
} ;# End of if stmt
return
} ;# End of proc trace-auto_index
proc process-pkg-info {find_level find_pkgs} {
global auto_index
# Do not go any further it find level is negative
if {$find_level < 0} {
return
} ;# End of if stmt
# Set a variable trace on auto_index so that it also
# finds auto-loaded tcl soruce files for a pkg
trace variable ::auto_index w trace-auto_index
# Also source files in existing auto_index array to
# avoid mixup of existing entries with new pkg
foreach {elm cmd} [array get auto_index] {
catch {uplevel #0 $cmd} err
} ;# End of foreach {elm cmd} loop
# load all specified pkgs
set pkgs $find_pkgs
if {[string equal $pkgs ""]} {
set pkgs [package names]
} ;# End of if stmt
foreach name $pkgs {
log::notice "%DLTK-PROCESS-PKG% $name"
if {![get-pkg-info exists $name]} {
# This package does not exist, so skip
continue
} ;# End of if stmt
# Load the package
if {$find_level && ![catch {package require $name} vers]} {
# If the package was loaded successfully, do nothing
# since its info will be added by package require
} else {
# If the package could not be loaded, register all
# known versions of the pkg
foreach nvers [get-pkg-info vers $name] {
add-pkg-srcs-info $name $nvers
} ;# End of foreach loop
} ;# End of if catch stmt
} ;# End of foreach name loop
return
} ;# End of proc process-pkg-info
proc add-pkg-srcs-info {name vers} {
global pkg_path_names
global pkg_dir_names
global pkg_reqs pkg_srcs pkg_load
global pkg_reqs_tmp pkg_srcs_tmp pkg_load_tmp
global pkg_names_tmp
set type [get-pkg-info type $name $vers]
if {[string equal $type ""]} {
# If the package is not known to the interpreter,
# its shouldn't be recorded, so return immediately.
return
} ;# End of if stmt
set elm [list $name $vers]
set path [get-pkg-info path $name $vers]
set dir [get-pkg-info dir $name $vers]
# Skip if pkg name has been previously recorded
if {[info exists pkg_names_tmp($elm)]} {
return
} ;# End of if stmt
set pkg_names_tmp($elm) $vers
# Store the files sourced and its package require
# dependencies in a global variable.
set pkg_srcs($elm) [list]
set pkg_load($elm) [list]
set pkg_reqs($elm) [list]
if {[info exists pkg_srcs_tmp($name)]} {
set pkg_srcs($elm) $pkg_srcs_tmp($name)
unset pkg_srcs_tmp($name)
} ;# End of if stmt
if {[info exists pkg_load_tmp($name)]} {
set pkg_load($elm) $pkg_load_tmp($name)
unset pkg_load_tmp($name)
} ;# End of if stmt
if {[info exists pkg_reqs_tmp($name)]} {
set pkg_reqs($elm) $pkg_reqs_tmp($name)
unset pkg_reqs_tmp($name)
} ;# End of if stmt
lappend pkg_path_names($path) $elm
lappend pkg_dir_names($dir) $elm
log::debug "PKG-PATH $elm $path"
return
} ;# End of proc add-pkg-srcs-info
proc add-pkg-info {name vers path {dir ""}} {
global pkg_names_vers
global pkg_names_path
global pkg_names_dir
# Don't go any further if:
# 1. the specified pkg vers has been recorded previously, or
# 2. the vers is null and any pkg vers has been previously recorded
set elm [list $name $vers]
if {[info exists pkg_names_path($elm)] ||
([string equal $vers ""] && [info exists pkg_names_vers($name)])} {
return
} ;# End of if stmt
if {![info exists pkg_names_vers($name)]} {
set pkg_names_vers($name) [list]
} ;# End of if stmt
if {![string equal $vers ""]} {
lappend pkg_names_vers($name) $vers
} ;# End of if stmt
# Save path and dir for pkg version.
set pkg_names_path($elm) $path
set pkg_names_dir($elm) $dir
log::debug "PATH-PKG $name $vers $path $dir"
return
} ;# End of proc add-pkg-info
proc get-pkg-info {what name {vers ""}} {
global pkg_names_vers
global pkg_names_path
global pkg_names_dir
# Parse $what
set retVal {}
set elm [list $name $vers]
switch -exact -- $what {
"path" {
if {[info exists pkg_names_path($elm)]} {
set retVal $pkg_names_path($elm)
} ;# End of if stmt
}
"dir" {
if {[info exists pkg_names_dir($elm)]} {
set retVal $pkg_names_dir($elm)
} ;# End of if stmt
}
"type" {
if {[info exists pkg_names_path($elm)]} {
set retVal [get-path-info type $pkg_names_path($elm)]
} ;# End of if stmt
}
"vers" {
# Returns list of known pkg versions
if {[info exists pkg_names_vers($name)]} {
set retVal $pkg_names_vers($name)
} ;# End of if stmt
}
"exists" {
# Returns 1 if the pkg version is known
# otherwise it returns 0
set retVal 0
if {[info exists pkg_names_vers($name)]} {
if {[string equal $vers ""] ||
[lsearch $pkg_names_vers($name) $vers] != "-1"} {
set retVal 1
} ;# End of if stmt
} ;# End of if stmt
}
default {
# should never reach here
return
}
} ;# End of switch stmt
return $retVal
} ;# End of proc get-pkg-info
proc get-path-info {what {path ""}} {
global pkg_path_names
global pkg_path_type
# Parse $what
set retVal {}
switch -exact -- $what {
"list" {
set retVal [array names pkg_path_names]
}
"type" {
# Returns type for the given path
if {[info exists pkg_path_type($path)]} {
set retVal $pkg_path_type($path)
} ;# End of if stmt
}
"pkgs" {
# Returns list of known pkgs for the given path
if {[info exists pkg_path_names($path)]} {
set retVal $pkg_path_names($path)
} ;# End of if stmt
}
default {
# should never reach here
return
}
} ;# End of switch stmt
return $retVal
} ;# End of proc get-path-info
proc process-pkg-paths {find_paths builtin_pkgs} {
global env auto_path
global pkg_path_type
global pkg_path_names
# Initialize builtin pkgs
set pkg_path_names() [list]
set pkg_path_type() builtin
foreach name $builtin_pkgs {
if {![catch {package provide $name} vers]} {
add-pkg-info $name $vers ""
} ;# End of if stmt
} ;# End of foreach pkt
# First save paths found in auto_path variable
# and set the flag to 0
set paths $find_paths
if {[string equal $paths ""]} {
set paths $auto_path
} ;# End of if stmt
foreach path $paths {
set path [get-canonical-path $path]
set pkg_path_names($path) [list]
set pkg_path_type($path) "auto_path"
} ;# End of foreach elm loop
# Next look for paths found in TCLLIBPATH variable
# and set the flag to 1
if {[info exists env(TCLLIBPATH)]} {
foreach path $::env(TCLLIBPATH) {
set path [get-canonical-path $path]
set pkg_path_names($path) [list]
set pkg_path_type($path) "tcllibpath"
} ;# End of foreach elm loop
} ;# End of if TCLLIBPATH exists sttm
return
} ;# End of proc process-pkg-paths
proc print-pkg-info {output} {
global pkg_srcs pkg_load pkg_reqs
set tabspc " "
set path_count 0
set pkgs_count 0
set reqs_count 0
set srcs_count 0
set load_count 0
set dltk_size 0
set path_data {}
foreach path [lsort [get-path-info list]] {
# Indentation
set indent "$tabspc"
incr path_count
set path_size 0
set pkg_data {}
log::info "path: $path"
foreach elm [lsort [get-path-info pkgs $path]] {
# Double indentation
set indent "$tabspc$tabspc"
incr pkgs_count
set pkg_size 0
set data {}
set name [lindex $elm 0]
set vers [lindex $elm 1]
set srcs $pkg_srcs($elm)
set load $pkg_load($elm)
set reqs $pkg_reqs($elm)
log::info " pkg: $name $vers"
foreach r $reqs {
incr reqs_count
set rname [lindex $r 0]
set rvers [lindex $r 1]
set rexact [lindex $r 2]
set rattr name=\"$rname\"
if {![string equal $rvers ""]} {
append rattr " " version=\"$rvers\"
} ;# End of if stmt
if {$rexact} {
append rattr " " exact=\"1\"
} ;# End of if stmt
log::info " require: $rname $rvers"
append data $indent $tabspc "<require $rattr/>" \n
} ;# End of foreach rname loop
foreach sname $srcs {
incr srcs_count
set sattr name=\"$sname\"
if {[file exists $sname]} {
set ssize [file size $sname]
if {$ssize} {
append sattr " " size=\"$ssize\"
incr pkg_size $ssize
} ;# End of if stmt
} else {
log::warn "Source file not found: $sname"
} ;# End of ifelse stmt
log::info " source: $sname"
append data $indent $tabspc "<source $sattr/>" \n
} ;# End of foreach sname loop
foreach lname $load {
incr load_count
set lattr name=\"$lname\"
if {[file exists $lname]} {
set lsize [file size $lname]
if {$lsize} {
append lattr " " size=\"$lsize\"
incr pkg_size $lsize
} ;# End of if stmt
} else {
log::warn "Load file not found: $lname"
} ;# End of ifelse stmt
log::info " load: $lname"
append data $indent $tabspc "<load $lattr/>" \n
} ;# End of foreach lname loop
# Create the pkg xml structure
set pattr name=\"$name\"
if {![string equal $vers ""]} {
append pattr " " version=\"$vers\"
} ;# End of if stmt
# Disable printing of directory name for the pkg for now
# set pkg_dir [get-pkg-info dir $name $vers]
# if {![string equal $pkg_dir ""]} {
# append pattr " " dir=\"$pkg_dir\"
# } ;# End of if stmt
if {$pkg_size} {
append pattr " " size=\"$pkg_size\"
} ;# End of if stmt
append pkg_data $indent "<package $pattr>" \n
append pkg_data $data
append pkg_data $indent "</package>" \n
incr path_size $pkg_size
} ;# End of foreach loop
# Create the path xml structure
set indent "$tabspc"
set type [get-path-info type $path]
set pattr "name=\"$path\" type=\"$type\""
if {$path_size} {
append pattr " " size=\"$path_size\"
}
append path_data $indent "<path $pattr>" \n
append path_data $pkg_data
append path_data $indent "</path>" \n
incr dltk_size $path_size
} ;# End of foreach loop
# Create the dltk xml structure
set dltk_data {}
append dltk_data "<DLTK size=\"$dltk_size\">" \n
append dltk_data $path_data
append dltk_data </DLTK>
# Print dltk data to stdout or output file
set fname [get-canonical-path $output]
if {[string equal $fname ""]} {
puts stdout $dltk_data
} else {
# Ensure that parent dir exists
set fdir [file dirname $fname]
file mkdir $fdir
set fh [open $fname w]
puts $fh $dltk_data
close $fh
} ;# End of if else stmt
log::notice "Output file: $fname"
log::notice "Number of pkg paths found: $path_count"
log::notice "Number of pkgs found: $pkgs_count"
log::notice "Number of files sourced: $srcs_count"
log::notice "Number of libs loaded: $load_count"
log::notice "Number of pkg dependencies: $reqs_count"
return
} ;# End of proc print-pkg-info
proc help {{usermsg ""}} {
set msg $usermsg
append msg {
USAGE: dltk.tcl [subcmd] [options] ?-output <file>?
This script scans all the directories relevant to the tcl
interpreter and returns an xml structure consisting of library
paths, various packages available for loading alongwith its
corresponding version number, package dependencies and files
sourced or loaded by the package. The -output option can be
specified to save the xml structure to a file, otherwise it
print it to stdout.
> dltk.tcl get-paths ?-paths "<p1> .."? ?-output <file>? \
?-loglevel error|warn|notice|info|debug?
Print all paths known to tcl interpeter including $auto_path
and TCLLIBPATH values. Optionally restrict the paths printed
using -paths option.
> dltk.tcl get-pkgs ?-paths "<p1> .."? ?-pkgs "<pkg1> .."? ?-output <file>? \
?-fpkgs <file-with-list>? \
?-loglevel error|warn|notice|info|debug?
Print pkg paths and associated package names. Specify -paths and/or
-pkgs option to display limited number of paths and pkgs.
> dltk.tcl get-srcs ?-paths "<p1> .."? ?-pkgs "<pkg1> .."? ?-output <file>? \
?-fpkgs <file-with-list>? \
?-loglevel error|warn|notice|info|debug?
Print pkg paths, pkg names/versions and corresponding library
source files. Limit output to specified packages or paths using
-pkgs or -paths option.
> dltk.tcl
> dltk.tcl help
Print this help msg
}
puts stderr $msg
return
} ;# End of proc help
proc main {argv} {
global auto_path
global pkg_stack path_tmp
set pkg_stack {} ;# initialize to null
set path_tmp {} ;# initialize to null
set subcmd [lindex $argv 0]
# We could use tcl's time cmd below but we don't need
# microsecond resolution
set start_time [clock seconds]
# Initialize log level
::log::setlevel notice
set find_level 1
switch -exact -- $subcmd {
"get-paths" {
set find_level -1
}
"get-pkgs" {
set find_level 0
}
"get-srcs" {
set find_level 1
}
"help" -
default {
# Print help msg and quit
help
return
}
} ;# End of switch stmt
# Parse the options
set argc [llength $argv]
set idx 1
set paths [list]
set pkgs [list]
set output [list]
while {$idx < $argc} {
set opt [lindex $argv $idx]
incr idx
switch -exact -- $opt {
"-path" -
"-paths" {
set paths [lindex $argv $idx]
}
"-pkg" -
"-pkgs" {
set pkgs [lindex $argv $idx]
}
"-fpkgs" {
set fname [lindex $argv $idx]
set fp [open "$fname" r]
set pkgs [read $fp]
puts "Loaded: $pkgs"
close $fp
}
"-output" {
set output [get-canonical-path [lindex $argv $idx]]
}
"-loglevel" {
::log::setlevel [lindex $argv $idx]
}
default {
# Invalid option, print error
help "Invalid option: $opt\n\n"
return
}
} ;# End of switch stmt
incr idx
} ;# End of while loop
# First try to get list of builtin pkgs known to interpreter
# Then try to load an unknown pkg, so that it discovers all packages
set builtin_pkgs [package names]
catch {::package-org require unknown-random-[clock seconds]}
log::notice "%DLTK-NUM-PATHS% [llength $auto_path]"
log::notice "%DLTK-NUM-PKGS% [llength [package names]]"
# Process pkg paths in auto_path and TCLLIBPATH
process-pkg-paths $paths $builtin_pkgs
# Process pkg ifneeded bodies by invoking [pkg req]
process-pkg-info $find_level $pkgs
# Print pkg root folders
print-pkg-info $output
set stop_time [clock seconds]
log::notice "Run time = [expr {$stop_time-$start_time}] secs"
return
} ;# End of proc main
main $argv
puts stdout "DLTK-TCL-HELPER-9E7A168E-5EEF-4a46-A86D-0C82E90686E4-END-OF-STREAM"
# Exit needs to be called explicitly because some package may endup
# invoking Tk pkg which in turn may create a GUI window and wait forever.
::exit-org
## End dltk.tcl