377 lines
11 KiB
Racket
377 lines
11 KiB
Racket
#lang scribble/manual
|
|
|
|
@(require (for-label racket/base
|
|
racket/file
|
|
(only-in racket-makefile
|
|
target deps phony default-target make refresh-makefile
|
|
run raco rm-f rm-rf cleanup
|
|
list-dir/files list-files list-dirs
|
|
$target $deps $<)))
|
|
|
|
@title{racket-makefile}
|
|
@author{Hans Dijkema}
|
|
|
|
@defmodulelang[racket-makefile]
|
|
|
|
@tt{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 @racket[make].
|
|
|
|
@section{Installation}
|
|
|
|
@tt{racket-makefile} is available through the
|
|
@hyperlink["https://pkgs.racket-lang.org/"]{Racket Package Catalog}.
|
|
|
|
Install it with:
|
|
|
|
@verbatim{
|
|
raco pkg install racket-makefile
|
|
}
|
|
|
|
or using the @tt{DrRacket} package manager.
|
|
|
|
After installation a makefile can start with:
|
|
|
|
@verbatim{
|
|
#lang racket-makefile
|
|
}
|
|
|
|
If you want to see some example of a @tt{Makefile.rkt}}, you can look in the module code of @tt{racket-makefile}.
|
|
|
|
@section{A first makefile}
|
|
|
|
For example:
|
|
|
|
@racketblock[
|
|
(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 @tt{all} or
|
|
any other target.
|
|
|
|
@section{Executing targets}
|
|
|
|
@defform[(make name ...)]{
|
|
Builds the supplied targets. A bare identifier is always interpreted as a
|
|
literal target name, so @racket[(make clean)] selects the target named
|
|
@racket['clean] and does not require quoting.
|
|
|
|
With no arguments, @racket[(make)] builds the target selected by
|
|
@racket[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 @racket[(make clean all)]. Dependencies that are shared by multiple
|
|
selected targets are built only once during that @racket[make] invocation.
|
|
}
|
|
|
|
From the command line, load the makefile with @tt{-t} and evaluate a
|
|
@racket[make] form with @tt{-e}:
|
|
|
|
@verbatim{
|
|
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 @tt{-e} executes no target:
|
|
|
|
@verbatim{
|
|
racket -t Makefile.rkt
|
|
}
|
|
|
|
In DrRacket, open the makefile and press @bold{Run}. This loads and registers
|
|
the targets. Then execute targets in the Interactions window:
|
|
|
|
@verbatim{
|
|
> (make)
|
|
> (make clean)
|
|
> (make all)
|
|
> (make clean all)
|
|
}
|
|
|
|
After editing the makefile, reload its target definitions in the same Racket
|
|
process with:
|
|
|
|
@verbatim{
|
|
> (refresh-makefile)
|
|
> (make all)
|
|
}
|
|
|
|
@defproc[(refresh-makefile) void?]{
|
|
Reloads the most recently loaded @tt{racket-makefile} source file and registers
|
|
its targets again without restarting the Racket process. This works for both
|
|
@tt{#lang racket-makefile} and ordinary @tt{#lang racket} makefiles that require
|
|
@tt{main.rkt}. Modules required by the makefile that are already instantiated
|
|
are reused. This is useful for makefiles that require expensive libraries.
|
|
}
|
|
|
|
@section{Targets and dependencies}
|
|
|
|
@defform[(target name (deps dependency ...) body ...)]{
|
|
Defines a target. @racket[name] can be a symbol, string, path, or a Racket
|
|
expression producing one of those values. Each @racket[dependency] can also
|
|
produce a list; dependency lists are recursively flattened.
|
|
|
|
An unbound identifier is treated as a literal symbol. Consequently,
|
|
@racket[(target clean ...)] defines the target @racket['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 @racket[(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.
|
|
}
|
|
|
|
@defform[(deps dependency ...)]{
|
|
Specifies the dependencies of a target. Dependency expressions that produce
|
|
lists are automatically spliced into the dependency list. @racket[deps] is only
|
|
valid directly inside @racket[target].
|
|
}
|
|
|
|
@defform[(phony name ...)]{
|
|
Marks targets as phony. A phony target is always executed when requested or
|
|
when reached as a dependency.
|
|
}
|
|
|
|
@defform[(default-target name)]{
|
|
Selects the target used by @racket[(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.
|
|
|
|
@section{Recipe context}
|
|
|
|
Inside a target recipe the following identifiers are available:
|
|
|
|
@defidform[$target]{The current target name.}
|
|
|
|
@defidform[$deps]{A list containing all dependencies of the current target.}
|
|
|
|
@defidform[$<]{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
|
|
@racket[run].
|
|
|
|
@section{Running commands}
|
|
|
|
@defproc[(run [command list?]) void?]{
|
|
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 @racket['$target], @racket['$deps], and @racket['$<] are expanded
|
|
from the current recipe. @racket['$deps] is spliced into the command.
|
|
|
|
A non-zero command result raises an error.
|
|
}
|
|
|
|
For example:
|
|
|
|
@racketblock[
|
|
(target "hello"
|
|
(deps "hello.c")
|
|
(run '(cc -Wall -O2 -o $target $<)))
|
|
]
|
|
|
|
Ordinary Racket values can be inserted with quasiquote:
|
|
|
|
@racketblock[
|
|
(define CC 'cc)
|
|
(define CFLAGS '(-Wall -O2))
|
|
|
|
(target "hello"
|
|
(deps "hello.c")
|
|
(run `(,CC ,@CFLAGS -o $target $<)))
|
|
]
|
|
|
|
@section{Running Racket tools}
|
|
|
|
@defproc[(raco [command list?]) void?]{
|
|
Runs a @tt{raco} command using the @tt{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 @tt{racket} executable, and only then
|
|
falls back to @tt{PATH}. This makes the helper useful on Windows installations
|
|
where @tt{raco.exe} is installed next to @tt{racket.exe} but is not on
|
|
@tt{%PATH%}.
|
|
|
|
The command syntax is the same as for @racket[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
|
|
@racket['$target], @racket['$deps], and @racket['$<] are supported. A non-zero
|
|
result raises an error.
|
|
}
|
|
|
|
For example:
|
|
|
|
@racketblock[
|
|
(phony setup test)
|
|
|
|
(target setup
|
|
(raco '(setup racket-makefile)))
|
|
|
|
(target test
|
|
(raco '(test -p racket-makefile)))
|
|
]
|
|
|
|
@section{Cleanup helpers}
|
|
|
|
@defproc[(rm-f [path path-string?] ...) void?]{
|
|
Removes files when they exist. Missing files are ignored. Directories are not
|
|
removed; use @racket[rm-rf] for those.
|
|
}
|
|
|
|
@defproc[(rm-rf [path path-string?] ...) void?]{
|
|
Removes files or directory trees recursively. Missing paths are ignored.
|
|
}
|
|
|
|
@defproc[(cleanup [directory path-string?]
|
|
[patterns list?])
|
|
void?]{
|
|
Removes files below @racket[directory] that match the supplied Racket glob
|
|
patterns. Directories themselves are left in place. A pattern containing
|
|
@tt{**} searches recursively.
|
|
}
|
|
|
|
For example:
|
|
|
|
@racketblock[
|
|
(target clean
|
|
(rm-rf "compiled")
|
|
(cleanup "scrbl"
|
|
'("**.html"
|
|
"**.js"
|
|
"**.css")))
|
|
]
|
|
|
|
@section{Regexp directory helpers}
|
|
|
|
@defproc[(list-dir/files [directory path-string?]
|
|
[regexp regexp?]
|
|
[#:recursive recursive any/c #f])
|
|
list?]{
|
|
Returns entries below @racket[directory] whose file or directory name matches
|
|
@racket[regexp]. The regexp is applied to @racket[(file-name-from-path path)],
|
|
not to the complete path. By default only the direct contents of
|
|
@racket[directory] are inspected. With @racket[#:recursive #t], the complete
|
|
tree is walked using @racket[in-directory], and both files and directories can
|
|
be returned.
|
|
|
|
For a non-recursive listing, complete paths are built from @racket[directory],
|
|
so the result can be passed directly to file operations such as
|
|
@racket[rm-f] and @racket[rm-rf].
|
|
}
|
|
|
|
@defproc[(list-files [directory path-string?]
|
|
[regexp regexp?]
|
|
[#:recursive recursive any/c #f])
|
|
list?]{
|
|
Like @racket[list-dir/files], but keeps only paths for which
|
|
@racket[file-exists?] is true.
|
|
}
|
|
|
|
@defproc[(list-dirs [directory path-string?]
|
|
[regexp regexp?]
|
|
[#:recursive recursive any/c #f])
|
|
list?]{
|
|
Like @racket[list-dir/files], but keeps only paths for which
|
|
@racket[directory-exists?] is true.
|
|
}
|
|
|
|
These helpers make regexp-based cleanup concise. For example:
|
|
|
|
@racketblock[
|
|
(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
|
|
@tt{.bak} or @tt{~}.
|
|
|
|
@section{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:
|
|
|
|
@racketblock[
|
|
(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 @racket[objects] evaluates to a list of dependencies. Dependency
|
|
lists are recursively flattened by @tt{racket-makefile}.
|
|
|
|
@section{Package example}
|
|
|
|
The optional @hyperlink["https://docs.racket-lang.org/package-zipper/index.html"]{@tt{package-zipper}}
|
|
package combines naturally with @tt{racket-makefile}:
|
|
|
|
@racketblock[
|
|
(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:
|
|
|
|
@verbatim{
|
|
racket -t Makefile.rkt -e "(make package)"
|
|
}
|