It seems I have it working.
Alas, the proposed (...) syntax for this was not meant to be. GNU Make reads
line by line and performs ad hoc tokenizing, with cruft in it like the
foo.a(bar.o xyzzy.o) archive syntax and of course variable/macro expansions.
Working parentheses around the target list in that grotty code would be a lot
of work, and create risk.
So, syntax-wise, I have it like this:
+ y.tab.c y.tab.h: parser.y ...
recipe ...
The + symbol is already involved in += syntax. If it's not followed by
=, then it's not recognized; I added a new token type for it when it's
followed by whitspace or EOL.
When the + symbol is present, then every file is added to the
"also_make" list of every other file, which groups them all together
as one lump.
How it works is that the rules are still replicated exactly as before
for each target; it's this "also_make" linkage which suppresses the
duplicate recipe invocations; Make understands that if the recipe is run
for a target, then all of its "also_make" targets are considered
updated.
Patch against GNU Make 4.2.1:
8-<-------8-<-------8-<-------8-<-------8-<-------8-<-------8-<-------8-<---
From: Kaz Kylheku <
k...@kylheku.com>
Date: Tue, 12 Mar 2019 19:35:02 -0700
Subject: [PATCH] Implement "grouped targets" in ordinary rules.
This patch adds the + (plus) syntax:
+ tgt1 tgt2 ... tgtn : pre1 pre2 ...
recipe
When the + is present, then the targets are understood to be built
together by one invocation of the recipe: each one lists the others in
its "also_make" list, similarly to what multiple-target pattern rules
already do.
* doc/make.texi: Section on Multiple Targets updated.
* read.c (enum make_word_type): New enum member, w_plus.
(record_files): New argument, are_also_makes flag which indicates
that the targets are all linked together as also-makes.
If this is true, then we build an also_make list which contains
each target represented as a dep. When all targets are registered,
we then make a second pass, installing a copy of this also_make
list into each target, possibly catenating it with an existing
also_make list.
(record_waiting_files): Pass new argument to record_files.
(eval): Check for the + token and set a new local variable
also_make_targets if that is so. This is then passed to
record_files by the record_waiting_files macro.
(get_next_mword): If a + is followed by whitespace, or end of string,
rathe than =, then instead of falling through, recognize it as the new
w_plus token.
---
doc/make.texi | 41 ++++++++++++++++++++++++++++-----
read.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 95 insertions(+), 10 deletions(-)
diff --git a/doc/make.texi b/doc/make.texi
index 01bcec7..033c72d 100644
--- a/doc/make.texi
+++ b/doc/make.texi
@@ -3012,13 +3012,27 @@ both pieces to the suffix list. In practice, suffixes normally begin with
@cindex targets, multiple
@cindex rule, with multiple targets
-A rule with multiple targets is equivalent to writing many rules, each with
-one target, and all identical aside from that. The same recipe applies to
-all the targets, but its effect may vary because you can substitute the
-actual target name into the recipe using @samp{$@@}. The rule contributes
-the same prerequisites to all the targets also.
+When a rule has multiple targets, the semantics depends on what kind
+of rule it is, and whether the "grouped targets" feature is invoked.
-This is useful in two cases.
+There are two possible semantics. A rule with multiple targets may be
+equivalent to writing many rules, each with one target, and all identical aside
+from that. Under this semantics, the same recipe applies to all the targets,
+but its effect may vary because you can substitute the actual target name into
+the recipe using @samp{$@@}. The rule contributes the same prerequisites to
+all the targets also. This is known as "replicated recipe semantics".
+The other semantics is "grouped target" semantics. Under this semantics,
+the files are all understood to be updated or created together by a single
+invocation of the recipe.
+
+Pattern rules implicitly follow "grouped target" semantics.
+
+Ordinary rules use "replicated recipe" semantics by default. If
+the symbol @samp{+} is placed before the first target, separated
+from it by whitespace, then the rule follows "grouped target"
+semantics.
+
+"Replicated recipe semantics" is useful in two cases.
@itemize @bullet
@item
@@ -3064,6 +3078,21 @@ types of output, one if given @samp{-big} and one if given
for an explanation of the @code{subst} function.
@end itemize
+"Grouped target" semantics is useful when a recipe builds multiple
+files, which are further involved in dependencies. For instance,
+it can express a rule rule for building a parser using Yacc, whose
+recipe generates @samp{y.tab.c} and @samp{y.tab.h} at the same time.
+Both are made to be dependent on @samp{parser.y}. If neither one
+exists, or if both are out of date with respect to @samp{parser.y},
+the recipe is invoked only once. Furthermore, if for some reason
+just one of these two files is missing or out of date, GNU Make
+knows that running the recipe also updates the other.
+
+Note that under "grouped target" semantics, the @samp{$@@} variable
+still expands to a single target: it refers to that target which
+triggered the execution of the recipe. The recipe cannot rely on
+@samp{$@@} being a stable reference to a particular one of the targets.
+
Suppose you would like to vary the prerequisites according to the
target, much as the variable @samp{$@@} allows you to vary the recipe.
You cannot do this with multiple targets in an ordinary rule, but you
diff --git a/read.c b/read.c
index b870aa8..983bdc5 100644
--- a/read.c
+++ b/read.c
@@ -71,7 +71,7 @@ struct vmodifiers
enum make_word_type
{
w_bogus, w_eol, w_static, w_variable, w_colon, w_dcolon, w_semicolon,
- w_varassign
+ w_varassign, w_plus
};
@@ -141,7 +141,8 @@ static void do_undefine (char *name, enum variable_origin origin,
static struct variable *do_define (char *name, enum variable_origin origin,
struct ebuffer *ebuf);
static int conditional_line (char *line, int len, const floc *flocp);
-static void record_files (struct nameseq *filenames, const char *pattern,
+static void record_files (struct nameseq *filenames, int are_also_makes,
+ const char *pattern,
const char *pattern_percent, char *depstr,
unsigned int cmds_started, char *commands,
unsigned int commands_idx, int two_colon,
@@ -576,6 +577,7 @@ eval (struct ebuffer *ebuf, int set_default)
unsigned int cmds_started, tgts_started;
int ignoring = 0, in_ignored_define = 0;
int no_targets = 0; /* Set when reading a rule without targets. */
+ int also_make_targets = 0; /* Set when reading grouped targets. */
struct nameseq *filenames = 0;
char *depstr = 0;
long nlines = 0;
@@ -593,7 +595,8 @@ eval (struct ebuffer *ebuf, int set_default)
{ \
fi.lineno = tgts_started; \
fi.offset = 0; \
- record_files (filenames, pattern, pattern_percent, depstr, \
+ record_files (filenames, also_make_targets, pattern, \
+ pattern_percent, depstr, \
cmds_started, commands, commands_idx, two_colon, \
prefix, &fi); \
filenames = 0; \
@@ -601,6 +604,7 @@ eval (struct ebuffer *ebuf, int set_default)
commands_idx = 0; \
no_targets = 0; \
pattern = 0; \
+ also_make_targets = 0; \
} while (0)
pattern_percent = 0;
@@ -1027,6 +1031,20 @@ eval (struct ebuffer *ebuf, int set_default)
beginning, expanding as we go, and looking for "interesting"
chars. The first word is always expandable. */
wtype = get_next_mword (line, NULL, &lb_next, &wlen);
+
+ /* If the line starts with + followed by whitespace, then
+ it specifies that the targets are understood to be updated/created
+ together by a single invocation of the recipe. In this case,
+ we record a single entry for the first target which follows the +,
+ and install the remaining targets as the also_make list of deps
+ for that file, rather than iterating over the targets and replicating
+ the rule for each one. */
+ if (wtype == w_plus) {
+ also_make_targets = 1;
+ lb_next += wlen;
+ wtype = get_next_mword (lb_next, NULL, &lb_next, &wlen);
+ }
+
switch (wtype)
{
case w_eol:
@@ -1931,7 +1949,8 @@ record_target_var (struct nameseq *filenames, char *defn,
that are not incorporated into other data structures. */
static void
-record_files (struct nameseq *filenames, const char *pattern,
+record_files (struct nameseq *filenames, int are_also_makes,
+ const char *pattern,
const char *pattern_percent, char *depstr,
unsigned int cmds_started, char *commands,
unsigned int commands_idx, int two_colon,
@@ -1939,6 +1958,7 @@ record_files (struct nameseq *filenames, const char *pattern,
{
struct commands *cmds;
struct dep *deps;
+ struct dep *also_make = 0;
const char *implicit_percent;
const char *name;
@@ -2158,6 +2178,15 @@ record_files (struct nameseq *filenames, const char *pattern,
f->cmds = cmds;
}
+ if (are_also_makes)
+ {
+ struct dep *also = alloc_dep();
+ also->name = f->name;
+ also->file = f;
+ also->next = also_make;
+ also_make = also;
+ }
+
f->is_target = 1;
/* If this is a static pattern rule, set the stem to the part of its
@@ -2222,6 +2251,27 @@ record_files (struct nameseq *filenames, const char *pattern,
O (error, flocp,
_("*** mixed implicit and normal rules: deprecated syntax"));
}
+
+ /* If the files are also-makes, then populate a copy of the also-make list
+ into each one. */
+
+ if (are_also_makes && also_make)
+ {
+ struct dep *i;
+
+ for (i = also_make; i != 0; i = i->next)
+ {
+ struct file *f = i->file;
+ struct dep *also_copy = copy_dep_chain(also_make);
+ struct dep *j;
+
+ for (j = also_copy; j->next != 0; j = j->next)
+ ;
+
+ j->next = f->also_make;
+ f->also_make = also_copy;
+ }
+ }
}
/* Search STRING for an unquoted STOPCHAR or blank (if BLANK is nonzero).
@@ -2676,6 +2726,12 @@ get_next_mword (char *buffer, char *delim, char **startp, unsigned int *length)
break;
case '+':
+ if (isspace(*p) || *p == 0)
+ {
+ wtype = w_plus;
+ break;
+ }
+ /* fallthrough */
case '?':
case '!':
if (*p == '=')