racket-makefile
1 Installation
2 A first makefile
3 Executing targets
make
4 Targets and dependencies
target
deps
phony
default-target
5 Recipe context
$target
$deps
$<
6 Running commands
run
7 Running Racket tools
raco
8 Cleanup helpers
rm-f
rm-rf
cleanup
9 Regexp directory helpers
list-dir/  files
list-files
list-dirs
10 Using ordinary Racket
11 Package example
9.2

racket-makefile🔗ℹ

Hans Dijkema

 #lang racket-makefile package: racket-makefile

racket-makefile is a small make-style language built on Racket. It adds targets, dependencies, timestamp based rebuilding, phony targets, command execution, Racket tool execution, and a few cleanup helpers. The rest of the language is ordinary Racket.

A makefile only defines targets. Loading or running the module does not execute any recipe automatically. Builds are started explicitly with make.

1 Installation🔗ℹ

Install the package from a local checkout with:

raco pkg install

For a versioned archive, specify the package name explicitly:

raco pkg install --name racket-makefile racket-makefile-0.1.5.zip

After installation a makefile can start with:

#lang racket-makefile

2 A first makefile🔗ℹ

For example:

(define CC 'cc)
(define CFLAGS '(-Wall -O2))
 
(default-target all)
(phony all clean)
 
(target all
  (deps "hello"))
 
(target "hello"
  (deps "hello.c")
  (run `(,CC ,@CFLAGS -o $target $<)))
 
(target clean
  (rm-f "hello")
  (rm-rf "compiled")
  (cleanup "scrbl" '("**.html" "**.js" "**.css")))

Loading the file only registers these targets; it does not build all or any other target.

3 Executing targets🔗ℹ

syntax

(make name ...)

Builds the supplied targets. A bare identifier is always interpreted as a literal target name, so (make clean) selects the target named 'clean and does not require quoting.

With no arguments, (make) builds the target selected by default-target. If no default target has been specified, the first declared target is used.

Multiple targets are allowed and are processed in the supplied order, for example (make clean all). Dependencies that are shared by multiple selected targets are built only once during that make invocation.

From the command line, load the makefile with -t and evaluate a make form with -e:

racket -t Makefile.rkt -e "(make)"

racket -t Makefile.rkt -e "(make all)"

racket -t Makefile.rkt -e "(make clean)"

racket -t Makefile.rkt -e "(make clean all)"

Loading a makefile without -e executes no target:

racket -t Makefile.rkt

In DrRacket, open the makefile and press Run. This loads and registers the targets. Then execute targets in the Interactions window:

> (make)

> (make clean)

> (make all)

> (make clean all)

4 Targets and dependencies🔗ℹ

syntax

(target name (deps dependency ...) body ...)

Defines a target. name can be a symbol, string, path, or a Racket expression producing one of those values. Each dependency can also produce a list; dependency lists are recursively flattened.

An unbound identifier is treated as a literal symbol. Consequently, (target clean ...) defines the target 'clean, while a bound identifier can be used to generate targets from ordinary Racket code. If a literal target name happens to be bound by Racket, quote it explicitly, for example (target 'compile ...).

The body is not evaluated when the target is declared. It is saved as the target recipe and evaluated only when the target must be rebuilt.

syntax

(deps dependency ...)

Specifies the dependencies of a target. Dependency expressions that produce lists are automatically spliced into the dependency list. deps is only valid directly inside target.

syntax

(phony name ...)

Marks targets as phony. A phony target is always executed when requested or when reached as a dependency.

syntax

(default-target name)

Selects the target used by (make) when no target is supplied. If no default target is specified, the first declared target is used.

A non-phony target is rebuilt when its output does not exist or when a dependency is newer than the target. Registered target dependencies are built first. A dependency that is neither a registered target nor an existing file is an error. Dependency cycles are reported as errors.

5 Recipe context🔗ℹ

Inside a target recipe the following identifiers are available:

syntax

$target

The current target name.

syntax

$deps

A list containing all dependencies of the current target.

syntax

$<

The first dependency of the current target. An error is raised when the target has no dependencies.

The same names can occur as symbols inside a quoted command passed to run.

6 Running commands🔗ℹ

procedure

(run command)  void?

  command : list?
Runs an external command directly, without an intermediate shell. The first item is the executable and the remaining items are arguments. Symbols are converted to strings. Nested lists are flattened, which makes Racket lists of flags convenient to use.

The symbols '$target, '$deps, and '$< are expanded from the current recipe. '$deps is spliced into the command.

A non-zero command result raises an error.

For example:

(target "hello"
  (deps "hello.c")
  (run '(cc -Wall -O2 -o $target $<)))

Ordinary Racket values can be inserted with quasiquote:

(define CC 'cc)
(define CFLAGS '(-Wall -O2))
 
(target "hello"
  (deps "hello.c")
  (run `(,CC ,@CFLAGS -o $target $<)))

7 Running Racket tools🔗ℹ

procedure

(raco command)  void?

  command : list?
Runs a raco command using the raco executable that belongs to the Racket installation currently running the makefile. The helper first checks the console binary directory reported by the current Racket installation, then the directory containing the current racket executable, and only then falls back to PATH. This makes the helper useful on Windows installations where raco.exe is installed next to racket.exe but is not on %PATH%.

The command syntax is the same as for run, except that the executable is supplied automatically. Symbols, strings, paths and numbers are converted to command-line arguments, nested lists are flattened, and the recipe values '$target, '$deps, and '$< are supported. A non-zero result raises an error.

For example:

(phony setup test)
 
(target setup
  (raco '(setup racket-makefile)))
 
(target test
  (raco '(test -p racket-makefile)))

8 Cleanup helpers🔗ℹ

procedure

(rm-f path ...)  void?

  path : path-string?
Removes files when they exist. Missing files are ignored. Directories are not removed; use rm-rf for those.

procedure

(rm-rf path ...)  void?

  path : path-string?
Removes files or directory trees recursively. Missing paths are ignored.

procedure

(cleanup directory patterns)  void?

  directory : path-string?
  patterns : list?
Removes files below directory that match the supplied Racket glob patterns. Directories themselves are left in place. A pattern containing ** searches recursively.

For example:

(target clean
  (rm-rf "compiled")
  (cleanup "scrbl"
           '("**.html"
             "**.js"
             "**.css")))

9 Regexp directory helpers🔗ℹ

procedure

(list-dir/files directory    
  regexp    
  [#:recursive recursive])  list?
  directory : path-string?
  regexp : regexp?
  recursive : any/c = #f
Returns entries below directory that match regexp. By default only the direct contents of directory are inspected. With #:recursive #t, the complete tree is walked using in-directory, and both files and directories can be returned.

For a non-recursive listing, complete paths are built from directory, so the result can be passed directly to file operations such as rm-f and rm-rf.

procedure

(list-files directory    
  regexp    
  [#:recursive recursive])  list?
  directory : path-string?
  regexp : regexp?
  recursive : any/c = #f
Like list-dir/files, but keeps only paths for which file-exists? is true.

procedure

(list-dirs directory    
  regexp    
  [#:recursive recursive])  list?
  directory : path-string?
  regexp : regexp?
  recursive : any/c = #f
Like list-dir/files, but keeps only paths for which directory-exists? is true.

These helpers make regexp-based cleanup concise. For example:

(default-target all)
(phony all clean)
 
(target all
  (displayln "use (make clean)"))
 
(target clean
  (display "cleaning up...")
  (apply rm-rf
         (list-dirs "." #px"compiled$" #:recursive #t))
  (apply rm-f
         (list-files "." #px"(?i:([.]bak|~)$)" #:recursive #t))
  (displayln "done."))

The second regular expression is case-insensitive and matches names ending in .bak or ~.

10 Using ordinary Racket🔗ℹ

No separate make programming language is introduced. Definitions, functions, loops, conditionals, modules, and Racket libraries remain available. For example, targets can be generated in a loop:

(define sources '("foo.c" "bar.c" "baz.c"))
 
(define objects
  (for/list ([src sources])
    (define obj (path-replace-extension src #".o"))
    (target obj
      (deps src)
      (run `(cc -c $< -o $target)))
    obj))
 
(target all
  (deps objects))

The expression objects evaluates to a list of dependencies. Dependency lists are recursively flattened by racket-makefile.

11 Package example🔗ℹ

The optional package-zipper package combines naturally with racket-makefile:

(require package-zipper)
 
(default-target package)
(phony clean package)
 
(target clean
  (rm-rf "compiled")
  (cleanup "scrbl" '("**.html" "**.js" "**.css")))
 
(target package
  (zip-package))

The package target can then be executed explicitly, for example:

racket -t Makefile.rkt -e "(make package)"