tclmake

tclmake 2022, v. 2.2

SEH 20220304 -- Version 2.2: fix for bug in operation of "--update" command line option.

SEH 20220118 -- Released version 2.1 with newly-configured ability to evaluate Tcl code in make variables. AM discovered that Tcl code placed in make variables would be evaluated, due to inadvisable use of eval commands in the old sections of the code. Though useful, those eval commands presented potential problems in makefile parsing, so I replaced the eval's with expand operators, but kept the ability to execute Tcl code by starting a make variable definition line with the keyword MAKE_EVAL. See examples below.

SEH 20220110 -- I've done some refurbishing of the original tclmake code released as part of the Ptolemy project at Berkeley, and added some new features. The new code can be found at: [L1 ]

tclmake is not meant to be a clone of standard make, but it borrows many features and adds a few useful features of its own. Anyone with experience using standard make should find it easy to pick up and use tclmake.

If you've ever struggled with writing a GNU makefile and thought, 'this would go a lot easier if I could write the update logic in Tcl', then tclmake may be the tool for you.

What a tclmake makefile looks like:

    # Value of MAKE_INIT var is treated as a Tcl script and executed after file
    # is parsed but before goal updating:
    MAKE_INIT = make_init

    # Make variables defined:
    SDX = lib/sdx.kit
    RUNTIME = bin/basekit

    # Procs can be defined and called within rule recipe script:
    proc make_init {} {
            set ::env(PATH) $::env(PATH):[file norm ~/bin]
    }

    # An option rule:
    --test-wrap :
            # Automatic variables like "$!" inserted into Tcl script by macro
            # substitution before execution, as GNU Make does:
            set target "$!"
            if {![file exists $target]} return
            set vfs [file root $target].vfs
            lassign [exec basekit $(SDX) version $target] date time version
            lassign [exec basekit $(SDX) version $vfs] vfsdate vfstime vfsversion
            # Proc MAKE_UPDATE drives whether a target is updated based on given
            # conditional, independent of file mtimes of prerequisites:
            MAKE_UPDATE $target [list $version ne $vfsversion]

    # A GNU make-style pattern rules:
    %.kit : %.vfs --test-wrap
            @puts "Wrapping [email protected]:"
            # Make vars substituted into Tcl script. If a var is undefined, an empty
            string is used:
            exec basekit $(SDX) wrap "[email protected]" -vfs "$<" $(WRAP_OPTIONS)

    %.exe : %.kit
            if {"$(RUNTIME)" eq ""} {
                    error "Make variable RUNTIME must be defined to make starpack."
            }
            @puts "Wrapping [email protected]:"
            set vfs [file root "[email protected]"].vfs
            exec basekit $(SDX) wrap "[email protected]" -vfs $vfs -runtime "$(RUNTIME)" $(WRAP_OPTIONS)

Like GNU make, tclmake features simple and pattern rules with targets and prerequisites, make variables and automatic variables with values inserted by macro substitution.

tclmake can be used as a stand-alone command line program, or as a package within another Tcl project.

tclmake allows you to define Tcl procs in the makefile, allowing you to organize complex update logic within your makefile.

You can define a 'MAKE_INIT' make variable to contain a Tcl script. If the variable exists after the makefile has been parsed but before updating of targets begins, this script will be evaluated. The script can for example change directories, load packages or otherwise initialize the environment.

The procedure MAKE_UPDATE is available to be called by any Tcl script in the makefile. It takes two arguments: a target name and a conditional expression. If the conditional evaluates to true, the update script of the specified target is run regardless of whether the target is out of date with respect to its prerequisites. If the expression is false, the target will not be updated even if it is out of date.

The MAKE_UPDATE proc allows you to define any criteria for updating a target, beyond simply comparing file modification times. If MAKE_UPDATE is called in the script of one of the target's prerequisites, when prerequisite updating is done and it's time for the target to be updated, the target's update script will be run or not based on the result of the MAKE_UPDATE conditional.

The example makefile above shows rules for wrapping a starkit and creating a starpack. If you want to know if a wrapped starkit needs to be updated from its unwrapped counterpart, you need to know if any file in the unwrapped starkit has changed. In a standard GNU makefile, you'd have to include all files in the unwrapped starkit as dependencies, and ensure this list of files remains current as your starkit is developed.

Fortunately the sdx package includes a command to create a version stamp for a starkit, which operates identically on a wrapped and an unwrapped starkit. The option rule '--test-wrap' generates version stamps for a wrapped and unwrapped starkit, and the MAKE_UPDATE proc is used to compare the version stamps and mark if the wrapped starkit needs to be regenerated.


Version 1.0 released 1998:

Author: John Reekie

Contact: John Reekie atatat uts dot edu dot au

Tclmake


AM After some initial struggles to get it to work, I managed to create an intriguing little makefile. To give some context:

  • I used a Github package https://github.com/WarrenWeckesser/dde_solver/ that contains a Fortran module in the main directory and a bunch of examples in the subdirectory examples.
  • Rather than write out the names of the examples in a makefile, I used [glob] to find the names of the individual source files and then the rule definitons that tclmake supports to automatically build all the examples.

The result is this minimalistic makefile in the tclmake style:

# makefile for "DDE_SOLVER" module and its examples

# keyword "MAKE_EVAL" at start of make var definition causes var value to
# have Tcl command and variable substitution done on it and result stored 
# in var:
MAKE_EVAL SOURCES = [glob examples/*.f90]
MAKE_EVAL EXES    = [string map {.f90 .exe} {$(SOURCES)}]

# Simple rule. First target in file becomes default goal:
all:    $(EXES)
   @puts "prereqs:$^"
   
# Simple rule without recipe adds prerequisites to targets:
$(EXES): dde_solver_m.o

# Pattern rule:
%.exe : %.f90
   exec gfortran -o [email protected] $< dde_solver_m.o

# This target is added as a dependency to all files in $(EXES) var:
dde_solver_m.o : dde_solver_m.f90
   exec gfortran -c $<

Note the list expansion in the "all" target. It took a bit of experimentation (as I am a trifle rusty on my make skills), but it works. You could go on with parametrising the stuff (.exe and .o and the name of the compiler), but for a first not-so-trivial makefile this does well enough.

(The build step fails when it tries to build the "program" in q1damodule.f90 as that merely defines another module, not a program perse. Oh well, that shows that a more in-depth analysis is required, but note: it does build 14 of the examples ;))

SEH 20220118 -- N.B. I rewrote AM's example above to conform to how tclmake 2.1 evaluates Tcl code in make variables.

AM's use case is a good fit for tclmake's "reverse pattern match rule" feature. Instead of the usual specifying of targets and matching prerequisites with them, a rule using this feature's syntax will glob a set of prerequisites, construct targets for them, then run the associated Tcl script once for each target/prereq pair. So the makefile above could be rewritten as:

all: dde_solver_m.o all_examples
   
all_examples : examples/%.exe :: examples/%.f90
   exec gfortran -o [email protected] $< dde_solver_m.o

dde_solver_m.o : dde_solver_m.f90
   exec gfortran -c $<