A Little Make Replacement

Copyright 2003 George Peter Staplin

You may use this under the same terms as Tcl.

A Little Make Replacement (AKA tickmake)

Make is a common build tool. It basically checks the file mtime of dependencies and compares them to the file mtime of target files (if they exist). If a dependency is newer than the target then the target should be generated. Most make programs are complex. They usually implement a limited-command-language that is difficult to work with. Make also has a lot of historical baggage... With Tcl we have a great command language that is well suited for a tool like this.

Do not be misled by the small amount of code to do this. It really is this simple!

If you have the desire to modify this on a Wiki page please append your changed version or your changes and keep the original intact.

Tested with Tcl 8.4.1 in OpenBSD.

Thanks to Gerald Lester for debugging.

 proc file.newer? {f _than_ f2} {
  if {![file exists $f] || ![file exists $f2]} {
   return 1
  }
  if {[file mtime $f] > [file mtime $f2]} {
   return 1
  } 
  return 0
 }
 
 proc is.file? f {
  return [file isfile $f]
 }
 
 proc is.target? t {
  return [info exists ::targetAr($t)]
 }
 
 array set ::targetAr {}
 
 proc make {t _from_ fromList _using_ body} {
  set ::targetAr($t) [list $fromList $body]
  interp alias {} $t {} make.target $t
 }
 
 proc make.target t {
  if {[is.target? $t]} {
   foreach {fromList body} [set ::targetAr($t)] break
 
   set build 0
   foreach f $fromList {
    if {[is.target? $f]} {
     set r [make.target $f]
     if {[is.file? $f] && [file.newer? $f than $t]} {
      set build 1
     } elseif {$r} {
      set build 1
     }
    } elseif {[is.file? $f]} {
     if {!$build} {
      set build [file.newer? $f than $t]
     }
    } else {
     return -code error "$f isn't a target or existing source"
    }
   }
   if {$build} {
    uplevel #0 $body
    return 1
   }
   return 0
  } else {
   return -code error "make.target called with an invalid target: $t"
  }
 }
 
 proc mexec args {
  set cmd [join $args]
  puts $cmd
  catch {eval exec -- $cmd} msg
  if {[string length $msg]} {
   puts $msg
  }
 }

We will now give examples of usage.

It's quite easy to use, and everything is evaluated in level 0 (global). It passes all of these tests for me. You can play around with the touch command to see that it properly generates targets when needed.

 set ::CC gcc
 set ::CFLAGS "-Wall -W"
 
 make test_2.o from test_2.c using {
  mexec $CC $CFLAGS -c test_2.c
 }
 
 make test_lib.a from [list test_2.o test_3.c] using {
  mexec $CC $CFLAGS -c test_3.c
  mexec ar cr test_lib.a test_2.o test_3.o
  mexec ranlib test_lib.a
 }
 
 make a.exe from [list test_1.c test_lib.a] using {
  mexec $CC $CFLAGS test_1.c test_lib.a -o a.exe
 }
 
 proc main {} {
  if {0 == $::argc} {
   a.exe
  } elseif {1 == $::argc} {
   eval [lindex $::argv 0]
  } else {
   return -code error "invalid number of arguments: $::argv0 ?target?"
  }
 }
 main

You can also use targets and sources with directory paths like so:

  make tdir/a.exe from [list tdir/test_1.c tdir/test_2.c tdir/test_3.c] using {
   set d tdir
   exec gcc $d/test_1.c $d/test_2.c $d/test_3.c -o $d/a.exe
  }
  tdir/a.exe

EF The code above has served as the basis for my make library http://www.sics.se/~emmanuel/?Code:make . I only have added the possibility to force the execution of some rules using make.force. The code is part of the library.


See also: smake, bras