shape(1)

πŸ‘‘ shape(1) β€” the ShapeTools era ✨

okay bestie, buckle up, we're doing shape(1), from the ShapeTools manual set. yes the manual name goes here, it's shape(1), and it's giving build system main character energy πŸ’…


🌸 NAME 🌸

shape β€” identify and build program configurations from versions of source objects 🫢


πŸŽ€ SYNOPSIS πŸŽ€

shape
     [ -f <description file> ]
     [ -R <version selection rule> ] [ -V <variant name> ]
     [ -echo <macro name> ] [ -force <target> ] [ -rebuild <target> ]
     [ - dDehiknprs ]
     [ -bct ] [ -help ] [ -nomsg ] [ -novclass ] [ -version ] [ -xpoff ]
     [ -xpon ]
     [ target1 target2 ... ] [ macro=value ... ] [ macro+=value ... ]

like, that's the whole vibe up front. no cap. ✨


πŸ¦‹ DESCRIPTION πŸ¦‹

so shape lets you transparently compile source objects β€” and those source objects can be either regular files, or source object versions living in the ShapeTools version object base. honestly iconic.

more generally, shape produces a set of derived objects (aka targets) from appropriately selected versions of corresponding source objects, according to a description of the dependencies between the objects. and it keeps track of the relevant parameters for compilations β€” source versions, compiler versions, compiler switches, etc. β€” which is how it delivers a safe and efficient build machinery. the way that it just remembers things for you πŸ˜­πŸ’–

when shape compiles source objects, it stores the resulting derived objects together with the effective compile parameters in its derived object cache. and before the derivation process for a requested object even starts, shape attempts to find an existing derived object that matches the requirements for the target, in the derived object cache. caching and restoring objects that are derived from immutable versions lets developers profit from previous builds by other team members. overall compile costs in projects are substantially reduced. slay.

when serving a build request, shape considers a possibly large number of versions that are stored for an object. which particular version gets bound to an object's name in the description file is determined by version selection rules. ✨

shape can manage builds of different variants of a system in parallel. it uses the combined potential of dynamic version selection, dynamic macro redefinition, and derived object management to handle variant builds. and since most β€” if not all β€” of the objects and parameters involved in a build are defined as macros, shape provides a great deal of flexibility by allowing you to alter some or all of the macros dynamically, depending on which variant shall be built. the concept of variant definition in shape's description file offers a clear focal point for all definitions that are relevant for a certain variant. periodt. πŸ’…


πŸ’– OPTIONS πŸ’–

  • -f <description file> β€” shape uses the argument you hand it as the name of the description file to be used for the build. if no -f option is specified, shape tries to find a description file under one of the names Shapefile, shapefile, Makefile, and makefile (checking from left to right πŸ‘’). if no regular file with one of these names can be found, but versions of respective files are available in the version object base, shape will use the most recent version. when more than one -f <description file> argument pair appears, shape reads each description file in turn. and if the name of the description file is specified as -, shape will read the system description from standard input. you can also specify the description file in bound version notation, e.g. Shapefile[2.8] or Shapefile[Release4] (see vbind(1) for details about bound version notation). πŸŽ€
  • -R <selection rule name> β€” activates the specified selection rule as the initial version binding for source objects. if the -R option is present, and a selection rule is defined as the first dependency of the first target, shape will use the selection rule passed via the command line, and ignore the first (and only the first) selection rule activation within the description file. this option is useful to override initial default selection rules, specified within the description file, from the command line. override queen πŸ‘‘
  • -V <variant name> β€” activates the variant specified by <variant name>. several variants can be activated simultaneously from the command line by specifying the -V option multiple times. all variant specific definitions will be in effect as soon as shape reads the corresponding variant definition in the description file. stack 'em up bestie πŸ’œ
  • -force <target> β€” forces shape to build the specified target unconditionally, i.e. even if a suitable, previously build object exists. no chill, just build πŸ”₯
  • -echo <macro name> β€” the value of the macro <macro name> is written to standard output. this option is useful to extract information from the system description file (e.g. shape -echo SOURCES, or shape -echo SUBSYSTEMS), or to control the effect of variant activations. peek behind the curtain ✨
  • -rebuild <target> β€” attempt a precise rebuild of target according to a bound configuration thread, supplied in a file named <target>.bct (see description of the -bct switch).
  • -d β€” run shape in debug mode. print out detailed information about object dependencies and attributes. πŸ›
  • -D β€” print detailed information about the version binding process, and shape's reasoning regarding (re)builds of targets, or retrievals from the derived object cache. this switch is useful to find out about the exact reasons why shape rederives a target (or not). it's giving explain yourself πŸ’€
  • -e β€” macro definitions that are imported from the environment (see description of the special macro IMPORT, below) override macro definitions in the description file. by default, macro definitions in the description file have precedence over imports from the environment. flip the hierarchy, we love a plot twist πŸŒ€
  • -h β€” print usage information on standard output (this is an abbreviation for the -help switch, see below).
  • -i β€” ignore error codes returned by commands. πŸ™ˆ
  • -k β€” when a nonzero error status is returned by an invoked command, the work on the current target is abandoned but shape continues with other branches that do not depend on the failed target. keep going bestie, we don't stop for anyone 🫢
  • -n β€” no execution mode. shape prints out commands, but does not execute them. even command lines beginning with @ are printed. if a command contains the $(MAKE) macro reference, however, that line is always executed in order to allow tracing of recursive build processes. fake it till you make it (but not for $(MAKE)).
  • -p β€” print out the complete set of macro definitions, target descriptions, and rule definitions, respectively.
  • -r β€” do not use shape's built-in implicit rules. implicit rules defined in the description file remain in effect. homegrown only 🌱
  • -s β€” run in silent mode. shape does not print out the commands before executing them. shhh 🀫
  • -bct β€” record the build in a bound configuration thread file. a shape configuration thread contains precise definitions of all source versions, their dependencies, the involved tools, and related options that were in effect for a build. the configuration thread for a produced toplevel target (the first target in the description file, or a target requested from the command line) is stored in a file named <target>.bct. bound configuration threads can be used as input for rebuilds (see option -rebuild, above). if the source version context of a bct-build is unsafe, shape will record that fact in the bct, and issue a warning message. ⚠️ receipts for everything, fr fr
  • -help β€” print usage information on standard output.
  • -nomsg β€” turn off the trace facility msg in version selection rules. πŸ“΅
  • -novclass β€” disable checking for incompatibility of activated variants. living dangerously, we love that for you 😭
  • -version β€” print the version identification of the shape program.
  • -xpoff β€” turn off attribute expansion in source versions retrieved from the object base. by default, attribute expansion is turned on for all source objects that are directly retrieved from the object base, and turned off for source objects that are regular files (see retrv(1) for details about attribute expansion).
  • -xpon β€” turn on attribute expansion for all source objects, even in regular files. by default, attribute expansion is turned off for source objects that are regular files, and turned on for all source objects that are directly retrieved from the object base. everything everywhere all at once ✨
  • target ... β€” a list of target names can be passed to shape via the command line. if no target is given on the command line, and the special target .DEFAULT is not defined within the description file, shape tries to produce the first target defined in the description file. no notes πŸ‘‘
  • <macro definition> β€” it is possible to define or modify macros in the description file from the command line. macros that are defined this way take precedence over all other definitions. command line macro definitions have either of two forms:
NAME=VALUE

and

NAME+=VALUE

with NAME being a word and VALUE an arbitrary string. if VALUE contains white space, make sure to quote it. the first form of command line macro definitions sets NAME to the substitution VALUE. if VALUE is empty, the macro is reset. the second form appends VALUE with a leading space character to the current substitution of NAME. the current substitution may be defined in the description file, or by a previous setting on the command line. for details about the semantics of macro definitions and substitutions, see the respective sections below. πŸ“š


okay that's the first chunk done β€” more parts are coming, don't go anywhere bestie πŸ’…βœ¨

✨ DESCRIPTION FILES ✨

okay so bestie, the way shape works is it's controlled by a system description file (usually a Makefile) that gives it the structural tea about the system you're managing. But unlike make(1), shape doesn't just vibe with plain files β€” it works on top of AtFS (Attributed File System), which is a whole repository of versioned objects. So genuine shape description files (usually called a Shapefile) have version selection rules and variant definitions on top of the standard Makefile dependency rules. It's giving upgraded. πŸ’…

Shape's description file is an upward compatible extension of make(1)'s description file, the Makefile. A super useful structuring convention is to keep a Makefile and a Shapefile in parallel. Only the genuine shape constructs (like version selection rules or variant definitions) go in Shapefile, while the bulk of target rule- and macro definitions stay in Makefile. And the Makefile shall be included in Shapefile (peep the description of the include directive below). This convention is iconic because programs developed with the ShapeTools system can be shipped as source distribution to sites that don't use ShapeTools. No cap. 🧚

Although shape is largely downward compatible with the original make program, it should be noted that several popular extensions of the original make program β€” like GNU Make or Sun Make β€” provide features not present in shape. See the section on known incompatibilities below. πŸ‘€

The description file is literally the ideal central information base for all sorts of product related definitions. Shape encourages you to develop a set of (project- or organization-specific) conventions for system description, and gives you a simple way to extract this info for other tools (see the -echo option, above). The description file syntax doesn't just specify component dependencies relevant for build processes β€” it allows a general, hierarchical definition of product oriented tasks. The concept of recursive dependencies maps directly to a stepwise refinement of task definitions. Such tasks can be fully, partly, or not at all automated as appropriate. So certain activities may be automated and standardized, while other activities are just informally described to document them or reason about them (see shape_rms(1) for examples). Periodt. πŸ’–


πŸ’« Syntactical Structure πŸ’«

The basic syntactical structure of shape's description file is made up of:

Comments

Comments begin with a # character and extend to the end of the line. In Shapefiles, the end of a line is defined as an unescaped newline (\<newline>), or the end of the file. The comment character can't be escaped, but can be quoted in single- or double quotes. Comment characters in command lines of target rules are ignored by shape. So don't stress. 😌

Directives

Directives are special keywords, known to shape. Directives begin at column 0 of a line and extend to the line end. Currently, the only directive recognized by shape is:

include <list of file names>

Macro Definitions

Macro definitions have the general form:

NAME <macro definition symbol> VALUE

NAME must be a single word consisting of a sequence of name characters. Name characters are all printable characters except the following:

$ # : = ; <space> \t \n

The macro definition symbol is either of =, +=, or :=. VALUE is an arbitrary string terminated by the end of the line, or a comment. Macro definitions usually begin in the first column of a line, but may be preceded by leading <space> characters. Macro definitions must not contain leading <tab> characters (see section on Macro Definitions, below, for more details). πŸ‘‘

Macro References

Macro references have one of the following forms:

$(<macro name>)
${<macro name>}
$<single character name>

The macro substitution operator ($) can't be escaped, but can be represented by the substitution $$. Macro substitution occurs anywhere in the description file, except in comments, macro names, left hand sides of version selection rule- and variant definition headers (see next section), and variant class definitions (see section on Macro Substitutions, below, for more details). Slay. πŸ’…

Rules

Rules are made up from a rule header, and an optional rule body. The rule header consists of a left hand side, a rule definition symbol, and an optional right hand side. The left hand side usually begins in column 0 of a line, and may be preceded by leading <space> characters. Left hand sides of rule headers must not contain leading <tab> characters. The optional right hand side of a rule header extends to the end of the line, or the beginning of the rule body. A rule body consists of consecutive lines beginning with a <tab> character. The body of a rule is terminated by the next line not beginning with a <tab> character, or the end of the file.

Shape recognizes three different kinds of rules, distinguished by their respective rule definition symbols:

  • target rules. Target rules have a single colon character (:) as rule definition symbol. The left hand side of target rule headers is a space-separated list of names. The optional right hand side consists of a space-separated list of names, followed by an optional list of production ingredients (see section on Target Rules, below).
  • version selection rules. Version selection rules have the rule definition symbol :-. The rule header of version selection rules has a single word on its left hand side, and no right hand side (see section on Version Selection Rules, below).
  • variant definitions. Although variant definitions are β€” as the name suggests β€” definitions, not rules (from a semantical view point), their syntactical representation is that of a rule. Variant definitions have the rule definition symbol :+. The rule header of a variant definition has a single word on its left hand side, and no right hand side (see section on Variant Definitions, below).

Variant Class Definitions

Variant class definitions have the form:

vclass <name> ::= (variant1, variant2, ...)

(see section on Variants, below). πŸ¦‹

Line Continuations

If the end of an input line is escaped by a backslash (\) the next line is considered as a continuation line. The backslash newline character sequence is replaced by a space. So you can keep your lines cute and short. ✨


🌸 Macro Definitions 🌸

Macro definitions associate names with strings that will be substituted wherever the name of the macro is referenced (see next section). Macros are useful for writing maintainable, and somewhat generic description files. Even moderately large projects will find it extremely rewarding to define conventions for naming and usage of certain macros throughout the product description file. Trust. πŸ’–

There are three different kinds of macro definitions:

Simple Macro Definitions

A simple macro definition looks like:

NAME = <any string>

The string that is associated with the macro name can contain macro references. If a macro is defined multiple times within a description file, the last definition will be effective. Macros defined on the command line take precedence over definitions of the same macro in the description file. So command line is the main character. πŸ‘‘

Additive Macro Definitions

This type of macro definition looks like:

NAME += <any string>

The string on the right hand side of the definition is appended to any existing value associated with NAME, separated by a space character. Multiple additive macro definitions are concatenated in the order in which they appear in the description file. If an additive macro definition occurs on the command line, the last string value defined in the description file is prepended to the string value defined on the command line. Additive macro definitions in the description file are appended to string values defined on the command line. So it's all about the order, bestie. 🫢

Evaluative Macro Definitions

Evaluative macros are defined in the following way:

NAME := <any string>

First, the string value is associated to NAME in the same way as for simple macro definitions. When NAME is substituted for the first time, the right hand side of the definition is evaluated, and the result of this evaluation replaces the original string value associated with NAME. Thus, evaluation of the right hand side occurs exactly once. This is particularly useful if the defining string is a command substitution (see next section). One and done, literally. πŸ’…

✨ Macro Substitutions ✨

okay bestie, so macro substitution is literally the glow-up where a macro reference gets swapped out for the string value tied to a macro name. πŸ’… if you reference a macro that's undefined? it just becomes an empty string, no drama. macro references can look like either of these:

$(NAME)
${NAME}
$<any single character>

valid macro refs include:

$(CFLAGS)
$7
${SOURCE-FILES}
$(X)
$X

the last two refs? identical substitutions, periodt. and $$ will substitute a single dollar sign. πŸ’Έ

Before a macro reference is substituted, the associated string gets evaluated. Evaluation of a string value includes:

  • substitution of all macro references in the string value
  • command substitution. Any substring of the string value enclosed in backquotes will be passed as command to the shell, and be replaced by the command's standard output.
  • string substitution. If a macro reference has the form $(NAME:<old>=<new>), the reference will be substituted by the evaluated value of NAME, with all occurrences of the string <old> replaced by the string <new>. This is particularly useful to maintain related lists, such as CSOURCES and OBJECTS for example, automatically:
CSOURCES := `echo *.c`
OBJECTS := $(CSOURCES:.c=.o)

Shape substitutes macro references as late as possible. Macro references occurring in a macro definition are only substituted when the defined macro itself is substituted. Macro references on the dependencies side of target rules are substituted when the rule is evaluated. Macro references on the target side of target rules are substituted immediately after shape has read the description file, i.e. before any production is started. Macro references in include directives are substituted when the directive is executed while shape reads the description file.


πŸ‘‘ Built-in and Special Purpose Macros πŸ‘‘

In order to provide parametrization of shape's built-in implicit rules, a number of predefined macros is supplied by convention. These macros have meaningful initial values that can be altered by the user. There are also several macros that have special meaning for shape.

Here's the full roster, bestie. every single one. πŸ’–

  • @ β€” full name of the current target. Initial value: <dynamic>. Remark: special.
  • ? β€” list of target dependencies. Initial value: <dynamic>. Remark: special.
  • < β€” name of the first target dependency. Initial value: <dynamic>. Remark: special.
  • * β€” prefix shared by target and the dependent filenames. Initial value: <dynamic>. Remark: special.
  • # β€” bound version id of the current dependency. Initial value: <dynamic>. Remark: special.
  • $ β€” the character $. Initial value: $. Remark: special.
  • + β€” name of object to be bound to a version (selection rules only!). Initial value: <dynamic>. Remark: special.
  • AS β€” Program for doing assembly. Initial value: as. Remark: conventional.
  • ASFLAGS β€” Flags for the assembler. Initial value: <none>. Remark: conventional.
  • CC β€” Program for compiling C programs. Initial value: cc. Remark: conventional.
  • CFLAGS β€” Flags for the C compiler. Initial value: <none>. Remark: conventional.
  • FC β€” Program for compiling Fortran programs. Initial value: f77. Remark: conventional.
  • FFLAGS β€” Flags for the Fortran compiler. Initial value: <none>. Remark: conventional.
  • HOSTTYPE β€” Host architecture of the computer that runs shape. The value of this macro is used by shape to construct the derivation key attribute for derived objects. Initial value: <none>. Remark: special.
  • IMPORT β€” List of environment variables that shall be imported as macro definitions. Initial value: <none>. Remark: special.
  • LD β€” Program to link programs. Initial value: ld. Remark: conventional.
  • LDFLAGS β€” Flags for the linker. Initial value: <none>. Remark: conventional.
  • LEX β€” Program to turn Lex grammars into C or Ratfor programs. Initial value: lex. Remark: conventional.
  • LFLAGS β€” Flags for the lexical analyzer lex. Initial value: <none>. Remark: conventional.
  • LOGNAME β€” The name or network-id under which the user who owns the shape process is logged on. Initial value: <dynamic>. Remark: special.
  • M2C β€” Program for compiling Modula2 programs. Initial value: m2c. Remark: conventional.
  • M2FLAGS β€” Flags for the Modula2 compiler. Initial value: <none>. Remark: conventional.
  • MAKE β€” The command line with which shape has been invoked. This macro is used for recursive calls to shape. Initial value: shape $(MAKEFLAGS). Remark: special.
  • MAKEFLAGS β€” Command line flags relevant for recursive calls to shape. Initial value: <defined from command line>. Remark: special.
  • PC β€” Program for compiling Pascal programs. Initial value: pc. Remark: conventional.
  • PFLAGS β€” Flags for the Pascal compiler. Initial value: <none>. Remark: conventional.
  • RFLAGS β€” Flags for the Fortran compiler for Ratfor programs. Initial value: <none>. Remark: conventional.
  • SHAPEPID β€” The process id of the running shape program. Initial value: <dynamic>. Remark: special.
  • SHAPEVERSION β€” The version id of the shape program. Initial value: shape_CM-4.4 (or above). Remark: special.
  • SHELL β€” The command processor for the target rule command lines. The referenced command processor must be able to take its commands from standard input (see section on Command execution, below). Initial value: /bin/sh. Remark: special.
  • VPATH β€” Search path extension for localizing source components. Initial value: <none>. Remark: special.
  • YACC β€” Program to turn Yacc grammars into C programs. Initial value: yacc. Remark: conventional.
  • YFLAGS β€” Flags for yacc. Initial value: <none>. Remark: conventional.
  • vpath β€” Dynamic search path extension for variants of source components. Initial value: <none>. Remark: special.

The function of the special purpose macros HOSTTYPE, IMPORT, MAKE, VPATH, and vpath are described in the sections on OPERATION, and Variants below.

🎯 TARGET RULES 🎯

Okay bestie, this is where the actual magic happens. A target rule is how you tell shape how β€” and under what conditions β€” a target gets derived from a set of source objects and/or other targets. And real talk: a target is just a name that can refer to a file but doesn't have to. So don't come for it. πŸ’…

The format goes like this:

<target>... : [<version binding>] [+<variant>...] [<dependency>...] \
        [: <ingredient>...] [; <command>]
        \t[<command>]
        ...

The header of a target rule (peep Syntactical Structure above) is a list of targets, terminated by a colon, followed by an optional list of dependencies, and then an optional list of production ingredients that begins after a second colon character. The header itself is terminated by a newline or a semicolon, and then comes the optional rule body.

The rule body = the command lines that get executed when a target needs to be rederived. The first command line can come immediately after the semicolon that ends the header. Every command line after that has to begin with a <tab> character. The body is terminated by the first line that doesn't begin with a <tab>, or by the end of the file. That's the whole deal. ✨

πŸŽ€ Targets

When multiple targets show up on the left hand side of a rule header and the derivation process needs to start, shape derives all of the targets in a single run. Efficiency queen behavior, honestly. πŸ‘‘

πŸ¦‹ Dependencies

Shape checks a target's dependencies from left to right, like scrolling a group chat from the top.

First: is that first dependency the name of a version selection rule? If it is, shape sets that selection rule active (eclipsing all previous selection rule activations, no takebacksies), then moves on to the next dependency.

Next: is the dependency a variant activation? If it starts with a + character followed by the name of a variant, that variant gets activated (see the Variants section below). Shape keeps checking for variant activations until it hits the first dependency that isn't one.

Then shape works through the rest of the list, binding (or deriving) each one as needed, doing a depth first traversal of the dependency graph (see the section on OPERATION, below).

πŸ“ Production Ingredients

Once all the dependencies have been bound, shape constructs the derivation key for the target. Think of it as the receipt 🧾 β€” an attribute defining the complete set of parameters that determine whether a target needs to be rebuilt.

Besides all the bound dependencies, the derivation key contains the production ingredients specified in the rule header. Production ingredients are typically complete definitions of the macros referenced in the command lines of the rule's body. So tool versions and switches that affect how a tool operates can become part of a target's derivation parameters. Chef's kiss. πŸ˜šπŸ‘Œ

To include macro definitions in a target's derivation key, this special reference

+(NAME1) +(NAME2) ...

has to appear in place of the production ingredients.

πŸ’… Command Lines

When shape concludes a target needs to be (re-)derived, the commands in the target rule body get executed. The body is consecutive lines, each treated as a separate command. Each command line is evaluated exactly like the Macro Substitution section describes, then passed to the command interpreter defined by the SHELL macro. Every command line runs as a separate process.

If you need complex commands that don't fit on a single line, or you wanna dodge the overhead of repeated process invocations, you can stretch a logical command line by escaping the newline with a backslash character (\<newline>) and continuing it on the next physical line. Slay. 🐍

Command lines may be preceded by one or two special characters:

  • - β€” shape ignores any nonzero error code returned by a command line whose first character is a minus sign. The minus sign is not passed to the shell. When a command returns a nonzero return status, shape usually treats the derivation process for the target as a failure and terminates β€” unless the -i or -k switches are in effect, or the .IGNORE special target is in effect.
  • @ β€” if the first character of a command is a @, shape doesn't print the command before executing it. The @ is not passed to the shell.
  • @- β€” if the first two non-<tab> characters are @-, shape ignores nonzero return codes and suppresses printing of the command line. Both at once, iconic.

If shape is invoked in no execution mode (-n), the evaluated command lines get printed on standard output, showing what shape would do if invoked without -n. But heads up β€” command lines containing the macro reference $(MAKE) are always executed, even if -n is set. That's on purpose, so you can simulate recursive builds that might span over subdirectories. The reference $(MAKE) gets substituted by a shape command invocation with all the relevant command line switches set.

Within command lines of the rule body, some parts of the target rule header can be dynamically referenced. When a command line is evaluated, these substitutions are possible:

  • $@ β€” full name of the current target
  • $? β€” list of dependencies
  • $< β€” name of the first dependency
  • $* β€” prefix shared by current and the dependent filenames
  • $# β€” bound version id of the current dependency (implicit rules only)

🌈 IMPLICIT RULES 🌈

Shape's target rules come in two different flavors: explicit and implicit. Implicit rules are basically templates β€” they define dependency patterns that apply to most targets of a given kind. Which is why implicit rules are sometimes called pattern rules. Shape converts make's old-style implicit rules (like .c.o:) into pattern rules while it reads the description file.

A typical dependency pattern? Files containing linkable object code β€” like module.o β€” depending on corresponding files containing source code, like module.c. The derivation process for most of these source/derived pairs is identical. So instead of writing separate rules for every single source/derived dependency in a system, you write one single generic rule, called an implicit rule. That's the move. 🧚

An implicit rule has this format:

%[.<suff1>] %[.<suff2>] ... : %[.<suff3>] %[.<suff4>]... \
    [: <ingredient>...] [; <command>]
    \t[<command>]
    \t[<command>]
    ...

The structure is the same as described above, except the names of targets and dependencies are replaced by target patterns and dependency templates.

The percent character in a target pattern acts as a wildcard matched against all of a target's name, up to the optional trailing suffix. For shape, an object name suffix is the sequence of characters enclosed by the last period character (.) within the name, and the <space> character terminating the name. Here's an example of shape's suffix concept in action:

  • sample.cde β†’ suffix: cde
  • sample.x.y.cc β†’ suffix: (none)
  • sample_c β†’ suffix: (none)
  • .sample.c β†’ suffix: c

And here's an example implicit rule that derives linkable object code from corresponding C source files:

%.o : %.c : +(CC) +(CFLAGS)
    \t@echo shape - executing: $(CC) -c $(CFLAGS) $#;
    \t@$(CC) $(CFLAGS) -E %.c |
      sed 's;^\(# [0-9][0-9]* \"\)%.c\(\".*\)$$;e1$#\2;' > %.i;
    \t@$(CC) -c $(CFLAGS) %.i;
    \t@rm %.i;
NOTE: This rule is shape's built-in implicit rule to compile C source files. The cryptic command sequence has the purpose to encode the complete file version id into the object code (e.g. sample.c[3.4] rather than sample.c). This is extremely useful in conjunction with with the -g switch of most C compilers, and version sensitive debuggers, such as vgdb.

If a target is derived using implicit rules, the name of the target is matched against the target patterns of the implicit rules. If a rule matches, the matching portion of the target name (the stem, referred to by the %) is consistently substituted for all other occurrences of the wildcard character throughout the rule. Once that's done, the implicit rule is treated like an explicit target rule. One rule, infinite glow-ups. ✨

πŸ‘‘ EXPLICIT RULES πŸ‘‘

Explicit rules associate explicit target names with explicit dependencies. They're most typically used to specify dependencies that implicit rules can't cover β€” like deriving an executable program by linking a whole bunch of object code modules together.

A lot of the time, explicit rules are used to specify only those target dependencies that aren't implied by an implicit rule (include dependencies for object files, for example), while the "natural" dependencies are just assumed to be present. And if a description file contains only that sort of explicit dependencies, shape automatically adds the omitted implicit dependencies (and an applicable rule body) to the total list of target dependencies. It's giving autocomplete. 🫢

πŸ’– BUILT-IN IMPLICIT RULES πŸ’–

Shape provides a whole roster of predefined implicit target rules covering many common source/target derivations. Here are the target patterns and dependency templates for shape's built-in implicit rules:

  • %.a from %.c β€” Compile archive library from C source
  • %.c from %.l β€” Generate C programs from Lex grammar
  • %.c from %.y β€” Generate C programs from Yacc grammar
  • %.o from %.l β€” Compile object code from Lex grammar
  • %.o from %.y β€” Compile object code from Yacc grammar
  • %.o from %.s β€” Translate assembler program to object code
  • %.o from %.r β€” Compile Ratfor source
  • %.o from %.F β€” Compile Fortran source
  • %.o from %.f β€” Compile Fortran source
  • %.sym from %.def β€” Compile Modula definition modules
  • %.o from %.mod β€” Compile Modula implementation modules
  • %.o from %.p β€” Compile Pascal source
  • %.o from %.c β€” Compile C source
  • % from %.sh β€” Make executable program from shell-script
  • % from %.r β€” Build executable program from Ratfor source
  • % from %.F β€” Build executable program from Fortran source
  • % from %.f β€” Build executable program from Fortran source
  • % from %.p β€” Build executable program from Pascal source
  • % from %.mod β€” Build executable program from Modula source
  • % from %.c β€” Build executable program from C source

For a complete definition of shape's built-in implicit rules, run shape -p. Go look, it's a whole vibe. πŸ”₯

✨ Special Purpose Targets ✨

okay so listen, shape has a whole lil squad of special purpose targets that you drop into the description file to control how she operates πŸ‘‘ by convention they all start with a period character, and they have no associated commands. no cap.

  • .DEFAULT β€” the commands in the rule body of the .DEFAULT target rule get executed for all targets that cannot be derived by explicit or implicit target rules. and if you want no commands at all to run for a rule but you still need .DEFAULT for other targets? give that rule an empty command β€” either a ; at the end of the rule header, or an empty line beginning with <tab>. also: if .DEFAULT has dependencies, and no targets are requested from the command line, those dependencies are treated as if they were targets requested from the command line. πŸ’…
  • .IGNORE β€” makes shape ignore non zero return codes of invoked commands. literally equivalent to the -i switch 🫢
  • .SILENT β€” silent command execution, bestie. the command lines are not printed before execution. equivalent to the -s switch 🀫
  • .BPOOL β€” only the dependencies associated with this target are stored in the derived object cache ✨
  • .NOBPOOL β€” dependencies associated with this target are not stored in the derived object cache. periodt.
πŸ’€ heads up: if both .BPOOL and .NOBPOOL are defined, only the difference set of both dependency lists will be stored in the derived object cache. iykyk.

🌈 Version Selection Rules 🌈

when shape builds a target, she uses version selection rules to bind a unique version to each name of the prerequisite source objects. a version selection rule is a name plus an associated set of predicate lists living in the rule body. the format is:

        <name> [( <arg1>, <arg2>,...)] :-
            \t[<pattern1>,] <pred1> (...), <pred2> (...);
            \t[<pattern2>,] <pred1> (...), <pred2> (...);
            ...
            \t.

so the body of a version selection rule is a sequence of alternatives, separated by semicolons. each alternative is an optional pattern, followed by a comma-separated list of predicates. the selection rule is terminated by a period character. and here's the vibe check on the logic:

  • the semicolon-separated sequence of alternatives = a logical OR expression
  • the comma-separated list of predicates in one alternative = a logical AND expression

Version Binding

Version binding is the process of determining exactly one version of a given source object from the set of all available versions. version binding is said to succeed if one of the rule alternatives succeeds. an alternative succeeds if it leads to the identification of exactly one version. otherwise? it fails. 😭

when shape binds a version to the name of a source object, she tries each alternative with a matching pattern, until the name is unambiguously bound to a version. if the pattern is omitted, the alternative will be tried unconditionally.

ngl, the functioning of version selection rules is one of shape's most important, yet most subtile aspects. so to give you an intuitive basis for understanding the selection rule mechanism, here's an example. the rule most_recent, below, binds:

  • files that were checked out for modification by the shape-invoking user
  • versions of files that were recently modified (β†’ status saved) by the same user
  • the most recently proposed version (β†’ status proposed) of files modified by other users,
  • or the file version from the last release.
    LASTRELEASE := `lastrelease`# "lastrelease" returns the name
                             # of the last release
    most_recent :-
        eq (status, busy), exists ($+[locked_by($(LOGNAME)):]);
        ge (status, saved), max (mtime),
          max (version), eq (author, $(LOGNAME));
        ge (status, proposed), max (mtime),
          max (version);
        eq (__SymbolicName__, $(LASTRELEASE));
        cut ($_rule$: couldn't bind $+ as requested!).

    locked_by (user_id) :-
        max (version), eq (locker, $_user_id$).

for a more detailed description of version selection rule syntax, semantics, and the list of built-in predicates, go read BindRules(7) πŸ“–

Activation of Version Selection Rules

a version selection for a certain target is invoked by specifying the name of the selection rule as the first dependency of a target, or by supplying a selection rule name as argument to the -R option. and if no selection rule is specified explicitly, shape uses her built-in version selection rule, which tries to bind a regular file, or the most recent version, to the name of an object ✨


πŸ¦‹ Variants πŸ¦‹

the term variant refers to the intention to manage a product that must comply with different sets of varying external constraints as a unit. independently from whatever particular semantics might get associated with the variant notion, there's a small number of techniques to implement software variation on a technical level. these techniques are:

  • physical separation of variant components. achieved by maintaining separate copies of components in different directories, or by maintaining variant specific branches in version control systems 🌿
  • source preprocessing of variant components. with this technique, multiple logical variants of a source component are maintained in a single file that contains preprocessor instructions. before a particular variant can be accessed, a preprocessor must extract it from the common source. a popular example is conditional compilation, controlled by the #if, and #ifdef instructions within the domain of C/C++ programming πŸ’…
  • composition variation of complex product variants. this one addresses the case when different variants of a complex product (such as a program) are composed from different sets of components 🧩
  • derivation variation (or variation of the process) that produces different variants of derived objects from the same set of sources by modifying parameters of the derivation process. a typical example is cross compilation of the same sources for different target platforms, or code instrumentation for various purposes, such as debugging, testing, profiling, or optimization πŸ”₯

depending on the particular needs of a project, all of these techniques may be in simultaneous use, and can occur intermixed as appropriate. and shape lets you associate logical variant names with a set of definitions that control all of the above mentioned techniques, making it possible to request builds of particular system variants (and combinations of compatible variants) without needing to worry about how these variants are realized technically. obsessed tbh 🫢

Variant Definitions

shape derives her flexibility from using macro substitution in the description file wherever possible. shape variant definitions are basically groups of macro definitions that take effect when the variant is activated for a build. a variant definition has this format:

        <variant-name> :+
            \t<Macro name1>=<Value>
            ...

when a variant is activated, the macro definitions associated with the variant become effective. any previous definition of a macro made in the description file, or on the command line, is replaced by the variant macro substitution. and if a macro is defined in several variants that are activated together, the respective values are concatenated ✨

Locating physically separate Variant Source Objects

shape provides a special macro, vpath, that is intended to be used in variant definitions. the vpath macro defines shape's search precedence when source version archives are located. if vpath is non-empty, shape tries to find any referenced source object in the vpath directories first.

if several activated variants define vpath, the variant search path is concatenated and searched from right to left, i.e. the last variant that has been activated has precedence. only if a referenced source component cannot be found in any of the vpath directories is the current directory searched. πŸ’…

and if a source object has been found, it will be bound by the current version selection rule, and be temporarily installed in the build directory. this means components maintained in a vpath subdirectory are temporarily moved up to the main directory. so you don't have to make any reference to a vpath subdirectory path in the target rules. iconic, honestly πŸ‘‘

Variant Activation

when a product is configured and built, variants are typically activated by supplying a variant name as argument to the -V options.

variants can also be activated for a given target by specifying respective, +-prefixed variant names as dependencies (see the section on Target Rules, above). variant activations for a target must occur before any real object dependency on the dependency line, and after the optional version selection rule activation. order matters bestie, no cap 🌸

🌈 Variant Class Definitions 🌈

Bestie. Okay. So you know how some variants are like, fundamentally incompatible? Like they simply cannot be in the same room together? Variant class definitions are the construct that lets you say that out loud. It's how you define variants that cannot be activated simultaneously. The format goes like this:

vclass <variant-class-name>  ::= ( <var1>, <var2> ...)

The same variant name can show up in multiple variant class definitions β€” it's not a one-class-per-variant situation, the girlies can multitask. But if a combination of variants is requested that includes any two variant names that are members of the same variant class, shape will throw an error message at you and terminate. Full stop. That's it. πŸ’€

You can turn this whole vibe check off by passing the -novclass switch on the command line, if you're feeling reckless.

NOTE: variant class definitions have to occur in the description file before any variant definition referenced in a variant class. Variant classes defined after the variants they reference literally cannot enforce mutual exclusion of incompatible variants. Order matters, bestie, it's giving "put your shoes on before you leave the house". πŸ‘Ÿ

✨ An Example ✨

The following example is gonna illustrate the use of variant definitions and variant classes together, so you can see how they play nice:

vclass compiler ::= (gnu, prop)

gnu:+
    CC = gcc -Wall
    OPTIMIZE = -O2 -inline-functions
    DEBUG = -g -g3
    PROFILE = -pg -a
    STDC = -ansi

prop:+
    CC = cc
    OPTIMIZE = +O3
    DEBUG = -g -z +Y
    PROFILE = -G
    STDC = -Aa

vclass quality ::= (debug, profile, optimize)

debug:+
    VARCFLAGS = $(DEBUG)

profile:+
    VARCFLAGS = $(PROFILE)

optimize:+
    VARCFLAGS = $(OPTIMIZE)

CFLAGS += $(VARCFLAGS)

If a variant needs to modify macros that have a predefined meaning, it's sometimes a good idea not to redefine the macro itself inside the variant section. Instead β€” and this is the slay part β€” you can augment an existing macro value using shape's additive macro definition facility, plus a macro from the variant definition that you defined for exactly this purpose (like VARCFLAGS up there in the example). Lowkey elegant, ngl. πŸ’…


πŸ‘‘ OPERATION πŸ‘‘

Okay so here's the whole plot of what happens when shape wakes up and gets to work. When it's invoked, shape first parses the command line. It records the names of the variants to be activated from the command line via the -V option. Next up, shape initializes the built-in and special macros. And also β€” not to be forgotten β€” shape's built-in derivation rules get initialized. Getting its whole face ready. ✨


πŸ“– Reading the Description File πŸ“–

After all that, all macro definitions made on the command line are made effective. Then shape goes looking for its description file and opens it.

If no description file is specified as an argument to the -f option, shape tries to find one of these files:

  • Shapefile
  • shapefile
  • Makefile
  • makefile

For each of these names, shape tries to find a regular file first, and if no such file exists, it tries to find the most recent version of that file in a version control archive. If no such version can be found either, shape moves on and tries the next name. It's a whole search party. πŸ”

When shape reads the description file, it collects all macro definitions and makes them immediately effective β€” unless a macro of the same name has been defined on the command line. Command line wins, periodt.

If the special macro IMPORT is encountered, the listed environment variables are defined as macros. And if macros with the same name as an imported environment variable show up in the description file, the description file one has precedence over the definition from the environment β€” unless the -e switch is in effect, in which case the whole hierarchy flips. πŸ™ˆ

When shape reads an include directive, it evaluates the rest of the line (that means the characters that immediately follow the directive) and interprets each word as the name of a file to be read. Each of those file names gets bound to either a regular file, or the most recent version of the file.

Shape opens each of the included files, suspends reading the current description file, and continues reading the contents of the included file(s) before it resumes reading the original control file. If multiple file names are specified in one include directive, shape reads each of the files in turn β€” starting with the leftmost, ending with the rightmost file name. And if an included file could not be opened? Shape issues a warning. Not a crash, just a little "hey bestie, that one didn't work". 🫢

While shape reads its description files, version selection rules and target rules are collected. They only get defined after shape has finished reading the description file. Meanwhile, macro-, variant-, and variant class definitions are made effective as soon as they've been recognized. Two different timelines, both valid. ⏰


πŸ› οΈ The Build Process πŸ› οΈ

Once the description file has been read, shape figures out which targets have been requested.

  • If targets have been requested from the command line, shape will attempt to build each of them, starting with the leftmost target and proceeding towards the rightmost.
  • If no target has been requested from the command line, shape searches the description file for a target named .DEFAULT. If such a target exists, and there are any dependencies associated with it, shape will attempt to build each of these dependencies, from left to right.
  • If no .DEFAULT target rule has been defined in the description file, shape will attempt to build the first target defined in the description file.

When shape builds a target, it proceeds as follows:

  1. Determine the names of the source objects for a given target by traversing the dependency graph, using built-in and user supplied target rules. The dependency graph is traversed depth first. The ids of all applied rules are recorded.
  2. For each required source object, locate the source version archive in the repository. Locating of source version archives takes the current vpath into account.
  3. Bind each of the source object's names to an appropriate version as implied by the currently active version selection rule. Record the id of each bound dependency. If a dependency is itself a derived object, use its cache key as id.
  4. Construct the derivation key for the current target from the target name and the records resulting from steps 1) and 3).
  5. Search the derived object cache for an object that has a derivation key identical to the key constructed in step 4). 6a. If an appropriate derived object was found, a copy of it is installed in the build directory, rather than deriving it from its sources. 😌 6b. If no appropriate derived object was found, it is created by deriving it from its parts. The resulting derived object is put into the derived object cache, and associated with the derivation key resulting from step 4).
Targets with an empty list of dependencies β€” and thus an empty derivation key β€” are always (re-) derived. No caching shortcuts for those, sorry babe.

When shape determines the dependencies of a requested target, it does so by evaluating either explicit target rules, or by applying β€” possibly built-in β€” implicit rules.

If explicit target rules specify object dependencies but no derivation script in the rule body, shape will attempt to supply an appropriate default derivation script. When searching for such a default derivation script, shape tries to find an applicable implicit rule for the current target. An implicit rule is considered applicable if:

  • it has the current target in its list of targets (after pattern substitution), and
  • all β€” explicit, and implied β€” dependencies exist.

If no implicit rule is found to be applicable, shape looks for the .DEFAULT target rule. If such a rule exists, and if it has an associated derivation script in its rule body, that script will be supplied as the default derivation script. If neither of the two possibilities leads to a default derivation script, shape gives up. πŸ’€ It tried, honestly.


πŸ—„οΈ Derived Object Caching πŸ—„οΈ

Before the derivation process for a requested target is even started, shape attempts to find a suitable derived object in the derived object cache that matches the required properties.

Shape is based on the derivation key concept for target objects. The derivation key is constructed according to the algorithm described above. The relevant parameters that go into the derivation key are:

  • the list of dependency ids
  • the target rule id
  • the list of production ingredients
  • the build platform β€” usually defined by the macro HOSTTYPE; if this macro is not defined, shape takes the host id as the build platform
  • the attribute expansion status of each source object

When an object has been derived, shape stores it in the derived object cache and marks it with the derivation key attribute. Iconic behavior honestly.

For a detailed trace of shape's derived object cache handling, and the use of derivation keys, run shape with the -D switch. It's giving behind-the-scenes documentary. 🎬

πŸš€ Command Execution πŸš€

When a target needs to be (re-) derived, shape executes the commands associated with the target.

Before the commands are executed, shape sets up the command execution context. The version objects of the target's dependencies are installed as regular files in the file system. If necessary, shape retrieves source objects from the version control archive. And if a file with the object's name already exists in the place where a version is to be installed, shape will temporarily move it to the AtFS subdirectory. After the command script has completed, shape will restore the original state of all affected directories. Very demure, very mindful, puts everything back where it found it. πŸŽ€

Shape executes a command line by starting the program referenced in the $(SHELL) macro, and opening a pipe to the resulting process. The command line is written to the pipe, and thus sent to the $(SHELL) process' standard input. Plumbing but make it chic. πŸ¦‹

Each of the command lines in a rule body is executed by a separate process. Which means β€” and this is important, bestie β€” the execution status of separate commands is not preserved. If you need multiple commands that rely on the execution status of previous commands, all of those commands must occur in a single command line. You can do this with line continuations (peep the section on Syntactical Structure, above).

NOTE: many command interpreters use the $ character as a special symbol (typically as a variable reference). Make sure to pass $ characters in commands to the $(SHELL) process by using the $$ special macro (see the section on Macro References, above). Double it up or it's not gonna do what you think it does. 😭

πŸ’€ INCOMPATIBILITIES πŸ’€

ok bestie, real talk time. so shape was literally designed to be upward compatible with Makefiles, so migrating from make(1) is mostly a smooth lil glide. most of make's description file features are in shape, no cap. BUT. there's a number of incompatibilities that may need to be taken care of, so don't just yolo it.

also there's a whole ecosystem of popular extensions to the original make program (like Sun's Make, HP's Make, GNU Make, nmake etc.) that offer various special features that aren't supported by other make extensions, or by shape. so when a migration from make to shape is planned, you should 100% check whether special extensions or incompatible features are being used. vibe check your Makefile first, fr fr. 🧚


🚫 Features not supported by shape

  • Double colon rules β€” these associate the same target with different derivation scripts, and they're useful for supporting different derivations for a target depending on which dependencies are out of date. but here's the thing: shape bases its decision on whether to derive on the derivation key, not on mere modification time stamps of files. so this sort of rule literally makes no sense in shape. it's giving "wrong era" πŸ’…
  • Archive member targets β€” these are objects that live in an archive file (see ar(1)) rather than the file system. within these archives, make bases its decisions on the modification time stamps of source files, and archive entry dates. there is no way for shape to simulate the concept of derivation keys for archive members. HOWEVER, maintenance of archives is actually easy with shape, because all data for compiled object files is maintained in the derived object cache. so if the source for an object that is stored in an archive gets modified, shape can rederive this object, and selectively replace the entry in the archive. slay. ✨
  • SCCS stuff β€” in order to provide basic support for team oriented development processes, make allows you to retrieve the most recent version of source files from SCCS archives. because of the awkward naming convention for SCCS version archive files, special support for dealing with these archives had to be built into make. but shape is tightly integrated with the AtFS version object repository, so there's no need for any special SCCS support. we simply don't need it, bestie. πŸ™ˆ
  • Special targets β€” shape does not recognize the special targets .PRECIOUS:, and .SUFFIXES:. let's break it down:
    • the .PRECIOUS target in Makefiles exists to prevent deletion of expensively derived intermediate targets (by default, make deletes intermediate targets). because shape stores intermediate targets in the derived object cache, there is no need for the .PRECIOUS feature. and to prevent caching of possibly large, useless intermediate targets, use the .NOBPOOL: special target (see section on Special Targets, above).
    • the .SUFFIXES target in Makefiles exists to introduce new suffix types into make's derivation engine, and to determine the order in which implicit rules (suffix rules in make terminology) are applied. in shape, new suffix types can be added dynamically, simply by introducing new implicit rules. moreover, shape has an intelligent algorithm that determines the applicable implicit rule. main character energy, honestly. πŸ‘‘

πŸŒ€ Features with different semantics

  • Environment Variables β€” many make programs import the entire set of environment variables as macro definitions into the build process. this can sometimes produce surprising results 😭. in shape, environment variables are explicitly imported with the IMPORT special macro. you control it, periodt.
  • ? Macro β€” in make's target rules, the special macro reference $? is substituted by the names of those dependency file names that have been updated since the current target has been derived. because shape bases its decision whether to derive on the concept of derivation key, rather than mere file modification time stamps, the ? macro cannot be correctly defined. instead, shape substitutes the entire list of dependency names β€” updated or not. it's the whole squad, no filtering. 🫢

πŸ“ FILES πŸ“

Shapefile, shapefile, Makefile, makefile, /tmp/shapeXXXXXX, <target name>.bct

πŸ‘€ SEE ALSO πŸ‘€

make(1), save(1), retrv(1), vadm(1), vl(1), vgdb(1), vbind(1),
afintro(3), atfstkintro(3), sttkintro(3), bindrules(7)

go read those if you wanna go deeper, iykyk. 🌈


⚠️ CAVEATS AND BUGS ⚠️

bestie, listen. macro references containing string substitutions cause a syntax error if used in place of target dependencies. yes, really. 😭

Workaround: use indirect macro substitution.

and ngl, there are probably more bugs in shape. please report any bug findings to shape-cr@cs.tu-berlin.de. we wanna know, fr fr. πŸ’€


πŸ“š FURTHER READING πŸ“š

  • Axel Mahler: ``Using the Shape Toolkit for Cooperative Software Development - A Tutorial'', in the toolkit distribution.
  • Axel Mahler and Andreas Lampen: ``An Integrated Toolset for Engineering Software Configurations'', Sigplan Notices, Vol. 24, No. 2, or Software Engineering Notes, Vol. 13, No. 5, November 1988.
  • Andreas Lampen and Axel Mahler: ``An Object Base for Attributed Software Objects'', Proceedings of the Fall 1988 EUUG Conference.

these and other papers are available via anonymous ftp from coma.cs.tu-berlin.de (pub/shapeTools/papers). go get your reading on, scholar. πŸŽ€


πŸ‘‘ AUTHOR πŸ‘‘

shape was designed by the shape project team at Technical University Berlin. the program was originally implemented by Wolfgang Obst (1988). extensive fixes and modifications were introduced by Axel Mahler (1992). valuable contributions came from Steve Emmerson. in 1993 most parts of shape were re-implemented by Axel Mahler. the version binding was re-implemented by Andreas Lampen. a complete re-implementation of the Shapefile parser was done by Juergen Nickelsen. absolute legends, every single one. πŸ’–

Contact:

SHAPE
Technical University Berlin
Sekr. FR 5-6
Franklinstr. 28/29
10587 Berlin

General correspondence: shape@cs.tu-berlin.de Bug reports and modification requests: shape-cr@cs.tu-berlin.de


aaaand that's a wrap on the whole shape(1) page, bestie!! πŸŽ‰ this final part came from shape_CM-4.9, dated Thu Jul 8 01:21:47 1993, and it's the shape(1) manual page β€” go build something iconic with it. mwah. πŸ’…βœ¨πŸ¦‹