commit 77b95ab67acd43dc93b38f8682470f64f7d366c5
Author: Dmitry Goncharov <dgoncharov@users.sf.net>
Date:   Tue Mar 14 20:58:06 2023 -0400

    [SV 63856] Implement .WAIT on the command line.
    
    * src/main.c (handle_non_switch_argument): Return whether arg is .WAIT.
    (decode_switches): Set wait_here for a command line goal that follows .WAIT.
    * src/remake.c (update_goal_chain): Honor wait_here for a command line goal.
    * tests/scripts/targets/WAIT: Add .WAIT tests.

diff --git a/src/main.c b/src/main.c
index a9d3a644..e77b4ad1 100644
--- a/src/main.c
+++ b/src/main.c
@@ -3015,14 +3015,14 @@ init_switches (void)
 
 
 /* Non-option argument.  It might be a variable definition.  */
-static void
+static int
 handle_non_switch_argument (const char *arg, enum variable_origin origin)
 {
   struct variable *v;
 
   if (arg[0] == '-' && arg[1] == '\0')
     /* Ignore plain '-' for compatibility.  */
-    return;
+    return 0;
 
 #ifdef VMS
   {
@@ -3075,7 +3075,12 @@ handle_non_switch_argument (const char *arg, enum variable_origin origin)
          Enter it as a file and add it to the dep chain of goals.
          Check ARG[0] because if the top makefile resets MAKEOVERRIDES
          then ARG points to an empty string in the submake.  */
-      struct file *f = enter_file (strcache_add (expand_command_line_file (arg)));
+      struct file *f;
+
+      if (strcmp (arg, ".WAIT") == 0)
+        return 1;
+
+      f = enter_file (strcache_add (expand_command_line_file (arg)));
       f->cmd_target = 1;
 
       if (goals == 0)
@@ -3116,6 +3121,7 @@ handle_non_switch_argument (const char *arg, enum variable_origin origin)
         define_variable_cname ("MAKECMDGOALS", value, o_default, 0);
       }
     }
+  return 0;
 }
 
 /* Called if the makefile resets the MAKEFLAGS variable.  */
@@ -3137,6 +3143,7 @@ decode_switches (int argc, const char **argv, enum variable_origin origin)
   struct command_switch *cs;
   struct stringlist *sl;
   int c;
+  int found_wait = 0;
 
   /* getopt does most of the parsing for us.
      First, get its vectors set up.  */
@@ -3160,8 +3167,13 @@ decode_switches (int argc, const char **argv, enum variable_origin origin)
         /* End of arguments, or "--" marker seen.  */
         break;
       else if (c == 1)
-        /* An argument not starting with a dash.  */
-        handle_non_switch_argument (coptarg, origin);
+        {
+          /* An argument not starting with a dash.  */
+          const int prior_found_wait = found_wait;
+          found_wait = handle_non_switch_argument (coptarg, origin);
+          if (prior_found_wait && lastgoal)
+            lastgoal->wait_here = 1;
+        }
       else if (c == '?')
         /* Bad option.  We will print a usage message and die later.
            But continue to parse the other options so the user can
@@ -3358,7 +3370,12 @@ decode_switches (int argc, const char **argv, enum variable_origin origin)
      to be returned in order, this only happens when there is a "--"
      argument to prevent later arguments from being options.  */
   while (optind < argc)
-    handle_non_switch_argument (argv[optind++], origin);
+    {
+      const int prior_found_wait = found_wait;
+      found_wait = handle_non_switch_argument (argv[optind++], origin);
+      if (prior_found_wait && lastgoal)
+        lastgoal->wait_here = 1;
+    }
 
   if (bad && origin == o_command)
     print_usage (bad);
diff --git a/src/remake.c b/src/remake.c
index fe67ab28..42a672ac 100644
--- a/src/remake.c
+++ b/src/remake.c
@@ -112,6 +112,7 @@ update_goal_chain (struct goaldep *goaldeps)
   unsigned long last_cmd_count = 0;
   int t = touch_flag, q = question_flag, n = just_print_flag;
   enum update_status status = us_none;
+  const unsigned int depth = rebuilding_makefiles ? 1 : 0;
 
   /* Duplicate the chain so we can remove things from it.  */
   struct dep *goals_orig = copy_dep_chain ((struct dep *)goaldeps);
@@ -130,6 +131,7 @@ update_goal_chain (struct goaldep *goaldeps)
   while (goals != 0)
     {
       struct dep *gu, *g, *lastgoal;
+      int running = 0, wait = 0;
 
       /* Start jobs that are waiting for the load to go down.  */
 
@@ -138,7 +140,6 @@ update_goal_chain (struct goaldep *goaldeps)
       /* Check for exited children.  If no children have finished since the
          last time we looked, then block until one exits.  If some have
          exited don't block, so we can possibly do more work.  */
-
       reap_children (last_cmd_count == command_count, 0);
       last_cmd_count = command_count;
 
@@ -147,16 +148,14 @@ update_goal_chain (struct goaldep *goaldeps)
       while (gu != 0)
         {
           /* Iterate over all double-colon entries for this file.  */
-          struct file *file;
+          struct file *file, *headfile;
           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;
-               file != NULL;
-               file = file->prev)
+          headfile = g->file->double_colon ? g->file->double_colon : g->file;
+          for (file = headfile; file != NULL; file = file->prev)
             {
               unsigned int ocommands_started;
               enum update_status fail;
@@ -181,8 +180,24 @@ update_goal_chain (struct goaldep *goaldeps)
                  actually run.  */
               ocommands_started = commands_started;
 
-              fail = update_file (file, rebuilding_makefiles ? 1 : 0);
+              stop = 0;
+
+              /* In the case of double colon rules, only the recipe of the 1st
+               * rule should be blocked by .WAIT. The recipes of all subsequent
+               * rules for the same file will execute sequentially in order
+               * after the 1st.  */
+              wait = file == headfile && g->wait_here && running;
+              if (wait)
+                {
+                  DBF (DB_VERBOSE, _(".WAIT is blocking '%s'.\n"));
+                  break;
+                }
+
+              fail = update_file (file, depth);
               check_renamed (file);
+              running |= (file->command_state == cs_running
+                          || file->command_state == cs_deps_running);
+
 
               /* Set the goal's 'changed' flag if any commands were started
                  by calling update_file above.  We check this flag below to
@@ -190,7 +205,6 @@ update_goal_chain (struct goaldep *goaldeps)
               if (commands_started > ocommands_started)
                 g->changed = 1;
 
-              stop = 0;
               if ((fail || file->updated) && status < us_question)
                 {
                   /* We updated this goal.  Update STATUS and decide whether
@@ -242,6 +256,9 @@ update_goal_chain (struct goaldep *goaldeps)
           /* Reset FILE since it is null at the end of the loop.  */
           file = g->file;
 
+          if (wait)
+            break;
+
           if (stop || !any_not_updated)
             {
               /* If we have found nothing whatever to do for the goal,
@@ -278,8 +295,10 @@ update_goal_chain (struct goaldep *goaldeps)
         }
 
       /* If we reached the end of the dependency graph update CONSIDERED
-         for the next pass.  */
-      if (gu == 0)
+         for the next pass.
+         In the case of waiting, increment CONSIDERED to prevent the same file
+         from getting pruned over and over again.  */
+      if (gu == 0 || wait)
         ++considered;
     }
 
@@ -377,7 +396,8 @@ update_file (struct file *file, unsigned int depth)
       if (f->command_state == cs_running
           || f->command_state == cs_deps_running)
         /* Don't run other :: rules for this target until
-           this rule is finished.  */
+           this rule is finished.  Multiple recipes running in parallel and
+           updating the same target will corrupt the target.  */
         return us_success;
 
       if (new > status)
diff --git a/tests/scripts/targets/WAIT b/tests/scripts/targets/WAIT
index b981023f..343b87ac 100644
--- a/tests/scripts/targets/WAIT
+++ b/tests/scripts/targets/WAIT
@@ -56,6 +56,73 @@ pre2: ; @#HELPER# -q out $@
 
 run_make_test(undef, '-j10 pre2', "pre2\n");
 
+# sv 63856.
+# .WAIT on the command line.
+
+run_make_test(q!
+pre1: ; @#HELPER# -q out start-$@ sleep 1 out end-$@
+pre2: ; @#HELPER# -q out $@
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10 pre1 .WAIT pre2', "start-pre1\nend-pre1\npre2\n");
+
+# Multiple consecutive .WAITs.
+
+run_make_test(q!
+all : pre1 .WAIT .WAIT .WAIT pre2
+pre1: ; @#HELPER# -q out start-$@ sleep 1 out end-$@
+pre2: ; @#HELPER# -q out $@
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10', "start-pre1\nend-pre1\npre2\n");
+
+# First and last prerequsites are .WAIT.
+
+run_make_test(q!
+all : .WAIT pre1 .WAIT pre2 .WAIT
+pre1: ; @#HELPER# -q out start-$@ sleep 1 out end-$@
+pre2: ; @#HELPER# -q out $@
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10', "start-pre1\nend-pre1\npre2\n");
+
+# All prerequisites are .WAITs.
+
+run_make_test(q!
+all : .WAIT .WAIT .WAIT
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10', "#MAKE#: Nothing to be done for 'all'.\n");
+
+run_make_test(q!
+all:
+!,
+              '-j10 .WAIT', "#MAKE#: Nothing to be done for 'all'.\n");
+
+# Wait between the duplicate goals.
+
+run_make_test(q!
+all: hello.tsk .WAIT hello.tsk
+hello.tsk:; $(info $@)
+!,
+              '-j10', "hello.tsk\n#MAKE#: Nothing to be done for 'all'.\n");
+
+# Wait between the duplicate command line goals.
+
+run_make_test(q!
+hello.tsk:; $(info $@)
+!,
+              '-j10 hello.tsk .WAIT hello.tsk', "hello.tsk\n#MAKE#: 'hello.tsk' is up to date.\n#MAKE#: 'hello.tsk' is up to date.\n");
+
+
 # Ensure .WAIT doesn't wait between all targets
 
 run_make_test(q!
@@ -71,6 +138,20 @@ pre3: ; @#HELPER# -q wait TWO out $@ file THREE
 
 unlink(qw(TWO THREE));
 
+# Ensure .WAIT on the command line doesn't wait between all targets.
+
+run_make_test(q!
+pre1: ; @#HELPER# -q out start-$@ sleep 1 out end-$@
+pre2: ; @#HELPER# -q out start-$@ file TWO wait THREE out end-$@
+pre3: ; @#HELPER# -q wait TWO out $@ file THREE
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10 pre1 .WAIT pre2 pre3', "start-pre1\nend-pre1\nstart-pre2\npre3\nend-pre2\n");
+
+unlink(qw(TWO THREE));
+
 # Ensure .WAIT waits for ALL targets on the left before ANY targets on the right
 
 run_make_test(q!
@@ -88,6 +169,23 @@ post2: ; @#HELPER# -q file POST2 wait POST1 out $@
 
 unlink(qw(PRE1 PRE2 POST1 POST2));
 
+# Ensure .WAIT on the command line waits for ALL targets on the left before ANY
+# targets on the right.
+
+run_make_test(q!
+pre1: ; @#HELPER# -q out start-$@ file PRE1 wait PRE2 sleep 1 out end-$@
+pre2: ; @#HELPER# -q wait PRE1 out $@ file PRE2
+
+post1: ; @#HELPER# -q wait POST2 out $@ file POST1
+post2: ; @#HELPER# -q file POST2 wait POST1 out $@
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10 pre1 pre2 .WAIT post1 post2', "start-pre1\npre2\nend-pre1\npost1\npost2\n");
+
+unlink(qw(PRE1 PRE2 POST1 POST2));
+
 # See if .WAIT takes effect between different lists of prereqs
 # In the current implementation, .WAIT waits only between two prerequisites
 # in a given target.  These same two targets might be run in a different
@@ -180,6 +278,17 @@ pre2: ; @#HELPER# -q out $@
 !,
               '-j10 --shuffle=reverse', "start-pre1\nend-pre1\npre2\n");
 
+# Ensure we don't shuffle if .WAIT is set on the command line.
+
+run_make_test(q!
+pre1: ; @#HELPER# -q out start-$@ sleep 1 out end-$@
+pre2: ; @#HELPER# -q out $@
+
+# This is just here so we don't fail with older versions of make
+.WAIT:
+!,
+              '-j10 --shuffle=reverse pre1 .WAIT pre2', "start-pre1\nend-pre1\npre2\n");
+
 # Warn about invalid .WAIT definitions
 
 run_make_test(q!
