On this page:
3.1 Creating Targets
3.2 Building Targets
3.3 Recording Results
3.4 Parallelism
3.5 Build API
target?
target-name
target-path
target-shell
input-file-target
input-data-target
target
rule
rule?
phony-rule
phony-rule?
token?
build
build/  dep
build/  no-dep
build/  command-line
build/  command-line*
find-target
make-at-dir
command-target?
command-target->target
file-sha256
sha256?
sha256-length
no-sha256
provide-targets
bounce-to-targets
make-targets
8.12

3 Zuo as a make Replacement🔗ℹ

The zuo/build module is reprovided by zuo.

The zuo/build library is modeled on make and Shake for tracking dependencies and build steps. The library has two layers:

A target represents either an input to a build (such as a source file) or a generated output, and a target can depend on any number of other targets. A target’s output is represented by a string that is normally an SHA-256 hash: more precisely, it is represented by a value satisfying the predicate sha256?. The build procedure records hashes and dependencies in a database located alongside non-input targets, so it can avoid rebuilding targets when nothing has changed since the last build. Unlike make, timestamps are used only as a shortcut to avoiding computing the SHA-256 of a file (i.e., if the timestamp has not changed, the SHA-256 result is assumed to be unchanged).

“Recursive make” is encouraged in the sense that a target’s build rule can call build to start a nested build, or it can call build/dep to build or register a dependency that is discovered in the process of building.

Here’s an example of a Zuo script to build "demo" by compiling and linking "main.c" and "helper.c":

#lang zuo
 
(provide-targets targets-at)
 
(define (targets-at at-dir vars)
  (define demo (at-dir (.exe "demo")))
 
  (define main.c (at-source "main.c"))
  (define main.o (at-dir (.c->.o "main.c")))
 
  (define helper.c (at-source "helper.c"))
  (define helper.o (at-dir (.c->.o "helper.c")))
 
  (make-targets
   `([:target ,demo (,main.o ,helper.o)
              ,(lambda (dest token)
                 (c-link dest (list main.o helper.o) vars))]
     [:target ,main.o (,main.c)
              ,(lambda (dest token)
                 (c-compile dest main.c vars))]
     [:target ,helper.o (,helper.c)
              ,(lambda (dest token)
                 (c-compile dest helper.c vars))]
     [:target clean ()
              ,(lambda (token)
                 (for-each rm* (list main.o helper.o demo)))])))

Although the make-targets function takes a makefile-like description of targets and dependencies, this script is still much more verbose than a Unix-specific makefile that performs the same task. Zuo is designed to support the kind of syntactic abstraction that could make this script compact, but the current implementation is aimed at build tasks that are larger and more complex. In those cases, it’s not just a matter of dispatching to external tools like a C compiler, and most Zuo code ends up in helper functions and libraries outside the make-targets form.

3.1 Creating Targets🔗ℹ

Construct a target with either input-file-target (given a filename), input-data-target (given a value whose ~s form is hashed), or target (given a filename for a real target or a symbol for a phony target).

Only a target created with target can have dependencies, but they are not specified when target is called, because computing dependencies for a target may involve work that can be skipped if the target isn’t needed. Instead, target takes a get-rule procedure that will be called if the dependencies are needed. The get-rule procedure returns up to three results in a rule record: a list of dependencies; the hash of an already-built version of the target, if one exists, where file-sha256 is used by default; and a rebuild procedure that is called if the returned hash, the hash of dependencies (rebuilt if needed), and recorded results from a previous build together determine that a rebuild is needed.

When a target’s rebuild function is called, it optionally returns a hash for the result of the build if the target’s rule has one, otherwise file-sha256 is used to get a result hash. Either way, it’s possible that the result hash is the same as the one returned by get-rule; that is, maybe a dependency of the target changed, but the change turned out not to affect the built result. In that case, rebuilding for other targets that depend on this one can be short-circuited.

Finally, in the process of building a target, a rebuild procedure may discover additional dependencies. A discovered dependency sent to build/dep is recorded as a dependency of the target in addition to the ones that were reported by get-deps. Any changes in these additional targets trigger a rebuild of the target in the future. Meanwhile, the build system assumes that if none of the dependencies change, then the set of additional dependencies discovered by rebuild would be the same; that assumption allows the build system to skip rebuild and its discoveries if none of the dependencies have changed.

A phony target is like a regular target, but one that always needs to be rebuilt. A typical use of a phony target is to give a name to a set of “top-level” targets or to implement an action along the lines of make install. Create a phony target with target and a symbol name.

A target can declare multiple outputs by specifying additional outputs in a 'co-outputs option. The target’s rebuild procedure will be called if any of the additional outputs are missing or not consistent with the result of an earlier build.

In many cases, a plain path string can be used as a target as a shorthand for applying input-file-target to the path string.

3.2 Building Targets🔗ℹ

There is no global list of targets that build draws from. Instead, build starts with a given target, and it learns about other targets as get-dep procedures return them and as rebuild procedures expose them via build/dep. If build discovers multiple non-input targets with the same filename, then it reports an error.

The build/command-line function is a convenience to implement make-like command-line handling for building targets. The build/command-line procedure takes a list of targets, and it calls build on one or more of them based on command-line arguments (with help from find-target).

All relative paths are considered relative to the start-time current directory. This convention works well for running a Zuo script that’s in a source directory while the current directory is the build directory, as long as the script references source files with at-source to make them relative to the script. For multi-directory builds, a good convention is for each directory to have a script that exports a targets-at procedure, where targets-at takes an at-dir procedure (supplied as just build-path by default) to apply to each target path when building a list of targets, and a hash table of variables (analogous to variables that a makefile might provide to another makefile via make arguments).

As a further convenience following the targets-at model, the provide-targets form takes an identifier for such a targets-at procedure, and it both exports targets-at and creates a main submodule that calls build/command-line* on with the targets-at procedure.

As a naming convention, consider using "main.zuo" in a directory where build results are intended to be written, but use "build.zuo" in a source directory that is intended to be (potentially) separate from the build directory. In other words, use "main.zuo" as a replacement for "Makefile" and "build.zuo" as a replacement for "Makefile.in" in a configure-style build. You may even have a configure script that generates a "main.zuo" script in a build directory so that zuo . is a replacement for make. The generated "main.zuo" could import the source directory’s "build.zuo" and calls build/command-line* on with the imported targets-at procedure plus at-source:

#lang zuo
(require "srcdir/build.zuo")
(build/command-line* targets-at at-source)

However, correctly encoding srcdir can be tricky when working from something like a shell configure script or batch file to generate "main.zuo". You may find it easier to write the path to a separate file using a shell-variable assignment syntax, and then have the generated "main.zuo" read from that file. The bounce-to-targets form implements that pattern. For example, if "Mf-config" is written in the same directory with a srcdir= line to specify the source directory (where no escapes are needed for the path after =), then a "main.zuo" of the form

#lang zuo
(bounce-to-targets "Mf-config" 'srcdir "build.zuo")

reads "Mf-config" to find and dispatch to "build.zuo" in the same way as the earlier example module.

3.3 Recording Results🔗ℹ

Build results are stored in a "_zuo.db" file in the same directory as a target (by default). Cached SHA-256 results with associated file timestamps are stored in a "_zuo_tc.db" in the same directory (i.e., the cached value for dependency is kept with the target, which is in a writable build space, while an input-file target might be in a read-only source space). A target’s options can specify an alternative directory to use for "_zuo.db" and "_zuo_tc.db". Timestamp recording in "_zuo_tc.db" is disabled if the SOURCE_DATE_EPOCH environment variable is set.

In the unfortunate case that a "_zuo.db" or "_zuo_tc.db" file gets mangled, then it may trigger an error that halts the build system, but the "_zuo.db" or "_zuo_tc.db" file will be deleted in reaction to the error. Another attempt at the build should recover, while perhaps rebuilding more than it would have otherwise, since the result of previous builds might have been lost.

Specify a location for the "_zuo.db" and "_zuo_tc.db" files associated with a target via the 'db-dir target option. The make-targets function recognizes as :db-dir clause to set the option for all of the targets that it creates.

3.4 Parallelism🔗ℹ

A build runs in a threading context, so a target’s get-deps or rebuild procedure can use thread-process-wait to wait on a process. Doing so can enable parallelism among targets, depending on the 'jobs option provided to build or build/command-line, a --jobs command-line argument parsed by build/command-line, a jobserver configuration as provided by GNU make and communicated through the MAKEFLAGS environment variable, or the ZUO_JOBS environment variable.

When calling build for a nested build from a target’s get-deps or rebuild procedures, supply the build token that is passed to get-deps to the build call. That way, parallelism configured for the enclosing build will be extended to the nested build.

3.5 Build API🔗ℹ

procedure

(target? v)  boolean?

  v : any/c
Returns #t if v is target, #f otherwise.

procedure

(target-name t)  (or/c symbol? path-string?)

  t : target?
Returns the name of a target, which is a path for most targets, but a symbol for an input-data target or a phony target.

procedure

(target-path t)  path-string?

  t : target?
The same as target-name for a target whose name is a path, and an error for other targets.

procedure

(target-shell t)  string?

  t : target?
Composes target-path with string->shell. Use this when getting a target name to include in a shell command.

procedure

(input-file-target path)  target?

  path : path-string?
Creates a target that represents an input file. An input-file target has no build procedure, and it’s state is summarized as a hash via file-sha256.

procedure

(input-data-target name content)  target?

  name : symbol?
  content : any/c
Similar to input-file-target for a would-be file that contains (~s content).

The result of (symbol->string name) must be distinct among all the input-data dependencies of a particular target, but it does not need to be globally unique.

procedure

(target name get-deps [options])  target?

  name : path-string?
  get-deps : (path-string? token? . -> . rule?)
  options : hash? = (hash)
(target name get-deps [options])  target?
  name : symbol?
  get-deps : (token? . -> . phony-rule?)
  options : hash? = (hash)
Creates a target that can have dependencies. If name is a path string, then it represents a file build target whose results are recorded to avoid rebuilding. If name is a symbol, then it represents a phony target that is always rebuilt.

In the case of a file target, get-deps receives name back, because that’s often more convenient for constructing a target when applying an at-dir function to create name.

The build token argument to get-deps represents the target build in progress. It’s useful with file-sha256 to take advantage of caching, with build/dep to report discovered targets, and with build/no-dep or build.

The following keys are recognized in options:

Changed in version 1.8: Added 'recur? for options.

procedure

(rule dependencies [rebuild sha256])  rule?

  dependencies : (listof (or/c target? path-string?))
  rebuild : (or/c (-> (or/c sha256? any/c)) #f) = #f
  sha256 : (or/c sha256? #f) = #f

procedure

(rule? v)  boolean?

  v : any/c
The rule procedure combines the three results expected from a procedure passed to target. See Creating Targets.

A path string can be reported as a dependency in dependencies, in which case it is coerced to a target using input-file-target. If sha256 is #f, file-sha256 is used to compute the target’s current hash, and rebuild is not expected to return a hash. If sha256 is not #f, then if rebuild is called, it must return a new hash.

procedure

(phony-rule dependencies rebuild)  phony-rule?

  dependencies : (listof (or/c target? path-string?))
  rebuild : (-> any/c)

procedure

(phony-rule? v)  boolean?

  v : any/c
The phony-rule procedure combines the two results expected from a procedure passed to target to create a phony target. Compared to the non-phony protocol, the result SHA-256 is omitted.

procedure

(token? v)  boolean?

  v : any/c
Returns #t if v is a token representing a target build, #f otherwise.

procedure

(build target [token options])  void?

  target : (or/c target? path-string? (listof (or/c target? path-string?)))
  token : (or/c #f token?) = #f
  options : hash? = (hash)
Builds target as a fresh build process, independent of any that might already be running (in the sense described below). A list of targets as target is coerced to a phony target that depends on the given list.

If target is a path, then it is coerced to target via input-file-target, but the only effect will be to compute the file’s SHA-256 or error if the file does not exist.

The options argument supplies build options, and the following keys are recognized:

If token is not #f, it must be a build token that was passed to a target’s get-deps to represent a build in progress (but paused to run this one). The new build process uses parallelism available within the in-progress build for the new build process.

Whether or not token is #f, the new build is independent of other builds in the sense that target results for others build are not reused for this one. That is, other builds and this one might check the states of some of the same files, but any triggered actions are separate, and phony targets are similarly triggered independently. Use build/dep or build/no-dep, instead, to recursively trigger targets within the same build.

Changed in version 1.1: Use maybe-jobserver-client if 'jobs is not set in options.
Changed in version 1.8: Added support for 'dry-run-mode in options.

procedure

(build/dep target token)  void?

  target : (or target? path-string?)
  token : token?
Like build, but continues a build in progress as represented by a token that was passed to a target’s get-deps or rebuild procedure. Targets reachable through target may have been built or have been in progress already, for example. After target is built, it is registered as a dependency of the target that received token (if the target is not phony).

procedure

(build/no-dep target token)  void?

  target : (or target? path-string?)
  token : token?
Like build/dep to continue a build in progress, but does not register a dependency. Using build/no-dep has an effect similar to Shake’s “order only” dependencies.

procedure

(build/command-line targets [options])  void?

  targets : (listof target?)
  options : hash? = (hash)
Parses command-line arguments to build one or more targets in targets, where the first one is built by default. The options argument is passed along to build, but may be adjusted via command-line flags such as --jobs, -n, or -q.

If options has a mapping for 'args, the value is used as the command-line arguments to parse instead of (hash-ref (runtime-env) 'args). If options has a mapping for 'usage, the value is used as the usage options string.

procedure

(build/command-line* targets-at    
  [at-dir    
  options])  void?
  targets-at : 
((path-string? ... . -> . path-string?) hash?
 . -> . (listof target?))
  at-dir : (path-string? ... . -> . path-string?)
   = (make-at-dir ".")
  options : hash? = (hash)
Adds a layer of target-variable parsing to build/command-line. Command-line arguments of the form name=value are parsed as variable assignments, where name is formed by a-z, A-Z, _, and 0-9, but not starting 0-9. These variables can appear anywhere in the command line and are removed from the argument list sent on to build/command-line, but no argument after a -- argument is parsed as a variable assignment.

The targets-at procedure is applied to at-dir and a hash table of variables, where each variable name is converted to a symbol and the value is left exactly as after =.

procedure

(find-target name targets [fail-k])  (or/c target? #f)

  name : string?
  targets : (listof target?)
  fail-k : (-> any/c) = (lambda () (error ....))
Finds the first target in targets that is a match for name, returning #f is not match is found. A name matches when it is the same as an entire symbol or path target name or when it matches a suffix that is preceded by / or \\. If no match is found, fail-k is called in tail position.

procedure

(make-at-dir path)  (path-string? ... . -> . path-string?)

  path : path-string?
Creates a function that is similar to one created by at-source, but relative to path.

procedure

(command-target? v)  boolean?

  v : any/c

procedure

(command-target->target target args)  target?

  target : command-target?
  args : list?
The command-target? predicate recognizes a target with the 'target? option, and command-target->target converts such a target to one where args are the argument when the target is built.

procedure

(file-sha256 file token)  sha256?

  file : path-string?
  token : (or/c token? #f)

procedure

(sha256? v)  boolean?

  v : any/c

value

sha256-length : integer? = 64

The file-sha256 procedure returns the SHA-256 hash of the content of file as a 64-character hexadecimal string (thus, sha256-length), or it returns no-sha256 if file does not exist.

The sha256? predicate recognizes no-sha256 and strings for which string-length returns either sha256-length or a multiple of sha256-length. The later case is used for multi-file targets, which concatenate the constituent SHA-256 strings.

See also string-sha256.

The empty string represents a non-existent target or one that needs to be rebuilt.

syntax

(provide-targets targets-at-id)

Provides targets-at-id as targets-at, and creates a main submodule that runs (build/command-line* targets-at-id). A script using provide-targets thus works as a makefile-like script or as an input to a larger build.

Changed in version 1.7: Removed build-path as a second argument to build/command-line* so that the default (make-at-dir ".") is used, instead.

syntax

(bounce-to-targets config-file-expr key-symbol-expr script-file-expr)

Chains to targets from (the path produced by) script-file-expr relative to the directory recorded in (the file whose path is produced by) config-file-expr using the key (produced by) key-symbol-expr, supplying the enclosing script’s directory as the target directory.

The path produced by config-file-expr is interpreted relative to the enclosing module. If the path in that file for key-symbol-expr is relative, it is treated relative to the config-file-expr path.

See Building Targets for an explanation of how bounce-to-targets is useful. The expansion of bounce-to-targets is roughly as follows:

(define config (config-file->hash (at-source config-file-expr)))
(define at-config-dir (make-at-dir (or (car (split-path config-file)) ".")))
(define script-file (at-config-dir (hash-ref config key-symbol-expr)
                                   script-file-expr))
(build/command-line* (dynamic-require script-file 'targets-at)
                     at-source)

procedure

(make-targets specs)  (listof target?)

  specs : list?
Converts a make-like specification into a list of targets for use with build. In this make-like specification, extra dependencies can be listed separately from a build rule, and dependencies can be written in terms of paths instead of target objects.

Although it might seem natural for this make-like specification to be provided as a syntactic form, typical makefiles use patterns and variables to generate sets of rules. In Zuo, map and similar are available for generating sets of rules. So, make-targets takes an S-expression representation of the declaration as specs, and plain old quasiquote and splicing can be used to construct specs.

The specs argument is a list of lines, where each line has one of the following shapes:

`[:target ,path (,dep-path-or-target ...) ,build-proc ,option ...]
`[:depend ,path (,dep-path-or-target ...)]
`[:target (,path ...) (,dep-path-or-target ...) ,build-proc ,option ...]
`[:depend (,path ...) (,dep-path-or-target ...)]
`[:db-dir ,path]

A ':target line defines a build rule that is implemented by build-proc, while a ':depend line adds extra dependencies for a path that also has a ':target line. A ':depend line with multiple paths is the same as a sequence of ':depend lines with the same dep-path-or-target list, but a ':target line with multiple paths creates a single target that builds all of the paths.

In ':target and ':depend lines, a path is normally a path string, but it can be a symbol for a phony target. When a ':target has multiple paths, they must all be path strings.

A build-proc accepts a path (if not phony) and a build token, just like a get-deps procedure for target, but build-proc should build the target like the rebuild procedure for rule (or phony-rule). When a ':target line has multiple paths, only the first one is passed to the build-proc.

A dep-path-or-target is normally a path string. If it is the same path as the path of a ':target line, then a dependency is established on that target. If dep-path-or-target is any other path string, it is coerced to an input-file target. A dep-path-or-target can also be a target that is created outside the make-targets call.

An option can be ':precious, ':command, ':noisy, ':quiet, ':eager, or ':recur to set the corresponding option (see target) in a target.

A ':db-dir line (appearing at most once) specifies where build information should be recorded for all targets. Otherwise, the build result for each target is stored in the target’s directory.

Changed in version 1.8: Added ':recur for option.