#!/bin/sh
# -*- tcl -*- \
exec tclsh "$0" ${1+"$@"}

# --------------------------------------------------------------
# Perform various checks and operations on the distribution.
# SAK = Swiss Army Knife.

set distribution   [file dirname [info script]]
set auto_path      [linsert $auto_path 0 [file join $distribution modules]]

set critcldefault {}
set critclnotes   {}
set dist_excluded {}

proc package_name    {text} {global package_name    ; set package_name    $text}
proc package_version {text} {global package_version ; set package_version $text}
proc dist_exclude    {path} {global dist_excluded   ; lappend dist_excluded $path}
proc critcl {name files} {
    global critclmodules
    set    critclmodules($name) $files
    return
}
proc critcl_main {name files} {
    global critcldefault
    set critcldefault $name
    critcl $name $files
    return
}
proc critcl_notes {text} {
    global critclnotes
    set critclnotes [string map {{\n    } \n} $text]
    return
}

source [file join $distribution support installation version.tcl] ; # Get version information.

set package_nv ${package_name}-${package_version}

catch {eval file delete -force [glob [file rootname [info script]].tmp.*]}

# --------------------------------------------------------------
# SAK internal debugging support.

# Configuration, change as needed
set  debug 0

if {$debug} {
    proc sakdebug {script} {uplevel 1 $script ; return}
} else {
    proc sakdebug {args} {}
}

# --------------------------------------------------------------
# Internal helper to load packages straight out of the local directory
# tree. Not something from an installation, possibly incompatible.

proc getpackage {package tclmodule} {
    global distribution
    if {[catch {package present $package}]} {
	set src [file join \
		$distribution modules \
		$tclmodule]
	if {[file exists $src]} {
	    uplevel #0 [list source $src]
	} else {
	    # Fallback
	    package require $package
	}
    }
}

# --------------------------------------------------------------

proc tclfiles {} {
    global distribution
    getpackage fileutil fileutil/fileutil.tcl
    set fl [fileutil::findByPattern $distribution -glob *.tcl]
    # Remove files under SCCS. They are repository, not sources to check.
    set tmp {}
    foreach f $fl {
	if {[string match *SCCS* $f]} continue
	lappend tmp $f
    }
    proc tclfiles {} [list return $tmp]
    return $tmp
}

proc modtclfiles {modules} {
    global mfiles guide
    load_modinfo
    set mfiles [list]
    foreach m $modules {
	eval $guide($m,pkg) $m __dummy__
    }
    return $mfiles
}

proc modules {} {
    global distribution
    set fl [list]
    foreach f [glob -nocomplain [file join $distribution modules *]] {
	if {![file isdirectory $f]} {continue}
	if {[string match CVS [file tail $f]]} {continue}

	if {![file exists [file join $f pkgIndex.tcl]]} {continue}

	lappend fl [file tail $f]
    }
    set fl [lsort $fl]
    proc modules {} [list return $fl]
    return $fl
}

proc modules_mod {m} {
    return [expr {[lsearch -exact [modules] $m] >= 0}]
}

proc dealias {modules} {
    set _ {}
    foreach m $modules {
	if {[file exists $m]} {
	    set m [file tail $m]
	}
	lappend _ $m
    }
    return $_
}

proc load_modinfo {} {
    global distribution modules guide
    source [file join $distribution support installation modules.tcl] ; # Get list of installed modules.
    source [file join $distribution support installation actions.tcl] ; # Get installer support code.
    proc load_modinfo {} {}
    return
}

proc imodules {} {global modules ; load_modinfo ; return $modules}

proc imodules_mod {m} {
    global modules
    load_modinfo
    return [expr {[lsearch -exact $modules $m] > 0}]
}

# Result: dict (package name --> list of package versions).

proc loadpkglist {fname} {
    set f [open $fname r]
    foreach line [split [read $f] \n] {
	set line [string trim $line]
	if {[string match @* $line]} continue
	if {$line == {}} continue
	foreach {n v} $line break
	lappend p($n) $v
	set p($n) [lsort -uniq -dict $p($n)]
    }
    close $f
    return [array get p]
}

# Result: dict (package name => list of (list of package versions, module)).

proc ipackages {args} {
    # Determine indexed packages (ifneeded, pkgIndex.tcl)

    global distribution

    if {[llength $args] == 0} {set args [modules]}

    array set p {}
    foreach m $args {
	set f [open [file join $distribution modules $m pkgIndex.tcl] r]
	foreach line [split [read $f] \n] {
	    if { [regexp {#}        $line]} {continue}
	    if {![regexp {ifneeded} $line]} {continue}
	    regsub {^.*ifneeded } $line {} line
	    regsub {([0-9]) \[.*$}  $line {\1} line

	    foreach {n v} $line break
	    set v [string trimright $v \\]

	    if {![info exists p($n)]} {
		set p($n) [list $v $m]
	    } else {
		# We have multiple versions of the same package. We
		# remember all versions.

		foreach {vlist mx} $p($n) break
		lappend vlist $v
		set p($n) [list [lsort -uniq -dict $vlist] $mx]
	    }
	}
	close $f
    }
    return [array get p]
}


# Result: dict (package name --> list of package versions).

proc ppackages {args} {
    # Determine provided packages (provide, *.tcl - pkgIndex.tcl)
    # We cache results for a bit of speed, some stuff uses this
    # multiple times for the same arguments.

puts /PP/$args//
    
    global ppcache
    if {[info exists ppcache($args)]} {
puts /PP/CACHED/res=(($ppcache($args)))
	return $ppcache($args)
    }

    global    p pf currentfile
    array set p {}

    if {[llength $args] == 0} {
	set files [tclfiles]
    } else {
	set files [modtclfiles $args]
    }

    getpackage fileutil fileutil/fileutil.tcl
    set capout [fileutil::tempfile] ; set capcout [open $capout w]
    set caperr [fileutil::tempfile] ; set capcerr [open $caperr w]

    array set notprovided {}

    foreach f $files {
	# We ignore package indices and all files not in a module.

	if {[string equal pkgIndex.tcl [file tail $f]]} {continue}
	if {![regexp modules $f]}                       {continue}

	# We use two methods to extract the version information from a
	# module and its packages. First we do a static scan for
	# appropriate statements. If that did not work out we try to
	# execute the script in a modified interpreter which lets us
	# pick up dynamically generated version data (like stored in
	# variables). If the second method fails as well we give up.

	# Method I. Static scan.

	# We do heuristic scanning of the code to locate suitable
	# package provide statements.

	set fh [open $f r]

	set currentfile [eval file join [lrange [file split $f] end-1 end]]

	set ok -1
	foreach line [split [read $fh] \n] {
	    if {[regexp "\#\\s*@sak\\s+notprovided\\s+(\[^\\s\]+)" $line -> nppname]} {
		sakdebug {puts stderr "PRAGMA notprovided = $nppname"}
		set notprovided($nppname) .
	    }

	    regsub "\#.*$" $line {} line
	    if {![regexp {provide} $line]} {continue}
	    if {![regexp {package} $line]} {continue}

	    # Now a stronger check for the actual command
	    if {![regexp {package[ 	][ 	]*provide} $line]} {continue}

	    set xline $line
	    regsub {^.*provide } $line {} line
	    regsub {\].*$}       $line {\1} line

	    sakdebug {puts stderr __$f\ _________$line}

            #foreach {n v} $line break
            if {[catch {
	      set n [lindex $line 0]
              set v [lindex $line 1]
            } err]} {
              sakdebug {puts stderr "Line: $line of file $f threw $err"}
              continue
            }

	    # HACK ...
	    # Module 'page', package 'page::gen::peg::cpkg'.
	    # Has a provide statement inside a template codeblock.
	    # Name is placeholder @@. Ignore this specific name.
	    # Better would be to use general static Tcl parsing
	    # to find that the string is a variable value.

	    if {[string equal $n @@]} continue

	    if {[regexp {^[0-9]+(\.[0-9]+)*$} $v]} {
		lappend p($n) $v
		set p($n) [lsort -uniq -dict $p($n)]
		set pf($n,$v) $currentfile
		set ok 1

		# We continue the scan. The file may provide several
		# versions of the same package, or multiple packages.
		continue
	    }

	    # 'package provide foo' are tests. Ignore.
	    if {$v == ""} continue

	    # We do not set the state to bad if we found ok provide
	    # statements before, only if nothing was found before.
	    if {$ok < 0} {
		set ok 0

		# No good version found on the current line. We scan
		# further through the file and hope for more luck.

		sakdebug {puts stderr @_$f\ _________$xline\t<$n>\t($v)}
	    }
	}
	close $fh

	# Method II. Restricted Execution.
	# We now try to run the code through a safe interpreter
	# and hope for better luck regarding package information.

	if {$ok == -1} {sakdebug {puts stderr $f\ IGNORE}}
	if {$ok == 0} {
	    sakdebug {puts -nonewline stderr $f\ EVAL}

	    # Source the code into a sub-interpreter. The sub
	    # interpreter overloads 'package provide' so that the
	    # information about new packages goes directly to us. We
	    # also make sure that the sub interpreter doesn't kill us,
	    # and will not get stuck early by trying to load other
	    # files, or when creating procedures in namespaces which
	    # do not exist due to us disabling most of the package
	    # management.

	    set fh [open $f r]

	    set ip [interp create]

	    # Kill control structures. Namespace is required, but we
	    # skip everything related to loading of packages,
	    # i.e. 'command import'.

	    $ip eval {
		rename ::if        ::_if_
		rename ::namespace ::_namespace_

		proc ::if {args} {}
		proc ::namespace {cmd args} {
		    #puts stderr "_nscmd_ $cmd"
		    ::_if_ {[string equal $cmd import]} return
		    #puts stderr "_nsdo_ $cmd $args"
		    return [uplevel 1 [linsert $args 0 ::_namespace_ $cmd]]
		}
	    }

	    # Kill more package stuff, and ensure that unknown
	    # commands are neither loaded nor abort execution. We also
	    # stop anything trying to kill the application at large.

	    interp alias $ip package {} xPackage
	    interp alias $ip source  {} xNULL
	    interp alias $ip unknown {} xNULL
	    interp alias $ip proc    {} xNULL
	    interp alias $ip exit    {} xNULL

	    # From here on no redefinitions anymore, proc == xNULL !!

	    $ip eval {close stdout} ; interp share {} $capcout $ip
	    $ip eval {close stderr} ; interp share {} $capcerr $ip

	    if {[catch {$ip eval [read $fh]} msg]} {
		sakdebug {puts stderr "ERROR in $currentfile:\n$::errorInfo\n"}
	    }

	    sakdebug {puts stderr ""}

	    close $fh
	    interp delete $ip
	}
    }

    close $capcout ; file delete $capout
    close $capcerr ; file delete $caperr

    # Process the accumulated pragma information, remove all the
    # packages which exist but not really, in terms of indexing.

    foreach n [array names notprovided] {
	catch { unset p($n) }
	array unset pf $n,*
    }

    set   pp [array get p]
    unset p

    set ppcache($args) $pp

puts /PP/res=(($pp))
    return $pp 
}

proc xNULL    {args} {}
proc xPackage {cmd args} {
    if {[string equal $cmd provide]} {
	global p pf currentfile
	foreach {n v} $args break

	# No version specified, this is an inquiry, we ignore these.
	if {$v == {}} {return}

	sakdebug {puts stderr \tOK\ $n\ =\ $v}

	lappend p($n) $v
	set p($n) [lsort -uniq -dict $p($n)]
	set pf($n,$v) $currentfile
    }
    return
}

proc sep {} {puts ~~~~~~~~~~~~~~~~~~~~~~~~}

proc gd-cleanup {} {
    global package_nv

    puts {Cleaning up...}

    set        fl [glob -nocomplain ${package_nv}*]
    foreach f $fl {
	puts "    Deleting $f ..."
	catch {file delete -force $f}
    }
    return
}

proc gd-gen-archives {} {
    global package_name package_nv

    puts {Generating archives...}

    set tar [auto_execok tar]
    if {$tar != {}} {
        puts "    Gzipped tarball (${package_nv}.tar.gz)..."
        catch {
            exec $tar cf - ${package_nv} | gzip --best > ${package_nv}.tar.gz
        }

        set bzip [auto_execok bzip2]
        if {$bzip != {}} {
            puts "    Bzipped tarball (${package_nv}.tar.bz2)..."
            exec tar cf - ${package_nv} | bzip2 > ${package_nv}.tar.bz2
        }

	set xz [auto_execok xz]
        if {$xz != {}} {
            puts "    Xzipped tarball (${package_nv}.tar.xz)..."
            exec tar cf - ${package_nv} | xz > ${package_nv}.tar.xz
        }
    }

    set zip [auto_execok zip]
    if {$zip != {}} {
        puts "    Zip archive     (${package_nv}.zip)..."
        catch {
            exec $zip -r ${package_nv}.zip ${package_nv}
        }
    }

    set sdx [auto_execok sdx]
    if {$sdx != {}} {
	file copy -force [file join ${package_nv} support installation main.tcl] \
		[file join ${package_nv} main.tcl]
	file rename ${package_nv} ${package_name}.vfs

	puts "    Starkit         (${package_nv}.kit)..."
	exec sdx wrap ${package_name}
	file rename   ${package_name} ${package_nv}.kit

	if {![file exists tclkit]} {
	    puts "    No tclkit present in current working directory, no starpack."
	} else {
	    puts "    Starpack        (${package_nv}.exe)..."
	    exec sdx wrap ${package_name} -runtime tclkit
	    file rename   ${package_name} ${package_nv}.exe
	}

	file rename ${package_name}.vfs ${package_nv}
    }

    puts {    Keeping directory for other archive types}

    ## Keep the directory for 'sdx' - kit/pack
    return
}

proc xcopyfile {src dest} {
    # dest can be dir or file
    global  mfiles
    lappend mfiles $src
    return
}

proc xcopy {src dest recurse {pattern *}} {
    if {[string equal $pattern *] || !$recurse} {
	foreach file [glob [file join $src $pattern]] {
	    set base [file tail $file]
	    set sub  [file join $dest $base]
	    if {0 == [string compare CVS $base]} {continue}
	    if {[file isdirectory $file]} then {
		if {$recurse} {
		    xcopy $file $sub $recurse $pattern
		}
	    } else {
		xcopyfile $file $sub
	    }
	}
    } else {
	foreach file [glob [file join $src *]] {
	    set base [file tail $file]
	    set sub  [file join $dest $base]
	    if {[string equal CVS $base]} {continue}
	    if {[file isdirectory $file]} then {
		if {$recurse} {
		    xcopy $file $sub $recurse $pattern
		}
	    } else {
		if {![string match $pattern $base]} {continue}
		xcopyfile $file $sub
	    }
	}
    }
}

proc xxcopy {src dest recurse {pattern *}} {
    global package_name

    file mkdir $dest
    foreach file [glob -nocomplain [file join $src $pattern]] {
        set base [file tail $file]
	set sub  [file join $dest $base]

	# Exclude CVS, SCCS, ... automatically, and possibly the temp
	# hierarchy itself too.

	if {0 == [string compare CVS        $base]} {continue}
	if {0 == [string compare SCCS       $base]} {continue}
	if {0 == [string compare BitKeeper  $base]} {continue}
	if {[string match ${package_name}-* $base]} {continue}
	if {[string match *~                $base]} {continue}

        if {[file isdirectory $file]} then {
	    if {$recurse} {
		file mkdir  $sub
		xxcopy $file $sub $recurse $pattern
	    }
        } else {
	    puts -nonewline stdout . ; flush stdout
            file copy -force $file $sub
        }
    }
}

proc gd-assemble {} {
    global package_nv distribution dist_excluded

    puts "Assembling distribution in directory '${package_nv}'"

    xxcopy $distribution ${package_nv} 1

    foreach f $dist_excluded {
	file delete -force [file join $package_nv $f]
    }
    puts ""
    return
}

proc normalize-version {v} {
    # Strip everything after the first non-version character, and any
    # trailing dots left behind by that, to avoid the insertion of bad
    # version numbers into the generated .tap file.

    regsub {[^0-9.].*$} $v {} v
    return [string trimright $v .]
}

proc gd-gen-tap {} {
    getpackage textutil textutil/textutil.tcl
    getpackage fileutil fileutil/fileutil.tcl

    global package_name package_version distribution tcl_platform

    set pname [textutil::cap $package_name]

    set modules   [imodules]
    array set pd  [getpdesc]
    set     lines [list]
    # Header
    lappend lines {format  {TclDevKit Project File}}
    lappend lines {fmtver  2.0}
    lappend lines {fmttool {TclDevKit TclApp PackageDefinition} 2.5}
    lappend lines {}
    lappend lines "##  Saved at : [clock format [clock seconds]]"
    lappend lines "##  By       : $tcl_platform(user)"
    lappend lines {##}
    lappend lines "##  Generated by \"[file tail [info script]] tap\""
    lappend lines "##  of $package_name $package_version"
    lappend lines {}
    lappend lines {########}
    lappend lines {#####}
    lappend lines {###}
    lappend lines {##}
    lappend lines {#}

    # Bundle definition
    lappend lines {}
    lappend lines {# ###############}
    lappend lines {# Complete bundle}
    lappend lines {}
    lappend lines [list Package [list $package_name [normalize-version $package_version]]]
    lappend lines "Base     @TAP_DIR@"
    lappend lines "Platform *"
    lappend lines "Desc     \{$pname: Bundle of all packages\}"
    lappend lines "Path     pkgIndex.tcl"
    lappend lines "Path     [join $modules "\nPath     "]"

    set  strip [llength [file split $distribution]]
    incr strip 2

    foreach m $modules {
	# File set of module ...

	lappend lines {}
	lappend lines "# #########[::textutil::strRepeat {#} [string length $m]]" ; # {}
	lappend lines "# Module \"$m\""
	set n 0
	foreach {p vlist} [ppackages $m] {
	    foreach v $vlist {
		lappend lines "# \[[format %1d [incr n]]\]    | \"$p\" ($v)"
	    }
	}
	if {$n > 1} {
	    # Multiple packages (*). We create one hidden package to
	    # contain all the files and then have all the true
	    # packages in the module refer to it.
	    #
	    # (*) This can also be one package for which we have
	    # several versions. Or a combination thereof.

	    array set _ {}
	    foreach {p vlist} [ppackages $m] {
		catch {set _([lindex $pd($p) 0]) .}
	    }
	    set desc [string trim [join [array names _] ", "] " \n\t\r,"]
	    if {$desc == ""} {set desc "$pname module"}
	    unset _

	    lappend lines "# -------+"
	    lappend lines {}
	    lappend lines [list Package [list __$m 0.0]]
	    lappend lines "Platform *"
	    lappend lines "Desc     \{$desc\}"
	    lappend lines Hidden
	    lappend lines "Base     @TAP_DIR@/$m"

	    foreach f [lsort -dict [modtclfiles $m]] {
		lappend lines "Path     [fileutil::stripN $f $strip]"
	    }

	    # Packages in the module ...
	    foreach {p vlist} [ppackages $m] {
		# NO DANGER. As we are listing only the packages P for
		# the module any other version of P in a different
		# module is _not_ listed here.

		set desc ""
		catch {set desc [string trim [lindex $pd($p) 1]]}
		if {$desc == ""} {set desc "$pname package"}

		foreach v $vlist {
		    lappend lines {}
		    lappend lines [list Package [list $p [normalize-version $v]]]
		    lappend lines "See   [list __$m]"
		    lappend lines "Platform *"
		    lappend lines "Desc     \{$desc\}"
		}
	    }
	} else {
	    # A single package in the module. And only one version of
	    # it as well. Otherwise we are in the multi-pkg branch.

	    foreach {p vlist} {{} {}} break	   
	    foreach {p vlist} [ppackages $m] break
	    set desc ""
	    catch {set desc [string trim [lindex $pd($p) 1]]}
	    if {$desc == ""} {set desc "$pname package"}

	    set v [lindex $vlist 0]

	    lappend lines "# -------+"
	    lappend lines {}
	    lappend lines [list Package [list $p [normalize-version $v]]]
	    lappend lines "Platform *"
	    lappend lines "Desc     \{$desc\}"
	    lappend lines "Base     @TAP_DIR@/$m"

	    foreach f [lsort -dict [modtclfiles $m]] {
		lappend lines "Path     [fileutil::stripN $f $strip]"
	    }
	}
	lappend lines {}
	lappend lines {#}
	lappend lines "# #########[::textutil::strRepeat {#} [string length $m]]"
    }

    lappend lines {}
    lappend lines {#}
    lappend lines {##}
    lappend lines {###}
    lappend lines {#####}
    lappend lines {########}

    # Write definition
    set    f [open [file join $distribution ${package_name}.tap] w]
    puts  $f [join $lines \n]
    close $f
    return
}

proc getpdesc  {} {
    global argv ; if {![checkmod]} return

    package require sak::doc
    sak::doc::Gen desc l $argv
    
    array set _ {}
    foreach file [glob -nocomplain doc/desc/*.l] {
        set f [open $file r]
	foreach l [split [read $f] \n] {
	    foreach {p sd d} $l break
	    set _($p) [list $sd $d]
	}
        close $f
    }
    file delete -force doc/desc

    return [array get _]
}

proc gd-gen-rpmspec {} {
    global package_version package_name distribution

    set in  [file join $distribution support releases package_rpm.txt]
    set out [file join $distribution ${package_name}.spec]

    write_out $out [string map \
			[list \
			     @PACKAGE_VERSION@ $package_version \
			     @PACKAGE_NAME@    $package_name] \
			[get_input $in]]
    return
}

proc gd-gen-yml {} {
    # YAML is the format used for the FreePAN archive network.
    # http://freepan.org/

    global package_version package_name distribution

    set in  [file join $distribution support releases package_yml.txt]
    set out [file join $distribution ${package_name}.yml]

    write_out $out [string map \
			[list \
			     @PACKAGE_VERSION@ $package_version \
			     @PACKAGE_NAME@    $package_name] \
			[get_input $in]]
    return
}

proc docfiles {} {
    global distribution

    getpackage fileutil fileutil/fileutil.tcl

    set res [list]
    foreach f [fileutil::findByPattern $distribution -glob *.man] {
	# Remove files under SCCS. They are repository, not sources to check.
	if {[string match *SCCS* $f]} continue
	lappend res [file rootname [file tail $f]].n
    }
    proc docfiles {} [list return $res]
    return $res
}

proc gd-tip55 {} {
    global package_version package_name distribution contributors
    contributors

    set in  [file join $distribution support releases package_tip55.txt]
    set out [file join $distribution DESCRIPTION.txt]

    set md [string map \
		[list \
		     @PACKAGE_VERSION@ $package_version \
		     @PACKAGE_NAME@    $package_name] \
		[get_input $in]]

    foreach person [lsort [array names contributors]] {
        set mail $contributors($person)
        regsub {@}  $mail " at " mail
        regsub -all {\.} $mail " dot " mail
        append md "Contributor: $person <$mail>\n"
    }

    write_out $out $md
    return
}

# Fill the global array of contributors to the bundle by processing
# the ChangeLog entries.
#
proc contributors {} {
    global distribution contributors
    if {![info exists contributors] || [array size contributors] == 0} {
        get_contributors [file join $distribution ChangeLog]

        foreach f [glob -nocomplain [file join $distribution modules *]] {
            if {![file isdirectory $f]} {continue}
            if {[string match CVS [file tail $f]]} {continue}
            if {![file exists [file join $f ChangeLog]]} {continue}
            get_contributors [file join $f ChangeLog]
        }
    }
}

proc get_contributors {changelog} {
    global contributors
    set f [open $changelog r]
    while {![eof $f]} {
        gets $f line
        if {[regexp {^[\d-]+\s+(.*?)<(.*?)>} $line r name mail]} {
            set name [string trim $name]
            if {![info exists names($name)]} {
                set contributors($name) $mail
            }
        }
    }
    close $f
}

proc validate_imodules_cmp {imvar dmvar} {
    upvar $imvar im $dmvar dm

    foreach m [lsort [array names im]] {
	if {![info exists dm($m)]} {
	    puts "  Installed, does not exist: $m"
	}
    }
    foreach m [lsort [array names dm]] {
	if {![info exists im($m)]} {
	    puts "  Missing in installer:      $m"
	}
    }
    return
}

proc validate_imodules {} {
    foreach m [imodules] {set im($m) .}
    foreach m [modules]  {set dm($m) .}

    validate_imodules_cmp im dm
    return
}

proc validate_imodules_mod {m} {
    array set im {}
    array set dm {}
    if {[imodules_mod $m]} {set im($m) .}
    if {[modules_mod  $m]} {set dm($m) .}

    validate_imodules_cmp im dm
    return
}
proc validate_versions_cmp {ipvar ppvar} {
    global pf
    getpackage struct::set struct/sets.tcl

    upvar $ipvar ip $ppvar pp
    set maxl 0
    foreach name [array names ip] {if {[string length $name] > $maxl} {set maxl [string length $name]}}
    foreach name [array names pp] {if {[string length $name] > $maxl} {set maxl [string length $name]}}

    foreach p [lsort [array names ip]] {
	if {![info exists pp($p)]} {
	    puts "  Indexed, no provider:           $p"
	}
    }
    foreach p [lsort [array names pp]] {
	if {![info exists ip($p)]} {
	    foreach k [array names pf $p,*] {
		puts "  Provided, not indexed:          [format "%-*s | %s" $maxl $p $pf($k)]"
	    }
	}
    }
    foreach p [lsort [array names ip]] {
	if {![info exists pp($p)]}               continue
	if {[struct::set equal $pp($p) $ip($p)]} continue

	# Compute intersection and set differences.
	foreach {__ pmi imp} [struct::set intersect3 $pp($p) $ip($p)] break

	puts "  Index/provided versions differ: [format "%-*s | %8s | %8s" $maxl $p $imp $pmi]"
    }
}

proc validate_versions {} {
    foreach {p vm}    [ipackages] {set ip($p) [lindex $vm 0]}
    foreach {p vlist} [ppackages] {set pp($p) $vlist}

    validate_versions_cmp ip pp
    return
}

proc validate_versions_mod {m} {
    foreach {p vm}    [ipackages $m] {set ip($p) [lindex $vm 0]}
    foreach {p vlist} [ppackages $m] {set pp($p) $vlist}

    validate_versions_cmp ip pp
    return
}

proc validate_testsuite_mod {m} {
    global distribution
    if {[llength [glob -nocomplain [file join $distribution modules $m *.test]]] == 0} {
	puts "  Without testsuite : $m"
    }
    return
}

proc bench_mod {mlist paths interp flags norm format verbose output coll rep} {
    global distribution env tcl_platform

    getpackage logger logger/logger.tcl
    getpackage bench  bench/bench.tcl

    ::logger::setlevel $verbose

    set pattern tclsh*
    if {$interp != {}} {
	set pattern [file tail $interp]
	set paths [list [file dirname $interp]]
    } elseif {![llength $paths]} {
	# Using the environment PATH is not a good default for
	# SAK. Use the interpreter running SAK as the default.
	if 0 {
	    set paths [split $env(PATH) \
			   [expr {($tcl_platform(platform) == "windows") ? ";" : ":"}]]
	}
	set interp [info nameofexecutable]
	set pattern [file tail $interp]
	set paths [list [file dirname $interp]]
    }

    set interps [bench::versions \
	    [bench::locate $pattern $paths]]

    if {![llength $interps]} {
	puts "No interpreters found"
	return
    }

    if {[llength $flags]} {
	set cmd [linsert $flags 0 bench::run]
    } else {
	set cmd [list bench::run]
    }

    array set DATA {}

    foreach m $mlist {
	set files [glob -nocomplain [file join $distribution modules $m *.bench]]
	if {![llength $files]} {
	    bench::log::warn "No benchmark files found for module \"$m\""
	    continue
	}

	for {set i 0} {$i <= $rep} {incr i} {
	    if {$i} { puts "Repeat $i" }

	    set run $cmd
	    lappend run $interps $files
	    array set tmp [eval $run]

	    # Merge new set of data into the previous run, if any.
	    foreach key [array names tmp] {
		set val $tmp($key)
		if {![info exists DATA($key)]} {
		    set DATA($key) $val
		    continue
		} elseif {[string is double -strict $val]} {
		    # Call user-request collation type
		    set DATA($key) [collate_$coll $DATA($key) $val $i]
		}
	    }
	    unset tmp
	}
    }

    _bench_write $output [array get DATA] $norm $format
    return
}

proc collate_min {cur new runs} {
    # Minimum
    return [expr {$cur > $new ? $new : $cur}]
}
proc collate_avg {cur new runs} {
    # Average
    return [expr {($cur * $runs + $new)/($runs+1)}]
}
proc collate_max {cur new runs} {
    # Maximum
    return [expr {$cur < $new ? $new : $cur}]
}

if 0 {proc bench_all {flags norm format verbose output} {
    bench_mod [modules] $flags $norm $format $verbose $output ? ?
    return
}}

proc _bench_write {output data norm format} {
    if {$norm != {}} {
	getpackage logger logger/logger.tcl
	getpackage bench  bench/bench.tcl

	set data [bench::norm $data $norm]
    }

    set data [bench::out::$format $data]

    if {$output == {}} {
	puts $data
    } else {
	set    output [open $output w]
	puts  $output "# -*- tcl -*- bench/$format"
	puts  $output $data
	close $output
    }
}

proc validate_testsuites {} {
    foreach m [modules] {
	validate_testsuite_mod $m
    }
    return
}

proc validate_pkgIndex_mod {m} {
    global distribution
    if {[llength [glob -nocomplain [file join $distribution modules $m pkgIndex.tcl]]] == 0} {
	puts "  Without package index : $m"
    }
    return
}

proc validate_pkgIndex {} {
    global distribution
    foreach m [modules] {
	validate_pkgIndex_mod $m
    }
    return
}

proc validate_doc_existence_mod {m} {
    global distribution
    if {[llength [glob -nocomplain [file join $distribution modules $m {*.[13n]}]]] == 0} {
	if {[llength [glob -nocomplain [file join $distribution modules $m {*.man}]]] == 0} {
	    puts "  Without * any ** manpages : $m"
	}
    } elseif {[llength [glob -nocomplain [file join $distribution modules $m {*.man}]]] == 0} {
	puts "  Without doctools manpages : $m"
    } else {
	foreach f [glob -nocomplain [file join $distribution modules $m {*.[13n]}]] {
	    if {![file exists [file rootname $f].man]} {
		puts "     no .man equivalent : $f"
	    }
	}
    }
    return
}

proc validate_doc_existence {} {
    global distribution
    foreach m [modules] {
	validate_doc_existence_mod $m
    }
    return
}


proc validate_doc_markup_mod {m} {
    package require sak::doc
    sak::doc::Gen null null [list $m]
    return
}

proc validate_doc_markup {} {
    package require sak::doc
    sak::doc::Gen null null [modules]
    return
}

proc run-frink {args} {
    global distribution

    set tmp [file rootname [info script]].tmp.[pid]

    if {[llength $args] == 0} {
	set files [tclfiles]
    } else {
	set files [lsort -dict [modtclfiles $args]]
    }

    foreach f $files {
	puts "FRINK ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
	puts "$f..."
	puts "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"

	catch {exec frink 2> $tmp -HJ $f}
	set data [get_input $tmp]
	if {[string length $data] > 0} {
	    puts $data
	}
    }
    catch {file delete -force $tmp}
    return
}

proc run-procheck {args} {
    global distribution

    if {[llength $args] == 0} {
	set files [tclfiles]
    } else {
	set files [lsort -dict [modtclfiles $args]]
    }

    foreach f $files {
	puts "PROCHECK ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
	puts "$f ..."
	puts "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"

	catch {exec procheck >@ stdout $f}
    }
    return
}

proc run-tclchecker {args} {
    global distribution

    if {[llength $args] == 0} {
	set files [tclfiles]
    } else {
	set files [lsort -dict [modtclfiles $args]]
    }

    foreach f $files {
	puts "TCLCHECKER ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
	puts "$f ..."
	puts "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"

	catch {exec tclchecker >@ stdout $f}
    }
    return
}

proc run-nagelfar {args} {
    global distribution

    if {[llength $args] == 0} {
	set files [tclfiles]
    } else {
	set files [lsort -dict [modtclfiles $args]]
    }

    foreach f $files {
	puts "NAGELFAR ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
	puts "$f ..."
	puts "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"

	catch {exec nagelfar >@ stdout $f}
    }
    return
}


proc get_input {f} {return [read [set if [open $f r]]][close $if]}

proc write_out {f text} {
    catch {file delete -force $f}
    puts -nonewline [set of [open $f w]] $text
    close $of
}

proc location_PACKAGES {} {
    global distribution
    return [file join $distribution support releases PACKAGES]
}

proc gd-gen-packages {} {
    global package_version distribution

    set P [location_PACKAGES]
    file copy -force $P $P.LAST
    set f [open $P w]
    puts $f "@@ RELEASE $package_version"
    puts $f ""

    array set packages {}
    foreach {p vm} [ipackages] {
	set packages($p) [lindex $vm 0]
    }

    nparray packages $f
    close $f
}

# --------------------------------------------------------------
# Handle modules using docstrip

proc docstripUser {m} {
    global distribution

    set mdir [file join $distribution modules $m]

    if {[llength [glob -nocomplain -dir $mdir *.stitch]]} {return 1}
    return 0
}

proc docstripRegen {m} {
    global distribution
    puts "$m ..."

    getpackage docstrip docstrip/docstrip.tcl

    set mdir [file join $distribution modules $m]

    foreach sf [glob -nocomplain -dir $mdir *.stitch] {
	puts "* [file tail $sf] ..."

	set here [pwd]
	set fail [catch {
	    cd [file dirname $sf]
	    docstripRunStitch [file tail $sf]
	} msg]
	cd $here
	if {$fail} {
	    puts "  [join [split $::errorInfo \n] "\n  "]"
	}
    }
    return
}

proc docstripRunStitch {sf} {
    # Run the stitch file in a restricted sandbox ...

    set box [restrictedIp {
	input   ::dsrs::Input
	options ::dsrs::Options
	stitch  ::dsrs::Stitch
	reset   ::dsrs::Reset
    }]

    ::dsrs::Init
    set fail [catch {interp eval $box [get_input $sf]} msg]
    if {$fail} {
	puts "    [join [split $::errorInfo \n] "\n    "]"
    } else {
	::dsrs::Final
    }

    interp delete $box
    return
}

proc emptyIp {} {
    set box [interp create]
    foreach c [interp eval $box {info commands}] {
	if {[string equal $c "rename"]} continue
	interp eval $box [list rename $c {}]
    }
    # Rename command goes last.
    interp eval $box [list rename rename {}]
    return $box
}

proc restrictedIp {dict} {
    set box [emptyIp]
    foreach {cmd localcmd} $dict {
	interp alias $box $cmd {} $localcmd
    }
    return $box
}

# --------------------------------------------------------------
# docstrip low level operations for stitching.

namespace eval ::dsrs {
    # Standard preamble to preambles

    variable preamble {}
    append   preamble                                       \n
    append   preamble "This is the file `@output@',"        \n
    append   preamble "generated with the SAK utility"      \n
    append   preamble "(sak docstrip/regen)."               \n
    append   preamble                                       \n
    append   preamble "The original source files were:"     \n
    append   preamble                                       \n
    append   preamble "@input@  (with options: `@guards@')" \n
    append   preamble                                       \n

    # Standard postamble to postambles

    variable postamble {}
    append   postamble                           \n
    append   postamble                           \n
    append   postamble "End of file `@output@'."

    # Default values for the options which are relevant to the
    # application itself and thus have to be defined always.
    # They are processed as global options, as part of argv.

    variable defaults {-metaprefix {%} -preamble {} -postamble {}}

    variable options ; array set options {}
    variable outputs ; array set outputs {}
    variable inputs  ; array set inputs  {}
    variable input   {}
}

proc ::dsrs::Init {} {
    variable outputs ; unset outputs ; array set outputs {}
    variable inputs  ; unset inputs  ; array set inputs  {}
    variable input   {}

    Reset ; # options
    return
}

proc ::dsrs::Reset {} {
    variable defaults
    variable options ; unset options ; array set options {}
    eval [linsert $defaults 0 Options]
    return
}

proc ::dsrs::Input {sourcefile} {
    # Relative to current directory = directory containing the active
    # stitch file.

    variable input $sourcefile
}

proc ::dsrs::Options {args} {
    variable options
    variable preamble
    variable postamble

    while {[llength $args]} {
	set opt [lindex $args 0]

	switch -exact -- $opt {
	    -nopreamble -
	    -nopostamble {
		set o -[string range $opt 3 end]
		set options($o) ""
		set args [lrange $args 1 end]
	    }
	    -preamble {
		set val $preamble[lindex $args 1]
		set options($opt) $val
		set args [lrange $args 2 end]
	    }
	    -postamble {
		set val [lindex $args 1]$postamble
		set options($opt) $val
		set args [lrange $args 2 end]
	    }
	    -metaprefix -
	    -onerror    -
	    -trimlines  {
		set val [lindex $args 1]
		set options($opt) $val
		set args [lrange $args 2 end]
	    }
	    default {
		return -code error "Unknown option: \"$opt\""
	    }
	}
    }
    return
}

proc ::dsrs::Stitch {outputfile guards} {
    variable options
    variable inputs
    variable input
    variable outputs
    variable preamble
    variable postamble

    if {[string equal $input {}]} {
	return -code error "No input file defined"
    }

    if {![info exist inputs($input)]} {
	set inputs($input) [get_input $input]
    }

    set intext $inputs($input)
    set otext  ""

    set c   $options(-metaprefix)
    set cc  $c$c

    set pmap [list @output@ $outputfile \
		  @input@   $input  \
		  @guards@  $guards]

    if {[info exists options(-preamble)]} {
	set pre $options(-preamble)

	if {![string equal $pre ""]} {
	    append otext [Subst $pre $pmap $cc] \n
	}
    }

    array set o [array get options]
    catch {unset o(-preamble)}
    catch {unset o(-postamble)}
    set opt [array get o]

    append otext [eval [linsert $opt 0 docstrip::extract $intext $guards]]

    if {[info exists options(-postamble)]} {
	set post $options(-postamble)

	if {![string equal $post ""]} {
	    append otext [Subst $post $pmap $cc]
	}
    }

    # Accumulate outputs in memory

    append outputs($outputfile) $otext
    return
}

proc ::dsrs::Subst {text pmap cc} {
    return [string trim "$cc [join [split [string map $pmap $text] \n] "\n$cc "]"]
}

proc ::dsrs::Final {} {
    variable outputs
    foreach o [array names outputs] {
	puts "  = Writing $o ..."

	if {[string equal \
		 docstrip/docstrip.tcl \
		 [file join [file tail [pwd]] $o]]} {

	    # We are writing over code required by ourselves.
	    # For easy recovery in case of problems we save
	    # the original 

	    puts "    *Saving original of code important to docstrip/regen itself*"
	    write_out $o.bak [get_input $o]
	}

	write_out $o $outputs($o)
    }
}

# --------------------------------------------------------------
# Configuration

proc __name    {} {global package_name    ; puts -nonewline $package_name}
proc __version {} {global package_version ; puts -nonewline $package_version}
proc __minor   {} {global package_version ; puts -nonewline [lindex [split $package_version .] 1]}
proc __major   {} {global package_version ; puts -nonewline [lindex [split $package_version .] 0]}

# --------------------------------------------------------------
# Development

proc __imodules {} {puts [imodules]}
proc __modules  {} {puts [modules]}
proc __lmodules {} {puts [join [modules] \n]}


proc nparray {a {chan stdout}} {
    upvar $a packages

    set maxl 0
    foreach name [lsort [array names packages]] {
        if {[string length $name] > $maxl} {
            set maxl [string length $name]
        }
    }
    foreach name [lsort [array names packages]] {
	foreach v $packages($name) {
	    puts $chan [format "%-*s %s" $maxl $name $v]
	}
    }
    return
}

proc __packages {} {
    array set packages {}
    foreach {p vm} [ipackages] {
	set packages($p) [lindex $vm 0]
    }
    nparray packages
    return
}

proc __provided {} {
    array set packages [ppackages]
    nparray packages
    return
}

proc checkmod {} {
    global argv
    package require sak::util
    return [sak::util::checkModules argv]
}

# -------------------------------------------------------------------------
# Critcl stuff
# -------------------------------------------------------------------------

# Build critcl modules. If no args then build the default critcl module.
proc __critcl {} {
    global argv critcl critclmodules critcldefault critclnotes tcl_platform
    if {$tcl_platform(platform) == "windows"} {
	set critcl [critcl-app-windows]
    } else {
	set critcl [critcl-app-unix]
    }

    set flags ""
    while {[string match -* [set option [lindex $argv 0]]]} {
        # -debug and -clean only work with critcl >= v04
        switch -exact -- $option {
            -keep  { append flags " -keep" }
	    -I -
	    -L -
	    -libdir -
	    -includedir -
	    -debug -
	    -pkg -
	    -target {
		append flags " $option [lindex $argv 1]"
		set argv [lreplace $argv 0 0]
	    }
            -clean { append flags " -clean" }
            -- { set argv [lreplace $argv 0 0]; break }
            default { break }
        }
        set argv [lreplace $argv 0 0]
    }

    if {$critcl != {}} {
        if {[llength $argv] == 0} {
            puts stderr "[string repeat - 72]"
	    puts stderr "Building critcl components."
	    if {$critclnotes != {}} {
		puts stderr $critclnotes
	    }
	    puts stderr "[string repeat - 72]"

            critcl_module $critcldefault $flags
        } else {
            foreach m [dealias $argv] {
                if {[info exists critclmodules($m)]} {
                    critcl_module $m $flags
                } else {
                    puts "warning: $m is not a critcl module"
                }
            }
        }
    } else {
        puts "error: cannot find a critcl to run."
        return 1
    }
    return
}

proc critcl-app-unix {} {
    # My, isn't it simpler under unix.

    # Look for a critcl sibling to the executing shell
    set shdir [file dirname [info nameofexecutable]]
    set shapp [file join $shdir critcl]
    if {[file exists $shapp]} { return $shapp }

    # Look for critcl in the path
    set critcl [auto_execok critcl]
}

proc critcl-app-windows {} {
    # Windows is a bit more complicated. We have to choose an
    # interpreter, and a starkit for it, and call both.
    #
    # We prefer tclkitsh, but try to make do with a tclsh. That
    # one will have to have all the necessary packages to support
    # starkits. ActiveTcl for example.

    set interpreter {}
    foreach i {critcl.exe tclkitsh tclsh} {
	set interpreter [auto_execok $i]
	if {$interpreter != {}} break
    }

    if {$interpreter == {}} {
	return -code error \
	    "failed to find either tclkitsh.exe or tclsh.exe in path"
    }

    # The critcl starkit can come out of the environment, or we
    # try to locate it using several possible names. We try to
    # find it if and only if we did not find a critcl starpack
    # before.

    if {[file tail $interpreter] == "critcl.exe"} {
	set critcl $interpreter
    } else {
	set kit {}
	if {[info exists ::env(CRITCL)]} {
	    set kit $::env(CRITCL)
	} else {
	    foreach k {critcl.kit critcl} {
		set kit [auto_execok $k]
		if {$kit != {}} break
	    }
	}

	if {$kit == {}} {
	    return -code error "failed to find critcl.kit or critcl in \
                  path.\n\
                  You may wish to set the CRITCL environment variable to the\
                  location of your critcl(.kit) file."
	}
	set critcl [concat $interpreter $kit]
    }

    return $critcl
}

# Prints a list of all the modules supporting critcl enhancement.
proc __critcl-modules {} {
    global critclmodules critcldefault
    foreach m [lsort -dict [array names critclmodules]] {
	if {$m == $critcldefault} {
	    puts "$m **"
	} else {
	    puts $m
	}
    }
    return
}

proc critcl_module {pkg {extra ""}} {
    global critcl distribution critclmodules critcldefault

    if {$pkg == $critcldefault} {
	set files {}
	foreach f $critclmodules($critcldefault) {
	    lappend files [file join $distribution modules $f]
	}
        foreach m [array names critclmodules] {
	    if {$m == $critcldefault} continue
            foreach f $critclmodules($m) {
                lappend files [file join $distribution modules $f]
            }
        }
    } else {
        foreach f $critclmodules($pkg) {
            lappend files [file join $distribution modules $f]
        }
    }
    set target [file join $distribution modules]

    if {"-libdir" ni $extra} { lappend extra -libdir [list $target] }
    if {"-pkg"    ni $extra} { lappend extra -pkg    [list $pkg]    }
    
    catch {
        puts "$critcl -cache [pwd]/.critcl -force $extra $files"
        eval exec $critcl -cache [pwd]/.critcl -force $extra $files 
    } r
    puts $r
    return
}

# -------------------------------------------------------------------------

proc __bench/edit {} {
    global argv argv0

    set format text
    set output {}

    while {[string match -* [set option [lindex $argv 0]]]} {
	set val [lindex $argv 1]
        switch -exact -- $option {
	    -format {
		switch -exact -- $val {
		    raw - csv - text {}
		    default {
			return -error "Bad format \"$val\", expected text, csv, or raw"
		    }
		}
		set format $val
	    }
	    -o    {set output $val}
            -- {
		set argv [lrange $argv 1 end]
		break
	    }
            default { break }
        }
        set argv [lrange $argv 2 end]
    }

    switch -exact -- $format {
	raw {}
	csv {
	    getpackage csv             csv/csv.tcl
	    getpackage bench::out::csv bench/bench_wcsv.tcl
	}
	text {
	    getpackage report           report/report.tcl
	    getpackage struct::matrix   struct/matrix.tcl
	    getpackage bench::out::text bench/bench_wtext.tcl
	}
    }

    getpackage bench::in bench/bench_read.tcl
    getpackage bench     bench/bench.tcl

    if {[llength $argv] != 3} {
	puts "Usage: $argv0 benchdata column newvalue"
    }

    foreach {in col new} $argv break

    _bench_write $output \
	[bench::edit \
	     [bench::in::read $in] \
	     $col $new] \
	{} $format
    return
}

proc __bench/del {} {
    global argv argv0

    set format text
    set output {}

    while {[string match -* [set option [lindex $argv 0]]]} {
	set val [lindex $argv 1]
        switch -exact -- $option {
	    -format {
		switch -exact -- $val {
		    raw - csv - text {}
		    default {
			return -error "Bad format \"$val\", expected text, csv, or raw"
		    }
		}
		set format $val
	    }
	    -o    {set output $val}
            -- {
		set argv [lrange $argv 1 end]
		break
	    }
            default { break }
        }
        set argv [lrange $argv 2 end]
    }

    switch -exact -- $format {
	raw {}
	csv {
	    getpackage csv             csv/csv.tcl
	    getpackage bench::out::csv bench/bench_wcsv.tcl
	}
	text {
	    getpackage report           report/report.tcl
	    getpackage struct::matrix   struct/matrix.tcl
	    getpackage bench::out::text bench/bench_wtext.tcl
	}
    }

    getpackage bench::in bench/bench_read.tcl
    getpackage bench     bench/bench.tcl

    if {[llength $argv] < 2} {
	puts "Usage: $argv0 benchdata column..."
    }

    set in [lindex $argv 0]

    set data [bench::in::read $in]

    foreach c [lrange $argv 1 end] {
	set data [bench::del $data $c]
    }

    _bench_write $output $data {} $format
    return
}

proc __bench/show {} {
    global argv

    set format text
    set output {}
    set norm   {}

    while {[string match -* [set option [lindex $argv 0]]]} {
	set val [lindex $argv 1]
        switch -exact -- $option {
	    -format {
		switch -exact -- $val {
		    raw - csv - text {}
		    default {
			return -error "Bad format \"$val\", expected text, csv, or raw"
		    }
		}
		set format $val
	    }
	    -o    {set output $val}
	    -norm {set norm $val}
            -- {
		set argv [lrange $argv 1 end]
		break
	    }
            default { break }
        }
        set argv [lrange $argv 2 end]
    }

    switch -exact -- $format {
	raw {}
	csv {
	    getpackage csv             csv/csv.tcl
	    getpackage bench::out::csv bench/bench_wcsv.tcl
	}
	text {
	    getpackage report           report/report.tcl
	    getpackage struct::matrix   struct/matrix.tcl
	    getpackage bench::out::text bench/bench_wtext.tcl
	}
    }

    getpackage bench::in bench/bench_read.tcl

    array set DATA {}

    foreach path $argv {
	array set DATA [bench::in::read $path]
    }

    _bench_write $output [array get DATA] $norm $format
    return
}

proc __bench {} {
    global argv

    # I. Process command line arguments for the
    #    benchmark commands - Validation, possible
    #    translation ...

    set flags   {}
    set norm    {}
    set format  text
    set verbose warn
    set output  {}
    set paths   {}
    set interp  {}
    set repeat  0
    set collate min

    while {[string match -* [set option [lindex $argv 0]]]} {
	set val [lindex $argv 1]
        switch -exact -- $option {
	    -throwerrors {lappend flags -errors $val}
	    -match -
	    -rmatch -
	    -iters -
	    -threads {lappend flags $option $val}
	    -o       {set output $val}
	    -norm    {set norm $val}
	    -path    {lappend paths $val}
	    -interp  {set interp $val}
	    -format  {
		switch -exact -- $val {
		    raw - csv - text {}
		    default {
			return -error "Bad format \"$val\", expected text, csv, or raw"
		    }
		}
		set format $val
	    }
	    -collate {
		switch -exact -- $val {
		    min - max - avg {}
		    default {
			return -error "Bad collation \"$val\", expected avg, max, or min"
		    }
		}
		set collate $val
	    }
	    -repeat {
		# TODO: test for integer >= 0
		set repeat $val
	    }
	    -verbose {
		set verbose info
		set argv [lrange $argv 1 end]
		continue
	    }
	    -debug {
		set verbose debug
		set argv [lrange $argv 1 end]
		continue
	    }
            -- {
		set argv [lrange $argv 1 end]
		break
	    }
            default { break }
        }
        set argv [lrange $argv 2 end]
    }

    switch -exact -- $format {
	raw {}
	csv {
	    getpackage csv             csv/csv.tcl
	    getpackage bench::out::csv bench/bench_wcsv.tcl
	}
	text {
	    getpackage report           report/report.tcl
	    getpackage struct::matrix   struct/matrix.tcl
	    getpackage bench::out::text bench/bench_wtext.tcl
	}
    }

    # Choose between benchmarking everything, or
    # only selected modules.

    if {[llength $argv] == 0} {
	_bench_all $paths $interp $flags $norm $format $verbose $output $collate $repeat
    } else {
	if {![checkmod]} {return}
	_bench_module [dealias $argv] $paths $interp $flags $norm $format $verbose $output $collate $repeat
    }
    return
}

proc _bench_module {mlist paths interp flags norm format verbose output coll rep} {
    global package_name package_version

    puts "Benchmarking $package_name $package_version development"
    puts "======================================================"
    bench_mod $mlist $paths $interp $flags $norm $format $verbose $output $coll $rep
    puts "------------------------------------------------------"
    puts ""
    return
}

proc _bench_all {paths flags interp norm format verbose output coll rep} {
    _bench_module [modules] $paths $interp $flags $norm $format $verbose $output $coll $rep
    return
}

# -------------------------------------------------------------------------

proc __oldvalidate_v {} {
    global argv
    if {[llength $argv] == 0} {
	_validate_all_v
    } else {
	if {![checkmod]} {return}
	foreach m [dealias $argv] {
	    _validate_module_v $m
	}
    }
    return
}

proc _validate_all_v {} {
    global package_name package_version
    set i 0

    puts "Validating $package_name $package_version development"
    puts "==================================================="
    puts "[incr i]: Consistency of package versions ..."
    puts "------------------------------------------------------"
    validate_versions
    puts "------------------------------------------------------"
    puts ""
    return
}

proc _validate_module_v {m} {
    global package_name package_version
    set i 0

    puts "Validating $package_name $package_version development -- $m"
    puts "==================================================="
    puts "[incr i]: Consistency of package versions ..."
    puts "------------------------------------------------------"
    validate_versions_mod $m
    puts "------------------------------------------------------"
    puts ""
    return
}


proc __oldvalidate {} {
    global argv
    if {[llength $argv] == 0} {
	_validate_all
    } else {
	if {![checkmod]} {return}
	foreach m $argv {
	    _validate_module $m
	}
    }
    return
}

proc _validate_all {} {
    global package_name package_version
    set i 0

    puts "Validating $package_name $package_version development"
    puts "==================================================="
    puts "[incr i]: Existence of testsuites ..."
    puts "------------------------------------------------------"
    validate_testsuites
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Existence of package indices ..."
    puts "------------------------------------------------------"
    validate_pkgIndex
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Consistency of package versions ..."
    puts "------------------------------------------------------"
    validate_versions
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Installed vs. developed modules ..."
    puts "------------------------------------------------------"
    validate_imodules
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Existence of documentation ..."
    puts "------------------------------------------------------"
    validate_doc_existence
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Validate documentation markup (doctools) ..."
    puts "------------------------------------------------------"
    validate_doc_markup
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Static syntax check ..."
    puts "------------------------------------------------------"

    set frink      [auto_execok frink]
    set procheck   [auto_execok procheck]
    set tclchecker [auto_execok tclchecker]
    set nagelfar [auto_execok nagelfar]

    if {$frink == {}} {puts "  Tool 'frink'    not found, no check"}
    if {($procheck == {}) || ($tclchecker == {})} {
	puts "  Tools 'procheck'/'tclchecker' not found, no check"
    }
    if {$nagelfar == {}} {puts "  Tool 'nagelfar' not found, no check"}

    if {($frink == {}) || ($procheck == {}) || ($tclchecker == {}) 
        || ($nagelfar == {})} {
	puts "------------------------------------------------------"
    }
    if {($frink == {}) && ($procheck == {}) && ($tclchecker == {})
        && ($nagelfar == {})} {
	return
    }
    if {$frink != {}} {
	run-frink
	puts "------------------------------------------------------"
    }
    if {$tclchecker != {}} {
	run-tclchecker
	puts "------------------------------------------------------"
    } elseif {$procheck != {}} {
	run-procheck
	puts "------------------------------------------------------"
    }
    if {$nagelfar    !={}} {
    	run-nagelfar 
	puts "------------------------------------------------------"
    }
    puts ""
    return
}

proc _validate_module {m} {
    global package_name package_version
    set i 0

    puts "Validating $package_name $package_version development -- $m"
    puts "==================================================="
    puts "[incr i]: Existence of testsuites ..."
    puts "------------------------------------------------------"
    validate_testsuite_mod $m
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Existence of package indices ..."
    puts "------------------------------------------------------"
    validate_pkgIndex_mod $m
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Consistency of package versions ..."
    puts "------------------------------------------------------"
    validate_versions_mod $m
    puts "------------------------------------------------------"
    puts ""

    #puts "[incr i]: Installed vs. developed modules ..."
    puts "------------------------------------------------------"
    validate_imodules_mod $m
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Existence of documentation ..."
    puts "------------------------------------------------------"
    validate_doc_existence_mod $m
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Validate documentation markup (doctools) ..."
    puts "------------------------------------------------------"
    validate_doc_markup_mod $m
    puts "------------------------------------------------------"
    puts ""

    puts "[incr i]: Static syntax check ..."
    puts "------------------------------------------------------"

    set frink    [auto_execok frink]
    set procheck [auto_execok procheck]
    set nagelfar [auto_execok nagelfar]
    set tclchecker [auto_execok tclchecker]
    
    if {$frink    == {}} {puts "  Tool 'frink'    not found, no check"}
    if {($procheck == {}) || ($tclchecker == {})} {
	puts "  Tools 'procheck'/'tclchecker' not found, no check"
    }
    if {$nagelfar == {}} {puts "  Tool 'nagelfar' not found, no check"}
    
    if {($frink == {}) || ($procheck == {}) || ($tclchecker == {}) ||
    	($nagelfar == {})} {
	puts "------------------------------------------------------"
    }
    if {($frink == {}) && ($procheck == {}) && ($nagelfar == {})
        && ($tclchecker == {})} {
	return
    }
    if {$frink    != {}} {
	run-frink $m
	puts "------------------------------------------------------"
    }
    if {$tclchecker != {}} {
	run-tclchecker $m
	puts "------------------------------------------------------"
    } elseif {$procheck != {}} {
	run-procheck $m
	puts "------------------------------------------------------"
    }
    if {$nagelfar    !={}} {
    	run-nagelfar $m
	puts "------------------------------------------------------"
    }
    puts ""

    return
}

# --------------------------------------------------------------
# Release engineering

proc __gendist {} {
    gd-cleanup
    gd-tip55
    gd-gen-rpmspec
    gd-gen-tap
    gd-gen-yml
    gd-assemble
    gd-gen-archives

    puts ...Done
    return
}

proc __gentip55 {} {
    gd-tip55
    puts "Created DESCRIPTION.txt"
    return
}

proc __yml {} {
    global package_name
    gd-gen-yml
    puts "Created YAML spec file \"${package_name}.yml\""
    return
}

proc __contributors {} {
    global contributors
    contributors
    foreach person [lsort [array names contributors]] {
        puts "$person <$contributors($person)>"
    }
    return
}

proc __tap {} {
    global package_name
    gd-gen-tap
    puts "Created Tcl Dev Kit \"${package_name}.tap\""
}

proc __rpmspec {} {
    global package_name
    gd-gen-rpmspec
    puts "Created RPM spec file \"${package_name}.spec\""
}


proc __release {} {
    # Regenerate PACKAGES, and extend
    gd-gen-packages
    return

    global argv argv0 distribution package_name package_version

    getpackage textutil textutil/textutil.tcl

    if {[llength $argv] != 2} {
	puts stderr "$argv0: wrong#args: release name sf-user-id"
	exit 1
    }

    foreach {name sfuser} $argv break
    set email "<${sfuser}@users.sourceforge.net>"
    set pname [textutil::cap $package_name]

    set notice "[clock format [clock seconds] -format "%Y-%m-%d"]  $name  $email

	*
	* Released and tagged $pname $package_version ========================
	* 

"

    set logs [list [file join $distribution ChangeLog]]
    foreach m [modules] {
	set m [file join $distribution modules $m ChangeLog]
	if {![file exists $m]} continue
	lappend logs $m
    }

    foreach f $logs {
	puts "\tAdding release notice to $f"
	set fh [open $f r] ; set data [read $fh] ; close $fh
	set fh [open $f w] ; puts -nonewline $fh $notice$data ; close $fh
    }

    gd-gen-packages
    return
}

# --------------------------------------------------------------
# Documentation

proc __desc  {} {
    global argv ; if {![checkmod]} return
    array set pd [getpdesc]

    getpackage struct::matrix struct/matrix.tcl
    getpackage textutil       textutil/textutil.tcl

    struct::matrix m
    m add columns 3

    puts {Descriptions...}
    if {[llength $argv] == 0} {set argv [modules]}

    foreach m [lsort [dealias $argv]] {
	array set _ {}
	set pkg {}
	foreach {p vlist} [ppackages $m] {
	    catch {set _([lindex $pd($p) 0]) .}
	    lappend pkg $p
	}
	set desc [string trim [join [array names _] ", "] " \n\t\r,"]
	set desc [textutil::adjust $desc -length 20]
	unset _

	m add row [list $m $desc]
	m add row {}

	foreach p [lsort -dictionary $pkg] {
	    set desc ""
	    catch {set desc [lindex $pd($p) 1]}
	    if {$desc != ""} {
		set desc [string trim $desc]
		set desc [textutil::adjust $desc -length 50]
		m add row [list {} $p $desc]
	    } else {
		m add row [list {**} $p ]
	    }
	}
	m add row {}
    }

    m format 2chan
    puts ""
    return
}

proc __desc/2  {} {
    global argv ; if {![checkmod]} return
    array set pd [getpdesc]

    getpackage struct::matrix struct/matrix.tcl
    getpackage textutil       textutil/textutil.tcl

    puts {Descriptions...}
    if {[llength $argv] == 0} {set argv [modules]}

    foreach m [lsort [dealias $argv]] {
	struct::matrix m
	m add columns 3

	m add row {}

	set pkg {}
	foreach {p vlist} [ppackages $m] {lappend pkg $p}

	foreach p [lsort -dictionary $pkg] {
	    set desc ""
	    set sdes ""
	    catch {set desc [lindex $pd($p) 1]}
	    catch {set sdes [lindex $pd($p) 0]}

	    if {$desc != ""} {
		set desc [string trim $desc]
		#set desc [textutil::adjust $desc -length 50]
	    }

	    if {$desc != ""} {
		set desc [string trim $desc]
		#set desc [textutil::adjust $desc -length 50]
	    }

	    m add row [list $p "  $sdes" "  $desc"]
	}
	m format 2chan
	puts ""
	m destroy
    }

    return
}

# --------------------------------------------------------------

proc __docstrip/users {} {
    # Print the list of modules using docstrip for their code.

    set argv [modules]
    foreach m [lsort $argv] {
	if {[docstripUser $m]} {
	    puts $m
	}
    }

    return
}

proc __docstrip/regen {} {
    # Regenerate modules based on docstrip.

    global argv ; if {![checkmod]} return
    if {[llength $argv] == 0} {set argv [modules]}

    foreach m [lsort [dealias $argv]] {
	if {[docstripUser $m]} {
	    docstripRegen $m
	}
    }

    return
}

# --------------------------------------------------------------
## Make sak specific packages visible.

lappend auto_path [file join $distribution support devel sak]

# --------------------------------------------------------------
## Dispatcher to the sak commands.

set  cmd  [lindex $argv 0]
set  argv [lrange $argv 1 end]
incr argc -1

# Prefer a command implementation found in the support tree.
# Then see if the command is implemented here, in this file.
# At last fail and report possible commands.

set base  [file dirname [info script]]
set sbase [file join $base support devel sak]
set cbase [file join $sbase $cmd]
set cmdf  [file join $cbase cmd.tcl]

if {[file exists $cmdf] && [file readable $cmdf]} {
    source $cmdf
    exit 0
}

if {[llength [info procs __$cmd]] == 0} {
    puts stderr "$argv0 : Illegal command \"$cmd\""
    set fl {}
    foreach p [info procs __*] {
	lappend fl [string range $p 2 end]
    }
    foreach p [glob -nocomplain -directory $sbase */cmd.tcl] {
	lappend fl [lindex [file split $p] end-1]
    }

    regsub -all . $argv0 { } blank
    puts stderr "$blank : Should have been [linsert [join [lsort -uniq $fl] ", "] end-1 or]"
    exit 1
}

__$cmd
exit 0
