]> git.ipfire.org Git - thirdparty/make.git/commitdiff
[SV 62100] Add '--shuffle' option support
authorSergei Trofimovich <siarheit@google.com>
Sat, 4 Jun 2022 19:48:01 +0000 (15:48 -0400)
committerPaul Smith <psmith@gnu.org>
Sat, 4 Jun 2022 23:04:37 +0000 (19:04 -0400)
Introduce non-deterministic ordering into goal and prerequisite
traversal to help tease out inconsistent failures that may happen
when running in parallel build mode.

Introduce second order into each dependency chain:
1. Existing order is syntactic order reachable via 'dep->next'
2. New order is shuffled order stored as 'dep->shuf' in each 'dep'

When updating goals and prerequisites and '--shuffle' is provided,
use the shuffled order to walk the graph.  When automatic variable
are set always use the syntactic order of parameters.

* Makefile.am: Add new src/shuffle.c and src/shuffle.h file.
* build_w32.bat: Ditto.
* builddos.bat: Ditto.
* makefile.com: Ditto.
* po/POTFILES.in: Ditto.
* doc/make.texi: Add documentation for --shuffle.
* doc/make.1: Ditto.
* src/dep.h (DEP): Add the shuf pointer.
* src/filedef.h (struct file): Add was_shuffled flag.
* src/main.c: (shuffle_mode): Global flag for the shuffle mode.
(usage): Add the --shuffle option.
(switches): Ditto.
(main): Set shuffle_mode based on the command line parameter.
Reshuffle prerequisites if requested.
* src/remake.c (update_goal_chain): Walk the shuffled list if enabled.
(update_file_1): Ditto.
* src/shuffle.h: Provide an interface for shuffling prerequisites.
* src/shuffle.c: Implement option parsing and prerequisite shuffling.
* tests/scripts/options/shuffle: Test shuffle option and modes.

18 files changed:
AUTHORS
Makefile.am
NEWS
build_w32.bat
builddos.bat
doc/make.1
doc/make.texi
makefile.com
po/POTFILES.in
src/dep.h
src/filedef.h
src/implicit.c
src/job.c
src/main.c
src/remake.c
src/shuffle.c [new file with mode: 0644]
src/shuffle.h [new file with mode: 0644]
tests/scripts/options/shuffle [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
index 12b9d19f68899f0d9134ed53bc25181bf6db5262..35719b99b650b2bc93267b20c2ac9a01b3e93378 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -90,6 +90,7 @@ Other contributors:
   Carl Staelin (Princeton University)
   Ian Stewartson (Data Logic Limited)
   Tobias Stoeckmann <tobias@stoeckmann.org>
+  Sergei Trofimovich <siarheit@google.com>
   Marc Ullman <marc@mathworks.com>
   Christof Warlich <cwarlich@gmx.de>
   Florian Weimer <fweimer@redhat.com>
index 8ddfc0e634b1884a18a56a51e28db6920c14f683..1e7d0e3c012e8a1bcedb5b8fd5698a2592ec9ee2 100644 (file)
@@ -35,8 +35,9 @@ make_SRCS =   src/ar.c src/arscan.c src/commands.c src/commands.h \
                src/hash.c src/hash.h src/implicit.c src/job.c src/job.h \
                src/load.c src/loadapi.c src/main.c src/makeint.h src/misc.c \
                src/os.h src/output.c src/output.h src/read.c src/remake.c \
-               src/rule.c src/rule.h src/signame.c src/strcache.c \
-               src/variable.c src/variable.h src/version.c src/vpath.c
+               src/rule.c src/rule.h src/shuffle.h src/shuffle.c \
+               src/signame.c src/strcache.c src/variable.c src/variable.h \
+               src/version.c src/vpath.c
 
 w32_SRCS =     src/w32/pathstuff.c src/w32/w32os.c src/w32/compat/dirent.c \
                src/w32/compat/posixfcn.c src/w32/include/dirent.h \
diff --git a/NEWS b/NEWS
index 169a7846761e24c028c312e1d432000ba21565d7..f555a0d7ad426af47599796faba485f37b2c69a7 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -61,6 +61,13 @@ https://sv.gnu.org/bugs/index.php?group=make&report_id=111&fix_release_id=109&se
   avoiding the need for heuristics.
   Implementation provided by Sven C. Dack <sdack@gmx.com>
 
+* New feature: The --shuffle command line option
+  This option reorders goals and prerequisites to simulate non-determinism
+  that may be seen using parallel build.  Shuffle mode allows a form of "fuzz
+  testing" of parallel builds to verify that all prerequisites are correctly
+  described in the makefile.
+  Implementation provided by Sergei Trofimovich <siarheit@google.com>
+
 * GNU make has sometimes chosen unexpected, and sub-optimal, chains of
   implicit rules due to the definition of "ought to exist" in the implicit
   rule search algorithm, which considered any prerequisite mentioned in the
index 653841d79b5daeaef5569fdeaa268789b7ded1e3..c464e26d52fd69cde9ac9df6f60876d1dfe8c777 100755 (executable)
@@ -257,6 +257,7 @@ call :Compile src/read
 call :Compile src/remake\r
 call :Compile src/remote-stub\r
 call :Compile src/rule\r
+call :Compile src/shuffle\r
 call :Compile src/signame\r
 call :Compile src/strcache\r
 call :Compile src/variable\r
index 2b3de30167fe8206b6f2e956ba304a42235ffd19..d4ff8a63fac3ef9d94811e0f9c6212f01ab519a2 100644 (file)
@@ -68,12 +68,13 @@ gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/s
 gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/remote-stub.c -o remote-stub.o\r
 gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/getopt.c -o getopt.o\r
 gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/getopt1.c -o getopt1.o\r
+gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/shuffle.c -o shuffle.o\r
 gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/lib/glob.c -o lib/glob.o\r
 gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/lib/fnmatch.c -o lib/fnmatch.o\r
 @echo off\r
 echo commands.o > respf.$$$\r
 for %%f in (job output dir file misc main read remake rule implicit default variable) do echo %%f.o >> respf.$$$\r
-for %%f in (expand function vpath hash strcache version ar arscan signame remote-stub getopt getopt1) do echo %%f.o >> respf.$$$\r
+for %%f in (expand function vpath hash strcache version ar arscan signame remote-stub getopt getopt1 shuffle) do echo %%f.o >> respf.$$$\r
 for %%f in (lib\glob lib\fnmatch) do echo %%f.o >> respf.$$$\r
 rem gcc  -c -I./src -I%XSRC% -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/guile.c -o guile.o\r
 rem echo guile.o >> respf.$$$\r
index ec6f8a3bfdba6d56014fbfde6d6b8c8ee1621177..33058f0812af17dfd872af59f149a081adb6fe86 100644 (file)
@@ -1,4 +1,4 @@
-.TH MAKE 1 "28 February 2016" "GNU" "User Commands"
+.TH MAKE 1 "31 May 2022" "GNU" "User Commands"
 .SH NAME
 make \- GNU make utility to maintain groups of programs
 .SH SYNOPSIS
@@ -319,6 +319,26 @@ Turn off
 .BR \-w ,
 even if it was turned on implicitly.
 .TP 0.5i
+.BI \-\-shuffle "[=MODE]"
+Enable shuffling of goal and prerequisite ordering.
+.I MODE
+is one of
+.I none
+to disable shuffle mode,
+.I random
+to shuffle prerequisites in random order,
+.I reverse
+to consider prerequisites in reverse order, or an integer
+.I <seed>
+which enables
+.I random
+mode with a specific
+.I seed
+value.  If
+.I MODE
+is omitted the default is
+.IR random .
+.TP 0.5i
 \fB\-W\fR \fIfile\fR, \fB\-\-what\-if\fR=\fIfile\fR, \fB\-\-new\-file\fR=\fIfile\fR, \fB\-\-assume\-new\fR=\fIfile\fR
 Pretend that the target
 .I file
index 8505e8d656b6638944475aefac6cfc1b2b189593..943c0941d2a1c9d8b35161c3eb83e449d9453f5e 100644 (file)
@@ -9422,6 +9422,45 @@ from the top-level @code{make} via @code{MAKEFLAGS}
 (@pxref{Recursion, ,Recursive Use of @code{make}})
 or if you set @samp{-k} in @code{MAKEFLAGS} in your environment.@refill
 
+@item --shuffle[=@var{mode}]
+@cindex @code{--shuffle}
+@c Extra blank line here makes the table look better.
+
+This option enables a form of fuzz-testing of prerequisite relationships.
+When parallelism is enabled (@samp{-j}) the order in which targets are
+built becomes less deterministic.  If prerequisites are not fully declared
+in the makefile this can lead to intermittent and hard-to-track-down build
+failures.
+
+The @samp{--shuffle} option forces @code{make} to purposefully reorder goals
+and prerequisites so target/prerequisite relationships still hold, but
+ordering of prerequisites of a given target are reordered as described below.
+
+The order in which prerequisites are listed in automatic variables is not
+changed by this option.
+
+The @code{.NOTPARALLEL} pseudo-target disables shuffling for that makefile.
+
+The @samp{--shuffle=} option accepts these values:
+
+@table @code
+@item random
+Choose a random seed for the shuffle.  This is the default if no mode is
+specified.  The chosen seed is also provided to sub-@code{make} commands.  The
+seed is included in error messages so that it can be re-used in future runs to
+reproduce the problem or verify that it has been resolved.
+
+@item reverse
+Reverse the order of goals and prerequisites, rather than a random shuffle.
+
+@item @var{seed}
+Use @samp{random} shuffle initialized with the specified seed value.  The
+@var{seed} is an integer.
+
+@item none
+Disable shuffling.  This negates any previous @samp{--shuffle} options.
+@end table
+
 @item -t
 @cindex @code{-t}
 @itemx --touch
index 4a36a3545f9008528af36a66013c72919c9b30a6..86da500c3a6f62721efdb2eb8b8a9dc00f621032 100644 (file)
@@ -76,7 +76,7 @@ $ filelist = "[.src]ar [.src]arscan [.src]commands [.src]default [.src]dir " + -
              "[.src]hash [.src]implicit [.src]job [.src]load [.src]main " + -
              "[.src]misc [.src]read [.src]remake [.src]remote-stub " + -
              "[.src]rule [.src]output [.src]signame [.src]variable " + -
-             "[.src]version [.src]strcache [.src]vpath " + -
+             "[.src]version [.src]shuffle [.src]strcache [.src]vpath " + -
              "[.src]vmsfunctions [.src]vmsify [.src]vms_progname " + -
              "[.src]vms_exit [.src]vms_export_symbol " + -
              "[.lib]alloca [.lib]fnmatch [.lib]glob [.src]getopt1 [.src]getopt"
index e163a058a0b3fc409884cf995c21a37264aeba97..16cfbdd9dd8e0f770f2d296b57beda0c1f6d28b5 100644 (file)
@@ -37,6 +37,7 @@ src/read.c
 src/remake.c
 src/remote-cstms.c
 src/rule.c
+src/shuffle.c
 src/signame.c
 src/strcache.c
 src/variable.c
index fdd3fa30f854b64d092b69ccbdd42a01d13df018..80092076acca8fe433bf602c7e482b76ed3fe086 100644 (file)
--- a/src/dep.h
+++ b/src/dep.h
@@ -46,6 +46,7 @@ struct nameseq
 #define DEP(_t)                                 \
     NAMESEQ (_t);                               \
     struct file *file;                          \
+    _t *shuf;                                   \
     const char *stem;                           \
     unsigned int flags : 8;                     \
     unsigned int changed : 1;                   \
index 392988e4d71decbc73a713fa49af073feb8fc255..4330d787f10bc323740fefe4d6db86f100813cf0 100644 (file)
@@ -108,6 +108,8 @@ struct file
                                    pattern-specific variables.  */
     unsigned int no_diag:1;     /* True if the file failed to update and no
                                    diagnostics has been issued (dontcare). */
+    unsigned int was_shuffled:1; /* Did we already shuffle 'deps'? used when
+                                    --shuffle passes through the graph.  */
   };
 
 
index 9aee7db5234c4c5dafc82b5c583c59c254a8f392..a89994b9196fbd1fcf58d2e73dd2fc22d48e0407 100644 (file)
@@ -22,6 +22,7 @@ this program.  If not, see <http://www.gnu.org/licenses/>.  */
 #include "variable.h"
 #include "job.h"      /* struct child, used inside commands.h */
 #include "commands.h" /* set_file_variables */
+#include "shuffle.h"
 #include <assert.h>
 
 static int pattern_search (struct file *file, int archive,
@@ -1053,8 +1054,14 @@ pattern_search (struct file *file, int archive,
 
       dep->next = file->deps;
       file->deps = dep;
+
+      /* The file changed its dependencies; schedule the shuffle.  */
+      file->was_shuffled = 0;
     }
 
+  if (!file->was_shuffled)
+    shuffle_deps_recursive (file->deps);
+
   if (!tryrules[foundrule].checked_lastslash)
     {
       /* Always allocate new storage, since STEM might be on the stack for an
index d24288ac613cf4c4abbf75eed69ee4343a6c94b9..80d261bbae81c43bd54a759ffab2aaad7b23471d 100644 (file)
--- a/src/job.c
+++ b/src/job.c
@@ -25,6 +25,8 @@ this program.  If not, see <http://www.gnu.org/licenses/>.  */
 #include "commands.h"
 #include "variable.h"
 #include "os.h"
+#include "dep.h"
+#include "shuffle.h"
 
 /* Default shell to use.  */
 #ifdef WINDOWS32
@@ -539,6 +541,7 @@ child_error (struct child *child,
   const struct file *f = child->file;
   const floc *flocp = &f->cmds->fileinfo;
   const char *nm;
+  const char *smode;
   size_t l;
 
   if (ignored && run_silent)
@@ -564,18 +567,29 @@ child_error (struct child *child,
 
   l = strlen (pre) + strlen (nm) + strlen (f->name) + strlen (post);
 
+  smode = shuffle_get_mode ();
+  if (smode)
+    {
+#define SHUFFLE_PREFIX " shuffle="
+      char *a = alloca (CSTRLEN(SHUFFLE_PREFIX) + strlen (smode) + 1);
+      sprintf (a, SHUFFLE_PREFIX "%s", smode);
+      smode = a;
+      l += strlen (smode);
+#undef SHUFFLE_PREFIX
+    }
+
   OUTPUT_SET (&child->output);
 
   show_goal_error ();
 
   if (exit_sig == 0)
-    error (NILF, l + INTSTR_LENGTH,
-           _("%s[%s: %s] Error %d%s"), pre, nm, f->name, exit_code, post);
+    error (NILF, l + INTSTR_LENGTH, _("%s[%s: %s] Error %d%s%s"),
+           pre, nm, f->name, exit_code, post, smode ? smode : "");
   else
     {
       const char *s = strsignal (exit_sig);
-      error (NILF, l + strlen (s) + strlen (dump),
-             "%s[%s: %s] %s%s%s", pre, nm, f->name, s, dump, post);
+      error (NILF, l + strlen (s) + strlen (dump), "%s[%s: %s] %s%s%s%s",
+             pre, nm, f->name, s, dump, post, smode ? smode : "");
     }
 
   OUTPUT_UNSET ();
index 0d2dff720a6300b0d99d0673a3a1036555545d5f..5912b06cf484222f555e8d7276e8ce2357cbc395 100644 (file)
@@ -24,6 +24,7 @@ this program.  If not, see <http://www.gnu.org/licenses/>.  */
 #include "rule.h"
 #include "debug.h"
 #include "getopt.h"
+#include "shuffle.h"
 
 #include <assert.h>
 #ifdef _AMIGA
@@ -233,6 +234,10 @@ static const int inf_jobs = 0;
 
 static char *jobserver_auth = NULL;
 
+/* Shuffle mode for goals and prerequisites.  */
+
+static char *shuffle_mode = NULL;
+
 /* Handle for the mutex used on Windows to synchronize output of our
    children under -O.  */
 
@@ -361,6 +366,9 @@ static const char *const usage[] =
     N_("\
   -R, --no-builtin-variables  Disable the built-in variable settings.\n"),
     N_("\
+  --shuffle[={SEED|random|reverse|none}]\n\
+                              Perform shuffle of prerequisites and goals.\n"),
+    N_("\
   -s, --silent, --quiet       Don't echo recipes.\n"),
     N_("\
   --no-silent                 Echo recipes (disable --silent mode).\n"),
@@ -479,6 +487,7 @@ static const struct command_switch switches[] =
     { CHAR_MAX+9, string, &jobserver_auth, 1, 0, 0, 0, 0, "jobserver-fds" },
     /* There is special-case handling for this in decode_switches() as well.  */
     { TEMP_STDIN_OPT, filename, &makefiles, 0, 0, 0, 0, 0, "temp-stdin" },
+    { CHAR_MAX+11, string, &shuffle_mode, 1, 1, 0, "random", 0, "shuffle" },
     { 0, 0, 0, 0, 0, 0, 0, 0, 0 }
   };
 
@@ -1504,6 +1513,22 @@ main (int argc, char **argv, char **envp)
       arg_job_slots = env_slots;
   }
 
+  /* Handle shuffle mode argument.  */
+  if (shuffle_mode)
+    {
+      const char *effective_mode;
+      shuffle_set_mode (shuffle_mode);
+
+      /* Write fixed seed back to argument list to propagate mode and
+         fixed seed to child $(MAKE) runs.  */
+      free (shuffle_mode);
+      effective_mode = shuffle_get_mode ();
+      if (effective_mode)
+        shuffle_mode = xstrdup (effective_mode);
+      else
+        shuffle_mode = NULL;
+    }
+
   /* Set a variable specifying whether stdout/stdin is hooked to a TTY.  */
 #ifdef HAVE_ISATTY
   if (isatty (fileno (stdout)))
@@ -2759,6 +2784,10 @@ main (int argc, char **argv, char **envp)
       O (fatal, NILF, _("No targets specified and no makefile found"));
     }
 
+  /* Shuffle prerequisites to catch makefiles with incomplete depends. */
+
+  shuffle_goaldeps_recursive (goals);
+
   /* Update the goals.  */
 
   DB (DB_BASIC, (_("Updating goal targets....\n")));
index 09cfb8ca769faddab316143c8096712c0cd6378a..2930a5bb0d81e146a7ba691f314af7dda4ece8b8 100644 (file)
@@ -93,8 +93,8 @@ update_goal_chain (struct goaldep *goaldeps)
   enum update_status status = us_none;
 
   /* Duplicate the chain so we can remove things from it.  */
-
-  struct dep *goals = copy_dep_chain ((struct dep *)goaldeps);
+  struct dep *goals_orig = copy_dep_chain ((struct dep *)goaldeps);
+  struct dep *goals = goals_orig;
 
   goal_list = rebuilding_makefiles ? goaldeps : NULL;
 
@@ -108,7 +108,7 @@ update_goal_chain (struct goaldep *goaldeps)
 
   while (goals != 0)
     {
-      struct dep *g, *lastgoal;
+      struct dep *gu, *g, *lastgoal;
 
       /* Start jobs that are waiting for the load to go down.  */
 
@@ -119,13 +119,15 @@ update_goal_chain (struct goaldep *goaldeps)
       reap_children (1, 0);
 
       lastgoal = 0;
-      g = goals;
-      while (g != 0)
+      gu = goals;
+      while (gu != 0)
         {
           /* Iterate over all double-colon entries for this file.  */
           struct file *file;
           int stop = 0, any_not_updated = 0;
 
+          g = gu->shuf ? gu->shuf : gu;
+
           goal_dep = g;
 
           for (file = g->file->double_colon ? g->file->double_colon : g->file;
@@ -235,31 +237,30 @@ update_goal_chain (struct goaldep *goaldeps)
 
               /* This goal is finished.  Remove it from the chain.  */
               if (lastgoal == 0)
-                goals = g->next;
+                goals = gu->next;
               else
-                lastgoal->next = g->next;
-
-              /* Free the storage.  */
-              free (g);
+                lastgoal->next = gu->next;
 
-              g = lastgoal == 0 ? goals : lastgoal->next;
+              gu = lastgoal == 0 ? goals : lastgoal->next;
 
               if (stop)
                 break;
             }
           else
             {
-              lastgoal = g;
-              g = g->next;
+              lastgoal = gu;
+              gu = gu->next;
             }
         }
 
       /* If we reached the end of the dependency graph update CONSIDERED
          for the next pass.  */
-      if (g == 0)
+      if (gu == 0)
         ++considered;
     }
 
+  free_dep_chain (goals_orig);
+
   if (rebuilding_makefiles)
     {
       touch_flag = t;
@@ -424,7 +425,7 @@ update_file_1 (struct file *file, unsigned int depth)
   FILE_TIMESTAMP this_mtime;
   int noexist, must_make, deps_changed;
   struct file *ofile;
-  struct dep *d, *ad;
+  struct dep *du, *d, *ad;
   struct dep amake;
   int running = 0;
 
@@ -532,16 +533,18 @@ update_file_1 (struct file *file, unsigned int depth)
       struct dep *lastd = 0;
 
       /* Find the deps we're scanning */
-      d = ad->file->deps;
+      du = ad->file->deps;
       ad = ad->next;
 
-      while (d)
+      while (du)
         {
           enum update_status new;
           FILE_TIMESTAMP mtime;
           int maybe_make;
           int dontcare = 0;
 
+          d = du->shuf ? du->shuf : du;
+
           check_renamed (d->file);
 
           mtime = file_mtime (d->file);
@@ -551,14 +554,16 @@ update_file_1 (struct file *file, unsigned int depth)
             {
               OSS (error, NILF, _("Circular %s <- %s dependency dropped."),
                    file->name, d->file->name);
+
               /* We cannot free D here because our the caller will still have
                  a reference to it when we were called recursively via
                  check_dep below.  */
               if (lastd == 0)
-                file->deps = d->next;
+                file->deps = du->next;
               else
-                lastd->next = d->next;
-              d = d->next;
+                lastd->next = du->next;
+
+              du = du->next;
               continue;
             }
 
@@ -607,8 +612,8 @@ update_file_1 (struct file *file, unsigned int depth)
             d->changed = ((file_mtime (d->file) != mtime)
                           || (mtime == NONEXISTENT_MTIME));
 
-          lastd = d;
-          d = d->next;
+          lastd = du;
+          du = du->next;
         }
     }
 
@@ -617,58 +622,61 @@ update_file_1 (struct file *file, unsigned int depth)
 
   if (must_make || always_make_flag)
     {
-      for (d = file->deps; d != 0; d = d->next)
-        if (d->file->intermediate)
-          {
-            enum update_status new;
-            int dontcare = 0;
+      for (du = file->deps; du != 0; du = du->next)
+        {
+          d = du->shuf ? du->shuf : du;
+          if (d->file->intermediate)
+            {
+              enum update_status new;
+              int dontcare = 0;
 
-            FILE_TIMESTAMP mtime = file_mtime (d->file);
-            check_renamed (d->file);
-            d->file->parent = file;
+              FILE_TIMESTAMP mtime = file_mtime (d->file);
+              check_renamed (d->file);
+              d->file->parent = file;
 
-            /* Inherit dontcare flag from our parent. */
-            if (rebuilding_makefiles)
-              {
-                dontcare = d->file->dontcare;
-                d->file->dontcare = file->dontcare;
-              }
+              /* Inherit dontcare flag from our parent. */
+              if (rebuilding_makefiles)
+                {
+                  dontcare = d->file->dontcare;
+                  d->file->dontcare = file->dontcare;
+                }
 
-            /* We may have already considered this file, when we didn't know
-               we'd need to update it.  Force update_file() to consider it and
-               not prune it.  */
-            d->file->considered = 0;
+              /* We may have already considered this file, when we didn't know
+                 we'd need to update it.  Force update_file() to consider it and
+                 not prune it.  */
+              d->file->considered = 0;
 
-            new = update_file (d->file, depth);
-            if (new > dep_status)
-              dep_status = new;
+              new = update_file (d->file, depth);
+              if (new > dep_status)
+                dep_status = new;
 
-            /* Restore original dontcare flag. */
-            if (rebuilding_makefiles)
-              d->file->dontcare = dontcare;
+              /* Restore original dontcare flag. */
+              if (rebuilding_makefiles)
+                d->file->dontcare = dontcare;
 
-            check_renamed (d->file);
+              check_renamed (d->file);
 
-            {
-              struct file *f = d->file;
-              if (f->double_colon)
-                f = f->double_colon;
-              do
-                {
-                  running |= (f->command_state == cs_running
-                              || f->command_state == cs_deps_running);
-                  f = f->prev;
-                }
-              while (f != 0);
-            }
+              {
+                struct file *f = d->file;
+                if (f->double_colon)
+                  f = f->double_colon;
+                do
+                  {
+                    running |= (f->command_state == cs_running
+                                || f->command_state == cs_deps_running);
+                    f = f->prev;
+                  }
+                while (f != 0);
+              }
 
-            if (dep_status && !keep_going_flag)
-              break;
+              if (dep_status && !keep_going_flag)
+                break;
 
-            if (!running)
-              d->changed = ((file->phony && file->cmds != 0)
-                            || file_mtime (d->file) != mtime);
-          }
+              if (!running)
+                d->changed = ((file->phony && file->cmds != 0)
+                              || file_mtime (d->file) != mtime);
+            }
+        }
     }
 
   finish_updating (file);
diff --git a/src/shuffle.c b/src/shuffle.c
new file mode 100644 (file)
index 0000000..ea6e836
--- /dev/null
@@ -0,0 +1,240 @@
+/* Provide prerequisite shuffle support.
+Copyright (C) 2022 Free Software Foundation, Inc.
+This file is part of GNU Make.
+
+GNU Make is free software; you can redistribute it and/or modify it under the
+terms of the GNU General Public License as published by the Free Software
+Foundation; either version 3 of the License, or (at your option) any later
+version.
+
+GNU Make is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include "makeint.h"
+
+#include "shuffle.h"
+
+#include "filedef.h"
+#include "dep.h"
+
+/* Supported shuffle modes.  */
+static void random_shuffle_array (void ** a, size_t len);
+static void reverse_shuffle_array (void ** a, size_t len);
+static void identity_shuffle_array (void ** a, size_t len);
+
+/* The way goals and rules are shuffled during update.  */
+enum shuffle_mode
+  {
+    /* No shuffle data is populated or used.  */
+    sm_none,
+    /* Random within dependency list.  */
+    sm_random,
+    /* Inverse order.  */
+    sm_reverse,
+    /* identity order. Differs from SM_NONE by explicitly populating
+       the traversal order.  */
+    sm_identity,
+  };
+
+/* Shuffle configuration.  */
+static struct
+  {
+    enum shuffle_mode mode;
+    unsigned int seed;
+    void (*shuffler) (void **a, size_t len);
+    char strval[INTSTR_LENGTH];
+  } config = { sm_none, 0, NULL, "" };
+
+/* Return string value of --shuffle= option passed.
+   If none was passed or --shuffle=none was used function
+   returns NULL.  */
+const char *
+shuffle_get_mode ()
+{
+  return config.strval[0] ? config.strval : NULL;
+}
+
+void
+shuffle_set_mode (const char *cmdarg)
+{
+  /* Parse supported '--shuffle' mode.  */
+  if (strcasecmp (cmdarg, "random") == 0)
+    {
+      config.mode = sm_random;
+      config.seed = (unsigned int) (time (NULL) ^ make_pid ());
+    }
+  else if (strcasecmp (cmdarg, "reverse") == 0)
+    config.mode = sm_reverse;
+  else if (strcasecmp (cmdarg, "identity") == 0)
+    config.mode = sm_identity;
+  else if (strcasecmp (cmdarg, "none") == 0)
+    config.mode = sm_none;
+  /* Assume explicit seed if starts from a digit.  */
+  else
+    {
+      const char *err;
+      config.mode = sm_random;
+      config.seed = make_toui (cmdarg, &err);
+
+      if (err)
+        {
+          OS (error, NILF, _("invalid shuffle mode: '%s'"), cmdarg);
+          die (MAKE_FAILURE);
+        }
+    }
+
+  switch (config.mode)
+    {
+    case sm_random:
+      config.shuffler = random_shuffle_array;
+      sprintf (config.strval, "%u", config.seed);
+      break;
+    case sm_reverse:
+      config.shuffler = reverse_shuffle_array;
+      strcpy (config.strval, "reverse");
+      break;
+    case sm_identity:
+      config.shuffler = identity_shuffle_array;
+      strcpy (config.strval, "identity");
+      break;
+    case sm_none:
+      config.strval[0] = '\0';
+      break;
+    }
+}
+
+/* Shuffle array elements using RAND().  */
+static void
+random_shuffle_array (void **a, size_t len)
+{
+  size_t i;
+  for (i = 0; i < len; i++)
+    {
+      void *t;
+
+      /* Pick random element and swap. */
+      unsigned int j = rand () % len;
+      if (i == j)
+        continue;
+
+      /* Swap. */
+      t = a[i];
+      a[i] = a[j];
+      a[j] = t;
+    }
+}
+
+/* Shuffle array elements using reverse order.  */
+static void
+reverse_shuffle_array (void **a, size_t len)
+{
+  size_t i;
+  for (i = 0; i < len / 2; i++)
+    {
+      void *t;
+
+      /* Pick mirror and swap. */
+      unsigned int j = len - 1 - i;
+
+      /* Swap. */
+      t = a[i];
+      a[i] = a[j];
+      a[j] = t;
+    }
+}
+
+/* Shuffle array elements using identity order.  */
+static void
+identity_shuffle_array (void **a UNUSED, size_t len UNUSED)
+{
+  /* No-op!  */
+}
+
+/* Shuffle list of dependencies by populating '->next'
+   field in each 'struct dep'.  */
+static void
+shuffle_deps (struct dep *deps)
+{
+  size_t ndeps = 0;
+  struct dep *dep;
+  void **da;
+  void **dp;
+
+  for (dep = deps; dep; dep = dep->next)
+    ndeps++;
+
+  if (ndeps == 0)
+    return;
+
+  /* Allocate array of all deps, store, shuffle, write back.  */
+  da = xmalloc (sizeof (struct dep *) * ndeps);
+
+  /* Store locally.  */
+  for (dep = deps, dp = da; dep; dep = dep->next, dp++)
+    *dp = dep;
+
+  /* Shuffle.  */
+  config.shuffler (da, ndeps);
+
+  /* Write back.  */
+  for (dep = deps, dp = da; dep; dep = dep->next, dp++)
+    dep->shuf = *dp;
+
+  free (da);
+}
+
+/* Shuffle 'deps' of each 'file' recursively.  */
+static void
+shuffle_file_deps_recursive (struct file *f)
+{
+  struct dep *dep;
+
+  /* Implicit rules do not always provide any depends.  */
+  if (!f)
+    return;
+
+  /* Avoid repeated shuffles and loops.  */
+  if (f->was_shuffled)
+    return;
+  f->was_shuffled = 1;
+
+  shuffle_deps (f->deps);
+
+  /* Shuffle dependencies. */
+  for (dep = f->deps; dep; dep = dep->next)
+    shuffle_file_deps_recursive (dep->file);
+}
+
+/* Shuffle goal dependencies first, then shuffle dependency list
+   of each file reachable from goaldep recursively.  Used by
+   --shuffle flag to introduce artificial non-determinism in build
+   order.  .*/
+
+void
+shuffle_deps_recursive (struct dep *deps)
+{
+  struct dep *dep;
+
+  /* Exit early if shuffling was not requested.  */
+  if (config.mode == sm_none)
+    return;
+
+  /* Do not reshuffle targets if Makefile is explicitly marked as
+     problematic for parallelism.  */
+  if (not_parallel)
+    return;
+
+  /* Set specific seed at the top level of recursion.  */
+  if (config.mode == sm_random)
+    srand (config.seed);
+
+  shuffle_deps (deps);
+
+  /* Shuffle dependencies. */
+  for (dep = deps; dep; dep = dep->next)
+    shuffle_file_deps_recursive (dep->file);
+}
diff --git a/src/shuffle.h b/src/shuffle.h
new file mode 100644 (file)
index 0000000..a3b275e
--- /dev/null
@@ -0,0 +1,26 @@
+/* Declarations for target shuffling support.
+Copyright (C) 2022-2022 Free Software Foundation, Inc.
+This file is part of GNU Make.
+
+GNU Make is free software; you can redistribute it and/or modify it under the
+terms of the GNU General Public License as published by the Free Software
+Foundation; either version 3 of the License, or (at your option) any later
+version.
+
+GNU Make is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+struct dep;
+struct goaldep;
+
+void shuffle_set_mode (const char *cmdarg);
+const char * shuffle_get_mode ();
+void shuffle_deps_recursive (struct dep* g);
+
+#define shuffle_goaldeps_recursive(_g) do{              \
+        shuffle_deps_recursive ((struct dep *)_g);      \
+    } while(0)
diff --git a/tests/scripts/options/shuffle b/tests/scripts/options/shuffle
new file mode 100644 (file)
index 0000000..e037ed1
--- /dev/null
@@ -0,0 +1,119 @@
+#                                                                    -*-perl-*-
+
+$description = "Test the --shuffle option.";
+
+$details = "Verify that --shuffle has expected effect on target order and argument order.";
+
+#
+# Test --shuffle=random
+#
+
+# TEST 1: Fixed seed should yield the same order from run to run.
+
+$makefile = &get_tmpfile;
+
+open(MAKEFILE, "> $makefile");
+print MAKEFILE <<'EOF';
+# More target prerequisites lower collision chance in TEST 2
+all: a_ b_ c_ d_ e_ f_ g_ i_ j_ k_ l_
+%: ; echo $@
+EOF
+close(MAKEFILE);
+
+$log1 = &get_logfile;
+$log2 = &get_logfile;
+&run_make_with_options($makefile, "--shuffle=12345", $log1);
+&run_make_with_options($makefile, "--shuffle=12345", $log2);
+
+&compare_output(&read_file_into_string($log1), $log2);
+
+# TEST 2: Sequential runs should produce different orders.
+
+$log3 = &get_logfile;
+$log4 = &get_logfile;
+&run_make_with_options($makefile, "--shuffle", $log3);
+&run_make_with_options($makefile, "--shuffle", $log4);
+
+++$tests_run;
+if (&read_file_into_string($log3) ne &read_file_into_string($log4)) {
+  print "ok\n" if $debug;
+  ++$tests_passed;
+}
+
+#
+# Test --shuffle=reverse
+#
+
+run_make_test('
+%: ; @echo $@
+all: a b c
+',
+              '--shuffle=reverse', "c\nb\na\nall");
+
+run_make_test('
+%: ; @echo $@
+all: a b c
+',
+              '--shuffle=none', "a\nb\nc\nall");
+
+run_make_test('
+%: ; @echo $@
+all: a b c
+',
+              '--shuffle=identity', "a\nb\nc\nall");
+
+# Make sure prerequisites get reverse order and commands don't get affected.
+run_make_test('
+all: foo.o ; @echo $@
+%.o : %.c ; @echo cc -c -o $@ $<
+foo.o : foo.c foo.h bar.h baz.h
+%.h: ; @echo $@
+%.c: ; @echo $@
+',
+              '--shuffle=reverse',
+              "baz.h\nbar.h\nfoo.h\nfoo.c\ncc -c -o foo.o foo.c\nall");
+
+# Make sure pattern prerequisites get reverse order and commands don't get
+# affected.
+run_make_test('
+all: foo_ ; @echo $@
+foo%: arg%1 arg%2 arg%3 arg%4 ; @echo bld $@ $< $(word 3,$^) $(word 2,$^) $(word 4,$^)
+
+arg%: ; @echo $@
+',
+              '--shuffle=reverse',
+              "arg_4\narg_3\narg_2\narg_1\nbld foo_ arg_1 arg_3 arg_2 arg_4\nall");
+
+# Check if make can survive circular dependency.
+run_make_test('
+all: a_ b_ ; @echo $@
+%_: ; @echo $@
+
+a_: b_
+b_: a_
+',
+              '--shuffle=reverse', "#MAKE#: Circular a_ <- b_ dependency dropped.\na_\nb_\nall");
+
+# Check if order-only dependencies get reordered.
+run_make_test('
+all: a_ ; @echo $@
+%_: ; @echo $@
+a_: b_ c_ | d_ e_
+',
+              '--shuffle=reverse', "e_\nd_\nc_\nb_\na_\nall");
+
+# Check if goals are reordered.
+run_make_test('
+%_: ; @echo $@
+',
+              '--shuffle=reverse a_ b_ c_', "c_\nb_\na_");
+
+# .NOTPARALLEL should prevent reordering from happening.
+run_make_test('
+%_: ; @echo $@
+# disable shuffling
+.NOTPARALLEL:
+',
+              '--shuffle=reverse a_ b_ c_', "a_\nb_\nc_");
+
+1;