2004-10-04
VI Short for Setok's and Venkat's make. A tcl based replacement for the traditional make. The concepts are the same as the great
smake. Here's whats different. Also see
smake musings- There is no setRule command. target accepts patterns as well as names and they're treated the same, resulting in shorter more elegant code.
- There is no upTarget command. The whole stack of targets built is available in the local variable stack.
- Exec is now called system and has options to be quiet and to ignore errors (a la make)
- Target changed is more inline with the traditional make. There are four cases when a target is considered changed. (a) when the code does a return 1 (b) when any dependency is built. (c) when a dependency is not built, but is a file with an older timestamp than the target and (d) when the target does not exist as a file at the end of the code.
- Targets built once are not built again in the same session (like make)
- target allows multiple targets (for convenience), just like each of them had been specified separately with the same body.
And to a lesser extent:
- (commentable) auto logging of commands run
- accept multiple targets on command line
- allow definition of global variables on the command line
- single file (< 300 lines) with no external dependencies (other than tcl). Perfect for the wiki.
- new commands dputs (for debug) and log (for logging) and tcl (print command before running).
- no compile and link commands (not really core).
I have used it extensively. Hope it works for someone else too!
#!/usr/local/tcl/8.4.5/bin/tclsh8.4
## Setok's and Venkat's Make. Provides similar functionality as 'make' but with TCL
## Authors: Kristoffer Lawson, setok@fishpool.com, Venks I venksi@yahoo.com
package require Tcl 8.3
namespace eval ::svmk {
namespace export target depend system tcl log dputs
}
set ::svmk::dbglevel 0
proc ::svmk::dputs {lvl txt} {
if {$lvl<=$::svmk::dbglevel} {
puts stderr "[string repeat " " $lvl]* $txt"
}
}
proc ::svmk::system {args} { # wrapper around exec
set ignore_err 0
set quiet 0
set newargs ""
if {![regexp {[^2]>} $args] && ![regexp {[|]} $args]} {
append newargs " >@ stdout"
}
if {![regexp {>&} $args] && ![regexp {[|][&]} $args] && ![regexp {2>} $args]} {
append newargs " 2>@ stderr"
}
while {[string match {\-*} [set opt [lindex $args 0]]]} {
set args [lrange $args 1 end]
if [string match "--" $opt] break
switch -- $opt {
"-i" - "-ignore" {set ignore_err 1}
"-q" - "-quiet" {set quiet 1}
default {error "Unknown option $opt to exec in $args"}
}
}
append args $newargs
if {$quiet == 0} {
::svmk::dputs 0 $args
}
set rc [catch {
eval [concat exec $args]
} errmsg]
if {$rc} { # failed
if {$ignore_err} { # ignore flag set
if {!$quiet} { # Not quiet print messages
::svmk::dputs 0 "$errmsg"
::svmk::dputs 1 "$::errorCode\n$::errorInfo"
::svmk::dputs 0 "Error from \"$args\" Ignored"
}
} else { # not ignored. Die
::svmk::dputs 0 "Failed: $::errorCode"
error $errmsg $::errorInfo $::errorCode
}
}
return $rc; # return the output of the command.
}
proc ::svmk::tcl {args} { # simple proc to print a command before running it, like system
::svmk::dputs 0 $args
uplevel $args
}
proc ::svmk::countchars {str char} { # count number of char in string
return [llength [regexp -inline -all \[$char\] $str]]
}
# Determines if pattern 1 is a child of pattern 2
proc ::svmk::ischild {pat1 pat2} {
# puts "[string match $pat2 $pat1],[string match $pat1 pat2],[countchars $pat1 "?"],[countchars $pat2 "?"]"
if {[string match $pat2 $pat1] && ![string match $pat1 $pat2]} {
return 1; # one way only match means it is a child
}
if {[string match $pat2 $pat1] && [string match $pat1 $pat2] &&
[countchars $pat1 "?"] > [countchars $pat2 "?"]} {
return 1; # two way match and lesser question marks means a child
}
return 0
}
# Returns 1 if the pattern was added, returns 0 if the pattern could not be added
proc ::svmk::addpattern {pattern {opattern *}} {
variable children
if {[string compare $pattern $opattern] == 0} {
return 1; # if identical, pretend we added
}
if ![::svmk::ischild $pattern $opattern] {
return 0; # can't be added under current opattern
}
::svmk::dputs 6 "Adding pattern <$pattern> under pattern <$opattern>"
set oldchildren $children($opattern)
foreach child $oldchildren { # check children against pattern
if [::svmk::addpattern $pattern $child] {
return 1; # got added as child somewhere our work is done
}
}
if ![info exists children($pattern)] {
set children($pattern) [list]; # children start off empty
}
# Check if current children of opattern need to be pushed under pattern
set children($opattern) [list $pattern]; # this is rebuilt with the ones that arent pushed
foreach child $oldchildren { # check children against pattern
if ![::svmk::addpattern $child $pattern] {
lappend children($opattern) $child; # wasn't added keep here.
}
}
return 1
}
# Returns most specific pattern for a particular target.
proc ::svmk::getpattern {target {pattern *}} {
foreach child $::svmk::children($pattern) { # check children against pattern
if [string match $child $target] {
return [::svmk::getpattern $target $child]
}
}
return $pattern
}
set ::svmk::children(*) [list]
# Debug Routine to print the children tree. Not used internally.
proc ::svmk::printchildren {{indentlevel 0} {pattern *}} {
::svmk::dputs 5 "[string repeat "--" $indentlevel]: <$pattern>"
incr indentlevel
foreach child $::svmk::children($pattern) {
::svmk::printchildren $indentlevel $child
}
}
proc ::svmk::target {targets code} { # define a new target, compiletime
foreach target $targets {
::svmk::addpattern $target
set ::svmk::code($target) $code
}
}
proc ::svmk::build {pattern target stack} { # run the code for a target, run time
::svmk::dputs 5 "Code is $::svmk::code($pattern)"
eval $::svmk::code($pattern)
return 0
}
proc ::svmk::depend {targets op} { # check dependencies for current target.
upvar target uptarget
foreach target $targets {
if ![info exists ::svmk::built($target)] {
set ::svmk::built($target) 0
lappend ::svmk::stack $target
dputs 4 "Building <$target> ([::svmk::getpattern $target]), stack <$::svmk::stack>"
set ::svmk::built($target) [::svmk::build [::svmk::getpattern $target] $target $::svmk::stack]
set ::svmk::stack [lreplace $::svmk::stack end end]
::svmk::dputs 4 "After code for <$target>, update is $::svmk::built($target)"
} else {
dputs 4 "Not rebuilding <$target> because it has been built before, update is $::svmk::built($target)"
}
set ::svmk::built($uptarget) [expr $::svmk::built($target) || $::svmk::built($uptarget)]
if {!$::svmk::built($uptarget) && [file exists $uptarget] && [file exists $target]} {
if {[file mtime $uptarget] < [file mtime $target]} {
::svmk::dputs 2 "Dependency <$target> ([clock format [file mtime $target]]) was not rebuilt but is newer than target <$uptarget> ([clock format [file mtime $uptarget]]), rebuild <$uptarget>"
set ::svmk::built($uptarget) 1
}
}
}
if {!$::svmk::built($uptarget) && ![file exists $uptarget]} {
::svmk::dputs 2 "Target <$uptarget> does not exist yet, rebuild"
set ::svmk::built($uptarget) 1
}
if {$::svmk::built($uptarget)} {
uplevel 1 $op
}
}
proc ::svmk::parse_opt {argv} { # Parse options from argv and return the rest of the arguments
set usage {
svmk [options] [VAR=value]* [target]*
Options:
-f <file> Read this file instead of default Smakefile
-d <level> Message Level (0 default, lower is quieter, higher is noisier)
--help Display this message
--version Display Version
VAR=value Presets global VAR before making any target
}
set ::svmk::makefile Smakefile; # default makefile name
set parsed_args [list]; # ends up with list of targets
set i 0
while {$i < [llength $argv]} {
set arg [lindex $argv $i]
if {[string equal $arg "-f"]} {
incr i
if {$i == [llength $argv]} {
error "-f requires filename as an argument\n$usage"
}
set ::svmk::makefile [lindex $argv $i]
} elseif {[string equal $arg "-d"]} {
incr i
if {$i == [llength $argv]} {
error "-d requires debug level as an argument\n$usage"
}
set ::svmk::dbglevel [lindex $argv $i]
} elseif {[string match "--help" $arg]} {
puts $usage
exit
} elseif {[string equal $arg "--version"]} {
regsub -all {\$([A-Za-z]+:)?} {$Revision: 1.4 $ $Date: 2004-10-05 06:00:30 $} {} version
puts "Version $version"
exit
} elseif {[regexp {^([^=]*)=(.*)$} $arg => name value]} {
set ::$name $value
} else {
lappend parsed_args $arg
}
incr i
}
if {![llength $parsed_args]} {
set parsed_args [list all]; # default target
}
return $parsed_args
}
proc ::svmk::log {str} { # procedure for logging
set fo [open [file rootname $::svmk::makefile].log a]
puts $fo "[clock format [clock seconds] -format "%Y/%m/%d.%H:%M:%S"]|$str"
close $fo
}
::svmk::target log {::svmk::log $::argv; exit}; # The Log Target - used to enter comments into the log
# Default Rule for no matches, target "*"
::svmk::target * {
set uptarget [lindex $stack end-1]
if {[file exists $target]} {
if {[file exists $uptarget]} {
set ttime [file mtime $target]
set utime [file mtime $uptarget]
if { $ttime < $utime} {
::svmk::dputs 3 "Dependency <$target> ([clock format $ttime]) < Target <$uptarget> ([clock format $utime]), No update"
return 0
} else {
::svmk::dputs 2 "Dependency <$target> ([clock format $ttime]) > Target <$uptarget> ([clock format $utime]), Update"
return 1
}
} else {
::svmk::dputs 2 "File <$uptarget> does not exist, force update"
return 1
}
} else {
error "do not know how to build <$target>"
}
}
proc svmk::svmk {argv} {
namespace eval :: {
namespace import ::svmk::*
interp alias {} setRule {} ::svmk::target
if [catch {
set targets [::svmk::parse_opt $argv]
source $::svmk::makefile
::svmk::printchildren
::svmk::log $::argv; # Comment this if you don't want logging
foreach target $targets {
::svmk::depend $target {}
}
} msg] {
::svmk::dputs -1 $msg
::svmk::dputs 1 "$::errorCode\n$::errorInfo"
exit 1
}
}
}
svmk::svmk $::argv