]> git.ipfire.org Git - thirdparty/make.git/commitdiff
[SV 8297] Implement "grouped targets" for explicit rules.
authorKaz Kylheku <kaz@kylheku.com>
Sat, 11 May 2019 22:35:03 +0000 (18:35 -0400)
committerPaul Smith <psmith@gnu.org>
Sun, 12 May 2019 20:29:20 +0000 (16:29 -0400)
This patch allows "grouped targets" using the &: syntax:

  tgt1 tgt2 ... tgtn &: pre1 pre2 ...
        recipe

When the &: separator is used (in single or double colon forms), all
the targets are understood to be built by a single invocation of the
recipe.  This is accomplished by piggy-backing on the already-existing
pattern rule feature, using the file's "also_make" list.

* NEWS: Add information about grouped targets.
* doc/make.texi (Multiple Targets): Add information on grouped targets.
(Pattern Intro): Refer to the new section to discuss multiple patterns.
* src/main.c (main): Add "grouped-targets" to .FEATURES
* src/read.c (make_word_type): Add new types for &: and &::.
(eval): Recognize the &: and &:: separator and remember when used.
(record_files): Accept an indicator of whether the rule is grouped.
If so, update also_make for each file to depend on the other files.
(get_next_mword): Recognize the &: and &:: word types.
* tests/scripts/features/grouped_targets: New test script.
* AUTHORS: Add Kaz Kylheku

AUTHORS
NEWS
doc/make.texi
src/main.c
src/makeint.h
src/read.c
tests/scripts/features/grouped_targets [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
index 685aede817bd4ad30d3647eedcc3625d5566b529..e80bad915bdf94f7eefe64d2b9119f2ad9c7f573 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -67,6 +67,7 @@ Other contributors:
   David A. Wheeler <dwheeler@dwheeler.com>
   David Boyce <dsb@boyski.com>
   Frank Heckenbach <f.heckenbach@fh-soft.de>
+  Kaz Kylheku <kaz@kylheku.com>
 
 With suggestions/comments/bug reports from a cast of ... well ...
 hundreds, anyway :)
diff --git a/NEWS b/NEWS
index eb25119c7cd3e536c9457ba8b1e48ebe7b414dc0..40127c36823c106c7236d5bde25615f927fa9500 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,14 @@ http://sv.gnu.org/bugs/index.php?group=make&report_id=111&fix_release_id=108&set
   treated BOTH as simple targets AND as pattern rules.  Behavior now matches
   the documentation, and pattern rules are no longer created in this case.
 
+* New feature: Grouped explicit targets
+  Pattern rules have always had the ability to generate multiple targets with
+  a single invocation of the recipe.  It's now possible to declare that an
+  explicit rule generates multiple targets with a single invocation.  To use
+  this, replace the ":" token with "&:" in the rule.  To detect this feature
+  search for 'grouped-target' in the .FEATURES special variable.
+  Implementation contributed by Kaz Kylheku <kaz@kylheku.com>
+
 * Makefiles can now specify the '-j' option in their MAKEFLAGS variable and
   this will cause make to enable that parallelism mode.
 
index 25a37c00ce98ee7569d05325d2a3dd853b5f079c..302292749209b03e54deab37fd108191787ac89a 100644 (file)
@@ -3012,13 +3012,22 @@ 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 an explicit rule has multiple targets they can be treated in one
+of two possible ways: as independent targets or as grouped targets.
+The manner in which they are treated is determined by the separator that
+appears after the list of targets.
 
-This is useful in two cases.
+@subsubheading Rules with Independent Targets
+@cindex independent targets
+@cindex targets, independent
+
+Rules that use the standard target separator, @code{:}, define
+independent targets.  This is equivalent to writing the same rule once
+for each target, with duplicated prerequisites and recipes.  Typically,
+the recipe would use automatic variables such as @samp{$@@} to specify
+which target is being built.
+
+Rules with independent targets are useful in two cases:
 
 @itemize @bullet
 @item
@@ -3030,13 +3039,18 @@ kbd.o command.o files.o: command.h
 
 @noindent
 gives an additional prerequisite to each of the three object files
-mentioned.
+mentioned.  It is equivalent to writing:
+
+@example
+kbd.o: command.h
+command.o: command.h
+files.o: command.h
+@end example
 
 @item
-Similar recipes work for all the targets.  The recipes do not need
-to be absolutely identical, since the automatic variable @samp{$@@}
-can be used to substitute the particular target to be remade into the
-commands (@pxref{Automatic Variables}).  For example:
+Similar recipes work for all the targets.  The automatic variable
+@samp{$@@} can be used to substitute the particular target to be
+remade into the commands (@pxref{Automatic Variables}).  For example:
 
 @example
 @group
@@ -3070,6 +3084,57 @@ You cannot do this with multiple targets in an ordinary rule, but you
 can do it with a @dfn{static pattern rule}.  @xref{Static Pattern,
 ,Static Pattern Rules}.
 
+@subsubheading Rules with Grouped Targets
+@cindex grouped targets
+@cindex targets, grouped
+
+If instead of independent targets you have a recipe that generates
+multiple files from a single invocation, you can express that
+relationship by declaring your rule to use @emph{grouped targets}.  A
+grouped target rule uses the separator @code{&:} (the @samp{&} here is
+used to imply ``all'').
+
+When @code{make} builds any one of the grouped targets, it understands
+that all the other targets in the group are also created as a result
+of the invocation of the recipe.  Furthermore, if only some of the
+grouped targets are out of date or missing @code{make} will realize
+that running the recipe will update all of the targets.
+
+As an example, this rule defines a grouped target:
+
+@example
+@group
+foo bar biz &: baz boz
+        echo $^ > foo
+        echo $^ > bar
+        echo $^ > biz
+@end group
+@end example
+
+During the execution of a grouped target's recipe, the automatic
+variable @samp{$@@} is set to the name of the particular target in the
+group which triggered the rule.  Caution must be used if relying on
+this variable in the recipe of a grouped target rule.
+
+Unlike independent targets, a grouped target rule @emph{must} include
+a recipe.  However, targets that are members of a grouped target may
+also appear in independent target rule definitions that do not have
+recipes.
+
+Each target may have only one recipe associated with it.  If a grouped
+target appears in either an independent target rule or in another
+grouped target rule with a recipe, you will get a warning and the
+latter recipe will replace the former recipe.  Additionally the target
+will be removed from the previous group and appear only in the new
+group.
+
+If you would like a target to appear in multiple groups, then you must
+use the double-colon grouped target separator, @code{&::} when
+declaring all of the groups containing that target.  Grouped
+double-colon targets are each considered independently, and each
+grouped double-colon rule's recipe is executed at most once, if at
+least one of its multiple targets requires updating.
+
 @node Multiple Rules, Static Pattern, Multiple Targets, Rules
 @section Multiple Rules for One Target
 @cindex multiple rules for one target
@@ -9772,20 +9837,13 @@ More than one pattern rule may match a target.  In this case
 @code{make} will choose the ``best fit'' rule.  @xref{Pattern Match,
 ,How Patterns Match}.
 
-@c !!! The end of of this paragraph should be rewritten.  --bob
-Pattern rules may have more than one target.  Unlike normal rules,
-this does not act as many different rules with the same prerequisites
-and recipe.  If a pattern rule has multiple targets, @code{make} knows
-that the rule's recipe is responsible for making all of the targets.
-The recipe is executed only once to make all the targets.  When
-searching for a pattern rule to match a target, the target patterns of
-a rule other than the one that matches the target in need of a rule
-are incidental: @code{make} worries only about giving a recipe and
-prerequisites to the file presently in question.  However, when this
-file's recipe is run, the other targets are marked as having been
-updated themselves.
 @cindex multiple targets, in pattern rule
 @cindex target, multiple in pattern rule
+Pattern rules may have more than one target; however, every target
+must contain a @code{%} character.  Pattern rules are always treated
+as grouped targets (@pxref{Multiple Targets, , Multiple Targets in a
+Rule}) regardless of whether they use the @code{:} or @code{&:}
+separator.
 
 @node Pattern Examples, Automatic Variables, Pattern Intro, Pattern Rules
 @subsection Pattern Rule Examples
index bcaeab76336fc18ee9bc1f3ef7bc74aeb20472da..57e589822f3cfc81e44f17c9eaf3c9cf31c9a7a4 100644 (file)
@@ -1315,6 +1315,7 @@ main (int argc, char **argv, char **envp)
   {
     const char *features = "target-specific order-only second-expansion"
                            " else-if shortest-stem undefine oneshell nocomment"
+                           " grouped-target"
 #ifndef NO_ARCHIVES
                            " archives"
 #endif
index 315500fce31242bdaf2d797c51ae8418c8a89048..668d07eedd1bb8c4a735a0cc1055ea7a4e894f15 100644 (file)
@@ -67,6 +67,7 @@ char *alloca ();
 # define __NO_STRING_INLINES
 #endif
 
+#include <stddef.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <signal.h>
index 45f3e35b5ea2d538eb34e85d6cf55d40dfb8a6ad..fd357ab87503e1b67ad357d0203e61e5976a0d3e 100644 (file)
@@ -72,7 +72,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_ampcolon, w_ampdcolon
   };
 
 
@@ -142,7 +142,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, size_t 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,
                           size_t commands_idx, int two_colon,
@@ -151,7 +152,7 @@ static void record_target_var (struct nameseq *filenames, char *defn,
                                enum variable_origin origin,
                                struct vmodifiers *vmod,
                                const floc *flocp);
-static enum make_word_type get_next_mword (char *buffer, char *delim,
+static enum make_word_type get_next_mword (char *buffer,
                                            char **startp, size_t *length);
 static void remove_comments (char *line);
 static char *find_map_unquote (char *string, int map);
@@ -574,6 +575,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;
@@ -591,7 +593,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;                                                      \
@@ -599,6 +602,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;
@@ -1023,7 +1027,7 @@ eval (struct ebuffer *ebuf, int set_default)
            variable we don't want to expand it.  So, walk from the
            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);
+        wtype = get_next_mword (line, &lb_next, &wlen);
         switch (wtype)
           {
           case w_eol:
@@ -1035,6 +1039,8 @@ eval (struct ebuffer *ebuf, int set_default)
 
           case w_colon:
           case w_dcolon:
+          case w_ampcolon:
+          case w_ampdcolon:
             /* We accept and ignore rules without targets for
                compatibility with SunOS 4 make.  */
             no_targets = 1;
@@ -1080,20 +1086,29 @@ eval (struct ebuffer *ebuf, int set_default)
               }
 
             colonp = find_char_unquote (p2, ':');
+
 #ifdef HAVE_DOS_PATHS
-            /* The drive spec brain-damage strikes again...  */
-            /* Note that the only separators of targets in this context
-               are whitespace and a left paren.  If others are possible,
-               they should be added to the string in the call to index.  */
-            while (colonp && (colonp[1] == '/' || colonp[1] == '\\') &&
-                   colonp > p2 && isalpha ((unsigned char)colonp[-1]) &&
-                   (colonp == p2 + 1 || strchr (" \t(", colonp[-2]) != 0))
-              colonp = find_char_unquote (colonp + 1, ':');
+            if (colonp > p2)
+              /* The drive spec brain-damage strikes again...
+                 Note that the only separators of targets in this context are
+                 whitespace and a left paren.  If others are possible, add them
+                 to the string in the call to strchr.  */
+              while (colonp && (colonp[1] == '/' || colonp[1] == '\\') &&
+                     isalpha ((unsigned char) colonp[-1]) &&
+                     (colonp == p2 + 1 || strchr (" \t(", colonp[-2]) != 0))
+                colonp = find_char_unquote (colonp + 1, ':');
 #endif
-            if (colonp != 0)
-              break;
 
-            wtype = get_next_mword (lb_next, NULL, &lb_next, &wlen);
+            if (colonp)
+              {
+                /* If the previous character is '&', back up before '&:' */
+                if (colonp > p2 && colonp[-1] == '&')
+                  --colonp;
+
+                break;
+              }
+
+            wtype = get_next_mword (lb_next, &lb_next, &wlen);
             if (wtype == w_eol)
               break;
 
@@ -1123,12 +1138,21 @@ eval (struct ebuffer *ebuf, int set_default)
               O (fatal, fstart, _("missing separator"));
           }
 
-        /* Make the colon the end-of-string so we know where to stop
-           looking for targets.  Start there again once we're done.  */
-        *colonp = '\0';
-        filenames = PARSE_SIMPLE_SEQ (&p2, struct nameseq);
-        *colonp = ':';
-        p2 = colonp;
+        {
+          char save = *colonp;
+
+          /* If we have &:, it specifies that the targets are understood to be
+             updated/created together by a single invocation of the recipe. */
+          if (save == '&')
+            also_make_targets = 1;
+
+          /* Make the colon the end-of-string so we know where to stop
+             looking for targets.  Start there again once we're done.  */
+          *colonp = '\0';
+          filenames = PARSE_SIMPLE_SEQ (&p2, struct nameseq);
+          *colonp = save;
+          p2 = colonp + (save == '&');
+        }
 
         if (!filenames)
           {
@@ -1930,7 +1954,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,
               size_t commands_idx, int two_colon,
@@ -1938,6 +1963,7 @@ record_files (struct nameseq *filenames, const char *pattern,
 {
   struct commands *cmds;
   struct dep *deps;
+  struct dep *also_make = NULL;
   const char *implicit_percent;
   const char *name;
 
@@ -1963,8 +1989,10 @@ record_files (struct nameseq *filenames, const char *pattern,
       cmds->command_lines = 0;
       cmds->recipe_prefix = prefix;
     }
+  else if (are_also_makes)
+    O (fatal, flocp, _("grouped targets must provide a recipe"));
   else
-     cmds = 0;
+     cmds = NULL;
 
   /* If there's a prereq string then parse it--unless it's eligible for 2nd
      expansion: if so, snap_deps() will do it.  */
@@ -2159,6 +2187,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
@@ -2223,6 +2260,29 @@ record_files (struct nameseq *filenames, const char *pattern,
         O (error, flocp,
            _("*** mixed implicit and normal rules: deprecated syntax"));
     }
+
+  /* If there are also-makes, then populate a copy of the also-make list into
+     each one. For the last file, we take our original also_make list instead
+     wastefully copying it one more time and freeing it.  */
+  {
+    struct dep *i;
+
+    for (i = also_make; i != NULL; i = i->next)
+      {
+        struct file *f = i->file;
+        struct dep *cpy = i->next ? copy_dep_chain (also_make) : also_make;
+
+        if (f->also_make)
+          {
+            OS (error, &cmds->fileinfo,
+                _("warning: overriding group membership for target '%s'"),
+                f->name);
+            free_dep_chain (f->also_make);
+          }
+
+        f->also_make = cpy;
+      }
+    }
 }
 \f
 /* Search STRING for an unquoted STOPMAP.
@@ -2660,6 +2720,8 @@ readline (struct ebuffer *ebuf)
      w_variable     A word containing one or more variables/functions
      w_colon        A colon
      w_dcolon       A double-colon
+     w_ampcolon     An ampersand-colon (&:) token
+     w_ampdcolon    An ampersand-double-colon (&::) token
      w_semicolon    A semicolon
      w_varassign    A variable assignment operator (=, :=, ::=, +=, ?=, or !=)
 
@@ -2668,7 +2730,7 @@ readline (struct ebuffer *ebuf)
    in a command list, etc.)  */
 
 static enum make_word_type
-get_next_mword (char *buffer, char *delim, char **startp, size_t *length)
+get_next_mword (char *buffer, char **startp, size_t *length)
 {
   enum make_word_type wtype;
   char *p = buffer, *beg;
@@ -2717,6 +2779,21 @@ get_next_mword (char *buffer, char *delim, char **startp, size_t *length)
         wtype = w_colon;
       goto done;
 
+    case '&':
+      if (*p == ':')
+        {
+          ++p;
+          if (*p != ':')
+            wtype = w_ampcolon; /* &: */
+          else
+            {
+              ++p;
+              wtype = w_ampdcolon; /* &:: */
+            }
+          goto done;
+        }
+      break;
+
     case '+':
     case '?':
     case '!':
@@ -2726,19 +2803,15 @@ get_next_mword (char *buffer, char *delim, char **startp, size_t *length)
           wtype = w_varassign; /* += or ?= or != */
           goto done;
         }
-      /* FALLTHROUGH */
+      break;
 
     default:
-      if (delim && strchr (delim, c))
-        {
-          wtype = w_static;
-          goto done;
-        }
+      break;
     }
 
   /* This is some non-operator word.  A word consists of the longest
      string of characters that doesn't contain whitespace, one of [:=#],
-     or [?+!]=, or one of the chars in the DELIM string.  */
+     or [?+!]=, or &:.  */
 
   /* We start out assuming a static word; if we see a variable we'll
      adjust our assumptions then.  */
@@ -2818,10 +2891,13 @@ get_next_mword (char *buffer, char *delim, char **startp, size_t *length)
             }
           break;
 
-        default:
-          if (delim && strchr (delim, c))
+        case '&':
+          if (*p == ':')
             goto done_word;
           break;
+
+        default:
+          break;
         }
 
       c = *(p++);
diff --git a/tests/scripts/features/grouped_targets b/tests/scripts/features/grouped_targets
new file mode 100644 (file)
index 0000000..ef9366b
--- /dev/null
@@ -0,0 +1,133 @@
+#                                                                    -*-perl-*-
+
+$description = "This test is about grouped multiple targets indicated by &:";
+$details = "Here we test for requirements like\n"
+          ."- if multiple such targets are updated, the recipe is run once\n"
+          ."- parsing issues related to the &: syntax itself\n";
+
+# Parsing: &: allowed without any targets.
+run_make_test(q{
+.PHONY: all
+&:;
+all: ;@echo -n
+},
+'', "");
+
+# Parsing: &: works not preceded by whitespace.
+run_make_test(q{
+foo&:;@echo foo
+},
+'foo', "foo");
+
+# Ordinary rule runs recipe four times for t1 t2 t3 t4.
+# Grouped target rule runs recipe once; others are considered updated.
+run_make_test(q{
+.PHONY: t1 t2 t3 t4 g1 g2 g3 g4
+t1 t2 t3 t4: ; @echo $@
+g1 g2 g3 g4 &: ; @echo $@
+},
+'t1 t2 t3 t4 g1 g2 g3 g4',
+"t1\n"
+."t2\n"
+."t3\n"
+."t4\n"
+."g1\n"
+."#MAKE#: Nothing to be done for 'g2'.\n"
+."#MAKE#: Nothing to be done for 'g3'.\n"
+."#MAKE#: Nothing to be done for 'g4'.");
+
+# Similar to previous test, but targets come from m1 phony
+# rather than from the command line. We don't see "Nothing to
+# be done for" messages. Also, note reversed order g4 g3 ...
+# Thus the auto variable $@ is "g4" when that rule fires.
+run_make_test(q{
+.PHONY: m1 t1 t2 t3 t4 g1 g2 g3 g4
+m1: t1 t2 t3 t4 g4 g3 g2 g1
+t1 t2 t3 t4: ; @echo $@
+g1 g2 g3 g4&: ; @echo $@
+},
+'',
+"t1\nt2\nt3\nt4\ng4");
+
+# Set a grouped target recipe for existing targets
+run_make_test(q{
+.PHONY: M a b
+M: a b
+a:
+a b&: ; @echo Y
+b:
+},
+'',
+"Y");
+
+# grouped targets require a recipe
+run_make_test(q{
+.PHONY: M a b
+M: a b
+a b&:
+},
+'',
+"#MAKEFILE#:4: *** grouped targets must provide a recipe.  Stop.", 512);
+
+# Pattern rules use grouped targets anyway so it's a no-op
+run_make_test(q{
+.PHONY: M
+M: a.q b.q
+a.% b.%&: ; @echo Y
+},
+'',
+"Y");
+
+# Double-colon grouped target rules.
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: a b
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"X");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: c
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"X\nY");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: a b c d e
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"X\nY");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: d e
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"Y");
+
+run_make_test(q{
+.PHONY: M a b c d e f g h
+M: f g h
+a b c&:: ; @echo X
+c d e&:: ; @echo Y
+f g h&:: ; @echo Z
+},
+'',
+"Z");
+
+# This tells the test driver that the perl test script executed properly.
+1;