On Sat, Feb 19, 2022 at 09:57:13AM +0200, Eli Zaretskii wrote: > > * Makefile.am (make_SRCS): Add src/shuf.h and src/shuf.c file references. > > * builddos.bat: Add shuf.o into build script.
> This should also update build_w32.bat, which is used to build Make on > MS-Windows. I think NEWS should also call out the new feature. Sounds good! Added there and to makefile.com. > > +Note that @code{make --shuffle clean all install} does reorder goals > > +similar to how @code{make -j clean all install} runs all targets in > > These should use @kbd, not @code, since you are describing commands > the user will type. I also recommend to wrap each one in @w{..}, so > that these (long) command isn't broken between 2 lines. Changed to @w{@kbd{..}}. > > +@samp{--shuffle=} accepts an optional value: > > I think nowadays it's customary to use @option as markup for > command-line options (unless Make wants to keep its manual compatible > to very old versions of Texinfo -- this is up to Paul to decide). I left it as is as I see on @option{} being used in make.texi. Can do as a separate patch if others agree. > > + /* TODO: could we make this shuffle more deterministic and less > > + dependent on current filesystem state? */ > > + if (! file->was_shuffled) > > + shuffle_file_deps_recursive (file); > > Should this TODO be resolved before installing the feature? Sounds good. Added re-seeding for each implicit dependency to get more deterministic shuffling. > > + /* Handle shuffle mode argument. */ > > + if (shuffle_mode_arg) > > + { > > + if (streq (shuffle_mode_arg, "none")) > > + sm = sm_none; > > + else if (streq (shuffle_mode_arg, "identity")) > > + sm = sm_identity; > > + else if (streq (shuffle_mode_arg, "reverse")) > > + sm = sm_reverse; > > + else if (streq (shuffle_mode_arg, "random")) > > + sm = sm_random; > > + /* Assume explicit seed if starts from a digit. */ > > + else if (ISDIGIT (shuffle_mode_arg[0])) > > + { > > + sm = sm_random; > > + shuffle_seed = atoi (shuffle_mode_arg); > > + } > > + else > > + { > > + O (error, NILF, _("error: unsupported --shuffle= option.")); > > + die (MAKE_FAILURE); > > + } > > + set_shuffle_mode (sm, shuffle_seed); > > + > > + /* Write fixed seed back to argument list to propagate fixed seed > > + to child $(MAKE) runs. */ > > + free (shuffle_mode_arg); > > + shuffle_mode_arg = xstrdup (shuffle_get_str_value ()); > > + } > > Should this be factored out and put on shuf.c? Sounds goo.d Moved out to shuf.c. Only left small option rewrite bit in main.c. > > + switch (mode) > > + { > > + case sm_random: > > + if (seed == 0) > > + seed = (int)time(NULL); > > + srand (seed); > > + shuffler = random_shuffle_array; > > + sprintf(shuffle_str_value, "%d", seed); > ^^ > Stylistic comment: I believe our style is to leave one space between > the function name and the opening parenthesis (here and elsewhere in > the patch). Good catch! Added whitespace everywhere in the new code. > > +random_shuffle_array (void ** a, size_t len) > > +{ > > + for (unsigned int i = 0; i < len; i++) > ^^^^^^^^^^^^^^^^^^^ > This requires some minimal level of ANSI C support that I'm not sure > Make already requires. Older compilers will error out here. i think it does. Moved out to a separate declaration and switched to size_t while at it to match function argument type. > > + /* TODO: below is almost identical to goaldeps shuffle. The only > > difference > > + is a type change. Worth deduplicating? */ > > Another TODO. Deduplicated by coercing 'strct goaldep*' traversal into 'struct dep *'. Similar to other dep.h primitives. > > + /* Shuffle dependencies. */ > > + /* TODO: this is a naive recursion. Is it good enough? Or better > > change it > > + to queue based implementation? */ > > And another one. Dropped a TODO as it's no worse than naive recursion in update_file_1. > > --- /dev/null > > +++ b/tests/scripts/options/shuffle-reverse > > This test doesn't seem to test the random option. I understand why > this is not easy, but shouldn't it still be tested, if only to make > sure the option works, and also that different seeds produce different > outcomes? Added tests/scripts/misc/rand-shuf test to test for random order and for unchanged order for a fixed seed. Attaching v3-0001-Add-shuffle-argument-support.patch with all the above applied. Thank you for thorough review, Eli! -- Sergei
>From 42a1df1b2873a3e362936cef474273dca08d2c57 Mon Sep 17 00:00:00 2001 From: Sergei Trofimovich <siarh...@google.com> Date: Fri, 4 Feb 2022 22:43:28 +0000 Subject: [PATCH v3] Add '--shuffle' argument support The idea of the change is to introduce non-deterministic ordering into goal and prerequisite traversal to imitate non-deterministic ordering of 'make -j g1 g2 g3' mode. The implementation is to 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 goals or prerequisites are updated we use shuffled order when '--shuffle' is passed to make. When recipes are evaluated we use syntactic order of parameters. Sample execution looks like: $ while echo ======= && rm -f a b && make --shuffle; do sleep 1; done ======= touch a cp a b ======= cp a b cp: cannot stat 'a': No such file or directory make: *** [Makefile:5: b] Error 1 --shuffle=1644097687 Note: here first run succeeds while second run fails and exposes the bug in Makefile. --shuffle=1644097687 allows us to rerun the same build sequence again. There are a few shuffle modes available: - --shuffle (or --shuffle=random): use random seed and produce random order - --shuffle=identity: use original order, but store and use it explicitly (most useful for GNU make testing) - --shuffle=reverse: use inverse order for every goal list and prerequisite list. It is frequently enough to trigger simplest bugs. - --shuffle=none: disable shuffle infrastructure completely. Useful to negate effect of previous --shuffle options. * Makefile.am (make_SRCS): Add src/shuf.h and src/shuf.c file references. * builddos.bat: Add shuf.o into build script. * build_w32.bat: Add shuf.c into build script. * doc/make.1: Add '--shuffle' option description. * doc/make.texi: Add '--shuffle' option description. * makefile.com: Add shuf.c into build script. * src/dep.h (DEP): Add 'shuf' field to store shuffled order. * src/filedef.h (struct file): Add 'was_shuffled' bit flag to guard against circular dependencies. * src/implicit.c (pattern_search): Reshuffle dependencies for targets dynamically extended with dependencies from implicit rules. * src/job.c (child_error): Print shuffle parameter used for dependency shuffling. * src/main.c: Add '--shuffle' option handling. * src/remake.c (update_goal_chain): Change goal traversal order to shuffled. * src/shuf.c: New file with shuffle infrastructure. * src/shuf.h: New file with shuffle infrastructure declarations. * tests/scripts/misc/rand-shuf: New file with randomness tests. * tests/scripts/options/shuffle-reverse: New file with basic tests. --- Makefile.am | 5 +- build_w32.bat | 1 + builddos.bat | 3 +- doc/make.1 | 34 ++++ doc/make.texi | 51 ++++++ makefile.com | 2 +- src/dep.h | 1 + src/filedef.h | 2 + src/implicit.c | 7 + src/job.c | 21 ++- src/main.c | 29 ++++ src/remake.c | 138 ++++++++------- src/shuf.c | 241 ++++++++++++++++++++++++++ src/shuf.h | 23 +++ tests/scripts/misc/rand-shuf | 40 +++++ tests/scripts/options/shuffle-reverse | 112 ++++++++++++ 16 files changed, 638 insertions(+), 72 deletions(-) create mode 100644 src/shuf.c create mode 100644 src/shuf.h create mode 100644 tests/scripts/misc/rand-shuf create mode 100644 tests/scripts/options/shuffle-reverse diff --git a/Makefile.am b/Makefile.am index 9be60fec..3ad19e0c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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/shuf.h src/shuf.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/build_w32.bat b/build_w32.bat index 17066d72..06a234e1 100755 --- a/build_w32.bat +++ b/build_w32.bat @@ -257,6 +257,7 @@ call :Compile src/read call :Compile src/remake call :Compile src/remote-stub call :Compile src/rule +call :Compile src/shuf call :Compile src/signame call :Compile src/strcache call :Compile src/variable diff --git a/builddos.bat b/builddos.bat index 9cecabec..cfb224c2 100644 --- a/builddos.bat +++ b/builddos.bat @@ -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 gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/getopt.c -o getopt.o gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/getopt1.c -o getopt1.o +gcc -c -I./src -I%XSRC%/src -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/src/shuf.c -o shuf.o 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 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 @echo off echo commands.o > respf.$$$ for %%f in (job output dir file misc main read remake rule implicit default variable) do echo %%f.o >> respf.$$$ -for %%f in (expand function vpath hash strcache version ar arscan signame remote-stub getopt getopt1) do echo %%f.o >> respf.$$$ +for %%f in (expand function vpath hash strcache version ar arscan signame remote-stub getopt getopt1 shuf) do echo %%f.o >> respf.$$$ for %%f in (lib\glob lib\fnmatch) do echo %%f.o >> respf.$$$ rem gcc -c -I./src -I%XSRC% -I./lib -I%XSRC%/lib -DHAVE_CONFIG_H -O2 -g %XSRC%/guile.c -o guile.o rem echo guile.o >> respf.$$$ diff --git a/doc/make.1 b/doc/make.1 index cca833fa..bc347016 100644 --- a/doc/make.1 +++ b/doc/make.1 @@ -319,6 +319,40 @@ Turn off .BR \-w , even if it was turned on implicitly. .TP 0.5i +.BI \-\-shuffle "[=MODE]" +Enable goal and prerequisite dependency order shuffling at execution. +Simulate reordering similar to one frequently seen in parallel ( +.B \-j +) execution mode. Shuffling is useful to increase possibility to trigger +bugs that stem from incompletely specified dependencies in targets. + +If the +.I MODE +is omitted, then the behavior is the same as if +.I random +was specified. +.I MODE +may be one of the following values: +.I random +to shuffle dependencies in random order, +.I identity +to preserve dependencies in original order while +using shuffling infrastructure (useful for debugging +.B make +itself, does not affect actual ordering), +.I reverse +reverses order for every goal and dependency, +.I none +disables shuffling infrastructure completely +(identical to not passing +.B \-\-shuffle +at all), +.I <seed> +enables +.I random +mode and specifies exact random seed (to reproduce exactly the same build +order on rerun). +.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 diff --git a/doc/make.texi b/doc/make.texi index 14476515..3e6b5bbe 100644 --- a/doc/make.texi +++ b/doc/make.texi @@ -9422,6 +9422,57 @@ 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. + +The primary purpose of this option is to expose missing or incomplete +dependencies in Makefiles designed to work in parallel (@samp{-j}) +execution mode. + +When executed in order build parallelism problems are sometimes only +seen on heavy loaded machines with high parallelism. @samp{--shuffle} +allows triggering subset of these bugs by reordering goals and targets. +That way we have a chance to expose missing dependency even in serial +(@samp{-j1}) mode! + +Note that @w{@kbd{make --shuffle clean all install}} does reorder goals +similar to how @w{@kbd{make -j clean all install}} runs all targets in +parallel. + +@samp{--shuffle=} accepts an optional value: + +@table @code +@item random +Shuffle dependencies in random order. This is equivalent to using +@samp{--shuffle} without extra parameters. + +Calls to child @code{$(MAKE)} passes through seed value picked by parent +process. That way we can reproduce build order by passing seed value +explicitly on future reruns. + +@item identity +Preserve dependencies in original order while using shuffling +infrastructure (useful for debugging @code{make} itself). Functionally +identical to @samp{--shuffle=none}. + +@item reverse +Reverse order of every goal and dependency. It's a simple shuffling +scheme that guarantees predictable ordering that differs from default. +Useful for @code{make} own test suite and for lightweight deterministic +test of a build system. + +@item none +Disable shuffling infrastructure completely (identical to not passing +@samp{--shuffle} option at all). Useful to explicitly negate of previous +@samp{--shuffle} options. + +@item <@var{seed}> +Enables @samp{random} mode and specified exact random seed (useful +when if is desirable to reproduce exactly the same build order on rerun). +@var{seed} has to be a numeric value. Example use is @samp{--shuffle=12345}. +@end table + @item -t @cindex @code{-t} @itemx --touch diff --git a/makefile.com b/makefile.com index eff906ef..c86a673a 100644 --- a/makefile.com +++ b/makefile.com @@ -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]shuf [.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" diff --git a/src/dep.h b/src/dep.h index e492a0b3..f7556ce2 100644 --- 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; \ diff --git a/src/filedef.h b/src/filedef.h index 0b9f6e17..d36a6011 100644 --- a/src/filedef.h +++ b/src/filedef.h @@ -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. */ }; diff --git a/src/implicit.c b/src/implicit.c index 2b5bdce3..bdefd775 100644 --- a/src/implicit.c +++ b/src/implicit.c @@ -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 "shuf.h" #include <assert.h> static int pattern_search (struct file *file, int archive, @@ -1029,8 +1030,14 @@ pattern_search (struct file *file, int archive, dep->next = file->deps; file->deps = dep; + + /* File changed it's dependencies. Schedule regeneration. */ + 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 diff --git a/src/job.c b/src/job.c index 9ae3cd6b..cd5c83f4 100644 --- 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 "shuf.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 *shuf; size_t l; if (ignored && run_silent) @@ -562,7 +565,18 @@ child_error (struct child *child, nm = a; } - l = strlen (pre) + strlen (nm) + strlen (f->name) + strlen (post); + shuf = shuffle_get_mode (); + if (shuf) + { + char *a = alloca (11 + strlen (shuf) + 1); + sprintf (a, " --shuffle=%s", shuf); + shuf = a; + } + else + shuf = ""; + + l = strlen (pre) + strlen (nm) + strlen (f->name) + strlen (post) + + strlen (shuf); OUTPUT_SET (&child->output); @@ -570,12 +584,13 @@ child_error (struct child *child, if (exit_sig == 0) error (NILF, l + INTSTR_LENGTH, - _("%s[%s: %s] Error %d%s"), pre, nm, f->name, exit_code, post); + _("%s[%s: %s] Error %d%s%s"), pre, nm, f->name, exit_code, post, + shuf); 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); + "%s[%s: %s] %s%s%s%s", pre, nm, f->name, s, dump, post, shuf); } OUTPUT_UNSET (); diff --git a/src/main.c b/src/main.c index aed4d815..e8526e4a 100644 --- a/src/main.c +++ b/src/main.c @@ -24,6 +24,7 @@ this program. If not, see <http://www.gnu.org/licenses/>. */ #include "rule.h" #include "debug.h" #include "getopt.h" +#include "shuf.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. */ @@ -358,6 +363,9 @@ static const char *const usage[] = N_("\ -R, --no-builtin-variables Disable the built-in variable settings.\n"), N_("\ + --shuffle[=[<seed>|random|identity|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"), @@ -472,6 +480,7 @@ static const struct command_switch switches[] = { CHAR_MAX+8, flag_off, &silent_flag, 1, 1, 0, 0, &default_silent_flag, "no-silent" }, { CHAR_MAX+9, string, &jobserver_auth, 1, 0, 0, 0, 0, "jobserver-fds" }, + { CHAR_MAX+10, string, &shuffle_mode, 1, 1, 0, "random", 0, "shuffle" }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 } }; @@ -1498,6 +1507,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))) @@ -2652,6 +2677,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"))); diff --git a/src/remake.c b/src/remake.c index bd6d8e51..4f25d4de 100644 --- a/src/remake.c +++ b/src/remake.c @@ -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/shuf.c b/src/shuf.c new file mode 100644 index 00000000..0a8ff5b1 --- /dev/null +++ b/src/shuf.c @@ -0,0 +1,241 @@ +/* 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/>. */ + +#include <stddef.h> + +#include "makeint.h" +#include "filedef.h" +#include "dep.h" +#include "shuf.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; + int seed; + void (*shuffler)(void ** a, size_t len); + /* Enough to hold largest '-1234567890' value. */ + char strval[12]; +} config = { sm_none, 0, 0, { 0 }, }; + +/* Return string value of --shuffle= option passed. + If none was passed or --shuffle=none was used function + returns NULL. */ +const char * +shuffle_get_mode (void) +{ + if (!config.strval[0]) + return NULL; + + return config.strval; +} + +void +shuffle_set_mode (const char *cmdarg) +{ + enum shuffle_mode mode; + int seed = 0; + + /* Parse supported '--shuffle' mode. */ + if (streq (cmdarg, "random")) + mode = sm_random; + else if (streq (cmdarg, "reverse")) + mode = sm_reverse; + else if (streq (cmdarg, "identity")) + mode = sm_identity; + else if (streq (cmdarg, "none")) + mode = sm_none; + /* Assume explicit seed if starts from a digit. */ + else if (ISDIGIT (cmdarg[0])) + { + mode = sm_random; + seed = atoi (cmdarg); + } + else + { + O (error, NILF, _("error: unsupported '--shuffle' mode.")); + die (MAKE_FAILURE); + } + + /* Update shuffle configuration. */ + config.mode = mode; + + switch (mode) + { + case sm_random: + if (seed == 0) + seed = (int)time (NULL); + config.seed = seed; + config.shuffler = random_shuffle_array; + sprintf (config.strval, "%d", 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 */, size_t /* len */) +{ + /* 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; + + /* 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/shuf.h b/src/shuf.h new file mode 100644 index 00000000..fb473b7d --- /dev/null +++ b/src/shuf.h @@ -0,0 +1,23 @@ +/* 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/>. */ + +void shuffle_set_mode (const char *cmdarg); +const char * shuffle_get_mode (void); +void shuffle_deps_recursive (struct dep * g); +SI void shuffle_goaldeps_recursive (struct goaldep *goaldeps) +{ + shuffle_deps_recursive ((struct dep *)goaldeps); +} diff --git a/tests/scripts/misc/rand-shuf b/tests/scripts/misc/rand-shuf new file mode 100644 index 00000000..ec7daf1a --- /dev/null +++ b/tests/scripts/misc/rand-shuf @@ -0,0 +1,40 @@ +# -*-perl-*- +$description = "Test shuffle randomness."; + +# 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); +# --shuffle uses random seed based on current time in seconds. +# Give it chance to change seed. +sleep(2); +&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; +} + +1; diff --git a/tests/scripts/options/shuffle-reverse b/tests/scripts/options/shuffle-reverse new file mode 100644 index 00000000..eb88f9b5 --- /dev/null +++ b/tests/scripts/options/shuffle-reverse @@ -0,0 +1,112 @@ +# -*-perl-*- + +$description = "Test the --shuffle option."; + +$details = "Verify that --shuffle has expected effect on target order and argument order."; + +run_make_test(' +%: + @echo $@ +all: a b c +', +'--shuffle=reverse', 'c +b +a +all'); + +run_make_test(' +%: + @echo $@ +all: a b c +', +'--shuffle=none', 'a +b +c +all'); + +run_make_test(' +%: + @echo $@ +all: a b c +', +'--shuffle=identity', 'a +b +c +all'); + +# 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 +bar.h +foo.h +foo.c +cc -c -o foo.o foo.c +all'); + +# 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 +arg_3 +arg_2 +arg_1 +bld foo_ arg_1 arg_3 arg_2 arg_4 +all'); + +# 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. +a_ +b_ +all'); + +# Check if order-only dependencies get reordered. +run_make_test(' +all: a_ + @echo $@ +%_: + @echo $@ +a_: b_ c_ | d_ e_ +', +'--shuffle=reverse', 'e_ +d_ +c_ +b_ +a_ +all'); + +# Check if goals are reordered. +run_make_test(' +%_: + @echo $@ +', +'--shuffle=reverse a_ b_ c_', 'c_ +b_ +a_'); + +1; -- 2.35.1