commit 965bce78754f92769ada675a37bec478e23760a1
Author: Dmitry Goncharov <dgoncharov@users.sf.net>
Date:   Sat Jan 20 11:11:27 2024 -0500

    [SV 64822, SV 36486] Fix appending to a pattern specific variable.
    
    Appending to a pattern specific variable produces an incorrect value in the
    presence of a command line definition or an env override of the variable.
    Also, fix pattern/target-specific appending to a variable with origin override.
    
    The following takes place.
    1. At parse time record_target_var sets the value of a pattern specific
    variable to the value defined on command line or to the value of the env
    override.
    2. Later, at build time, recursively_expand_for_file appends this value of the
    variable (set in record_target_var) to the command line value again, regardless
    of the origin of the variable.
    
    This patch modifies recursively_expand_for_file to avoid appending, unless the
    origin of the variable beats or equals the origin of one of the parent
    definitions of this variable.
    
    * src/variable.c (do_variable_definition): Avoid merging a
    pattern-specific variable with the parent definition when a command
    line or env override is present.
    * src/expand.c (recursively_expand_for_file): Avoid appending to a
    pattern-specific variable, unless the origin of this pattern-specific variable
    beats or equals the origin of one of the parent definitions of this variable.
    * tests/scripts/variables/append: Add tests.
    
    Reported by Rob <robw9739@gmail.com>,
    Brian Vandenberg <phantall@gmail.com>,
    Markus Oberhumer <markus@oberhumer.com>.

diff --git a/tests/scripts/variables/append b/tests/scripts/variables/append
new file mode 100644
index 00000000..4d1f4efd
--- /dev/null
+++ b/tests/scripts/variables/append
@@ -0,0 +1,280 @@
+#                                                                    -*-perl-*-
+
+use warnings;
+
+my $description = "Test appending to variables of various origins.";
+
+# sv 64822, sv 36486.
+# Test various combinations of appends in the presence and absence of command
+# line definitions, env overrides and makefile overrides.
+# Test the following statements.
+# A target or pattern specific definition or append is available at build time
+# only.
+# A definition with a makefile override can only be appended to with another
+# override.
+# A definition with a makefile override can only be redefined with another
+# override.
+# A target or pattern specific definition takes precedence over a global
+# definition for that target.
+# A global variable is immune to a target or pattern specific definition or
+# append.
+# A target or pattern specific definition is immune to another target or pattern
+# specific definition or append.
+# A prerequisite inherits a target or pattern specific value from its target.
+# "phobos" inherits the value of "hello" from "mars".
+
+my @goals = ('phobos', 'mars');
+my @specificities = ('', 's: ', '%: ');
+my @inits = ('', 'hello=file', 'hello:=file', 'override hello=file');
+my @global_append = ('', 'hello+=red');
+my @blue_override = ('', 'override ');
+my @yellow_override = ('', 'override ');
+my @cmdline = ('', 'hello=cmd', 'hello:=cmd hello+=cmd2');
+my @env_ovr = ('', '-e');
+my @envs = ('', 'env');
+
+for my $goal (@goals) {
+for my $spec (@specificities) {
+for my $init (@inits) {
+for my $ga (@global_append) {
+for my $bovr (@blue_override) {
+for my $yovr (@yellow_override) {
+for my $dashe (@env_ovr) {
+for my $env (@envs) {
+for my $cmd (@cmdline) {
+
+if ($env) {
+    $ENV{'hello'} = 'env';
+} else {
+    delete $ENV{'hello'};
+}
+
+my $parse_time_answer = '';
+my $build_time_answer = '';
+
+# For goal "phobos" target is "", "phobos: " and "phobo%: ".
+# For goal "mars" target is "", "mars: " and "mar%: ".
+my $target = '';
+if ($spec) {
+    $target = $goal;
+    chop($target);
+    $target .= $spec;
+}
+
+if ($init =~ 'override') {
+    $build_time_answer = 'file';
+} elsif ($cmd) {
+    $build_time_answer = $cmd =~ 'cmd2' ? 'cmd cmd2' : 'cmd';
+} elsif ($dashe and $env) {
+    $build_time_answer = 'env';
+} elsif ($init and !$target and $ga) {
+    $build_time_answer = 'file red';
+} elsif ($init) {
+    $build_time_answer = 'file';
+} elsif ($env and $ga) {
+    $build_time_answer = 'env red';
+} elsif ($env) {
+    $build_time_answer = 'env';
+} elsif ($ga) {
+    $build_time_answer = 'red';
+}
+
+if ($bovr and $yovr) {
+    $build_time_answer .= ' ' if ($build_time_answer);
+    $build_time_answer .= 'blue yellow';
+} elsif ($bovr) {
+    $build_time_answer .= ' ' if ($build_time_answer);
+    $build_time_answer .= 'blue';
+} elsif ($yovr and !($init =~ 'override') and !$cmd and !($dashe and $env)) {
+    $build_time_answer .= ' ' if ($build_time_answer);
+    $build_time_answer .= 'blue yellow';
+} elsif ($yovr) {
+    $build_time_answer .= ' ' if ($build_time_answer);
+    $build_time_answer .= 'yellow';
+} elsif ($init =~ 'override') {
+} elsif ($cmd) {
+} elsif ($dashe and $env) {
+} else {
+    $build_time_answer .= ' ' if ($build_time_answer);
+    $build_time_answer .= 'blue yellow';
+}
+
+
+if ($cmd and $target) {
+    $parse_time_answer = $cmd =~ 'cmd2' ? 'cmd cmd2' : 'cmd';
+} elsif ($env and !$dashe and $target and $ga) {
+    $parse_time_answer = 'env red';
+} elsif ($env and $target) {
+    $parse_time_answer = 'env';
+} elsif ($target and $ga) {
+    $parse_time_answer = 'red';
+} elsif ($target) {
+    $parse_time_answer = '';
+} else {
+    $parse_time_answer = $build_time_answer;
+}
+
+my $i = $init ? "$target$init" : '';
+
+# moon and satur% specific settings test that target and pattern
+# settings specific to one target do not affect another target.
+
+my $answer = $goal eq "mars" ?
+    "$parse_time_answer\n$build_time_answer\n" # From parse time and from "phobos" recipe.
+  . "$build_time_answer\n#MAKE#: 'mars' is up to date.\n" :  # From "mars" recipe.
+    "$parse_time_answer\n$build_time_answer\n#MAKE#: 'phobos' is up to date.\n";
+
+
+run_make_test("
+# goal = $goal
+# init = $init
+# target = $target
+# ga = $ga
+# bovr = $bovr
+# yovr = $yovr
+# cmd = $cmd
+# env = $env
+# dashe = $dashe
+
+moon: hello=1
+moon: override hello+=2
+
+$i
+$ga
+$target${bovr}hello+=blue
+$target${yovr}hello+=yellow
+
+satur%: override hello:=3
+satur%: hello+=4
+
+\$(info \$(hello))
+phobos:; \$(info \$(hello))
+mars: phobos; \$(info \$(hello))
+saturn:; \$(info \$(hello))
+", "$dashe $cmd $goal", "$answer");
+}
+}
+}
+}
+}
+}
+}
+}
+}
+
+# Preferably, we would want to run the tests below for all the combinations,
+# generated by the loop above. However, that causes the test to take a lot of
+# time.
+
+# The fix for sv 64822 went to recursively_expand_for_file.
+# There are three code paths that lead to recursively_expand_for_file.
+#  - export of a variable to target environment.
+#  - expansion of a substitution reference.
+#  - other expansions of a variable that was appended to.
+# Test target env, substitution reference in parse and build modes.;
+# Also test a mix of pattern and target specific definitions and appends.
+
+run_make_test(q!
+al%: hello=file
+al%: hello+=one
+all: hello+=two
+$(info $(hello))
+all:; $(info $(hello))
+!, "", "\nfile one two\n#MAKE#: 'all' is up to date.\n");
+
+run_make_test(q!
+hello=file
+al%: hello+=one
+all: hello+=two
+$(info $(hello:X=Y))
+all:; $(info $(hello:X=Y))
+!, "", "file\nfile one two\n#MAKE#: 'all' is up to date.\n");
+
+run_make_test(q!
+hello=file
+al%: hello+=one
+all: hello+=two
+export hello
+$(info $(shell echo $$hello))
+all:; $(info $(shell echo $$hello))
+!, "", "file\nfile one two\n#MAKE#: 'all' is up to date.\n");
+
+# "phobos" inherits the value of "hello" from "mars". On top of that there are
+# also "phobos" specific appends.
+for my $goal (@goals) {
+my $answer = $goal eq "mars" ?
+    "\nminit mone mtwo pone ptwo\n" # From parse time and from "phobos" recipe.
+  . "minit mone mtwo\n#MAKE#: 'mars' is up to date.\n" :  # From "mars" recipe.
+    "\npone ptwo\n#MAKE#: 'phobos' is up to date.\n"; # From parse time and from "phobos" recipe.
+
+run_make_test("
+mar%: hello=minit
+mar%: hello+=mone
+mars: hello+=mtwo
+phobo%: hello+=pone
+phobos: hello+=ptwo
+\$(info \$(hello))
+phobos:; \$(info \$(hello))
+mars: phobos; \$(info \$(hello))
+", "$goal", "$answer");
+}
+
+# This test is similar to the one above. The difference is that here there is a
+# pattern-specific definition of "hello" that matches "phobos".
+run_make_test(q!
+mar%: hello:=minit
+mar%: hello+=mone
+mars: hello+=mtwo
+phobo%: hello:=pinit
+phobo%: hello+=pone
+phobos: hello+=ptwo
+$(info $(hello))
+phobos:; $(info $(hello))
+mars: phobos; $(info $(hello))
+!, 'phobos', "\npinit pone ptwo\n#MAKE#: 'phobos' is up to date.\n");
+
+# Test pattern and target specific appends to a global variable that has origin override.
+# sv 36486.
+my @ops = ('=', '+=', ':=');
+@inits = ('', 'override ', 'al%: override ');
+@specificities = ('', 'all: ', 'al%: ');
+for my $init (@inits) {
+for my $spec (@specificities) {
+for my $op (@ops) {
+
+my $build_time_answer = '';
+if ($init =~ ':' and $op eq '+=' and !$spec) {
+    # This is the case where global variable obtains value 'one two three' at
+    # parse time and later at build time a pattern or target specific
+    # 'hello+=file' appends 'file'.
+    # e.g.
+    # al%: override hello+=file
+    # hello+=one
+    # hello+=two
+    # hello+=three
+    $build_time_answer = 'one two three file';
+} elsif ($init =~ 'override') {
+    $build_time_answer = 'file';
+} else {
+    $build_time_answer = 'file one two three';
+}
+
+my $parse_time_answer = $init =~ ':' ? '' : 'file';
+if (!$spec and ($init ne 'override ')) {
+    $parse_time_answer .= ' ' if $parse_time_answer;
+    $parse_time_answer .= 'one two three';
+}
+
+run_make_test("
+${init}hello${op}file
+${spec}hello+=one
+${spec}hello+=two
+${spec}hello+=three
+\$(info \$(hello))
+all:; \$(info \$(hello))
+", "", "$parse_time_answer\n$build_time_answer\n#MAKE#: 'all' is up to date.\n");
+}
+}
+}
+
+1;
