Repository: SCons/scons

Issue:
Fix coercion of Strings to Lists in MergeFlags
This issue was originally created at: 2014-07-13 05:15:21.
This issue was reported by: `dirkbaechle`.
dirkbaechle said at 2014-07-13 05:15:21
>As reported on the mailing list: https://scons.tigris.org/ds/viewMessage.do?dsForumId=1272&viewType=browseAll&dsMessageId=2876001 the `MergeFlags()` method doesn't handle appending flags to already existing values very well. The coercion of types in `ParseFlags()` (as copied from the `Append` method) should get amended to properly handle the following use case:
```py
env.MergeFlags({'CCFLAGS' : '-whatever -O3'})
```
where `CCFLAGS` is initially empty.

dirkbaechle said at 2014-07-13 05:33:10
>added dependency

dirkbaechle said at 2014-07-13 07:46:05
>*** Issue 2402 has been marked as a duplicate of this issue. ***


dirkbaechle said this issue depends on #2962 at 2014-07-13 05:33:10.

dirkbaechle said this issue is duplicated by #2402 at 2014-07-13 07:46:05.



Discussion:
Trying to figure this out: is this actually expected to work:
```
env.MergeFlags({'CCFLAGS' : '-whatever -O3'})
```
that is, passing a string containing multiple args as opposed to a list of args?
The comment about this being from `ParseFlags` seems to be a red herring - since a dictionary is passed, `ParseFlags` is not called:
```
        if not SCons.Util.is_Dict(args):
            args = self.ParseFlags(args)
```

Resolving patch:
diff --git a/SCons/Environment.py b/SCons/Environment.py
--- a/SCons/Environment.py
+++ b/SCons/Environment.py
@@ -642,17 +642,23 @@ def Override(self, overrides):
             else:
                 overrides[key] = SCons.Subst.scons_subst_once(value, self, key)
         env = OverrideEnvironment(self, overrides)
-        if merges: env.MergeFlags(merges)
+        if merges:
+            env.MergeFlags(merges)
         return env
 
     def ParseFlags(self, *flags):
-        """
-        Parse the set of flags and return a dict with the flags placed
-        in the appropriate entry.  The flags are treated as a typical
-        set of command-line flags for a GNU-like toolchain and used to
-        populate the entries in the dict immediately below.  If one of
-        the flag strings begins with a bang (exclamation mark), it is
-        assumed to be a command and the rest of the string is executed;
+        """Return a dict of parsed flags.
+
+        Parse ``flags`` and return a dict with the flags distributed into
+        the appropriate construction variable names.  The flags are treated
+        as a typical set of command-line flags for a GNU-like toolchain,
+        such as might have been generated by one of the \*-config scripts,
+        and used to populate the entries based on knowledge embedded in
+        this method - the choices are not expected to be portable to other
+        toolchains.
+
+        If one of the ``flags`` strings begins with a bang (exclamation mark),
+        it is assumed to be a command and the rest of the string is executed;
         the result of that evaluation is then added to the dict.
         """
         dict = {
@@ -741,6 +747,9 @@ def append_define(name, dict = dict):
                         t = ('-arch', arg)
                         dict['CCFLAGS'].append(t)
                         dict['LINKFLAGS'].append(t)
+                    elif append_next_arg_to == '--param':
+                        t = ('--param', arg)
+                        dict['CCFLAGS'].append(t)
                     else:
                         dict[append_next_arg_to].append(arg)
                     append_next_arg_to = None
@@ -792,11 +801,13 @@ def append_define(name, dict = dict):
                         dict['FRAMEWORKPATH'].append(arg[2:])
                     else:
                         append_next_arg_to = 'FRAMEWORKPATH'
-                elif arg in ['-mno-cygwin',
-                             '-pthread',
-                             '-openmp',
-                             '-fmerge-all-constants',
-                             '-fopenmp']:
+                elif arg in [
+                    '-mno-cygwin',
+                    '-pthread',
+                    '-openmp',
+                    '-fmerge-all-constants',
+                    '-fopenmp',
+                ]:
                     dict['CCFLAGS'].append(arg)
                     dict['LINKFLAGS'].append(arg)
                 elif arg == '-mwindows':
@@ -810,7 +821,16 @@ def append_define(name, dict = dict):
                 elif arg[0] == '+':
                     dict['CCFLAGS'].append(arg)
                     dict['LINKFLAGS'].append(arg)
-                elif arg in ['-include', '-imacros', '-isysroot', '-isystem', '-iquote', '-idirafter', '-arch']:
+                elif arg in [
+                    '-include',
+                    '-imacros',
+                    '-isysroot',
+                    '-isystem',
+                    '-iquote',
+                    '-idirafter',
+                    '-arch',
+                    '--param',
+                ]:
                     append_next_arg_to = arg
                 else:
                     dict['CCFLAGS'].append(arg)
@@ -819,24 +839,30 @@ def append_define(name, dict = dict):
             do_parse(arg)
         return dict
 
-    def MergeFlags(self, args, unique=1, dict=None):
-        """
-        Merge the dict in args into the construction variables of this
-        env, or the passed-in dict.  If args is not a dict, it is
-        converted into a dict using ParseFlags.  If unique is not set,
-        the flags are appended rather than merged.
-        """
+    def MergeFlags(self, args, unique=True):
+        """Merge flags into construction variables.
+
+        Merges the flags from ``args`` into this construction environent.
+        If ``args`` is not a dict, it is first converted to a dictionary with
+        flags distributed into appropriate construction variables.
+        See :meth:`ParseFlags`.
+
+        Args:
+            args: flags to merge
+            unique: merge flags rather than appending (default: True)
 
-        if dict is None:
-            dict = self
+        """
         if not SCons.Util.is_Dict(args):
             args = self.ParseFlags(args)
+
         if not unique:
             self.Append(**args)
-            return self
+            return
+
         for key, value in args.items():
             if not value:
                 continue
+            value = SCons.Util.Split(value)
             try:
                 orig = self[key]
             except KeyError:
@@ -873,7 +899,6 @@ def MergeFlags(self, args, unique=1, dict=None):
                     if v not in t:
                         t.insert(0, v)
             self[key] = t
-        return self
 
 
 def default_decide_source(dependency, target, prev_ni, repo_node=None):
@@ -1012,7 +1037,8 @@ def __init__(
             self._dict[key] = val
 
         # Finally, apply any flags to be merged in
-        if parse_flags: self.MergeFlags(parse_flags)
+        if parse_flags:
+            self.MergeFlags(parse_flags)
 
     #######################################################################
     # Utility methods that are primarily for internal use by SCons.
@@ -1164,9 +1190,7 @@ def get_tgt_sig_type(self):
     #######################################################################
 
     def Append(self, **kw):
-        """Append values to existing construction variables
-        in an Environment.
-        """
+        """Append values to existing construction variables in an Environment."""
         kw = copy_non_reserved_keywords(kw)
         for key, val in kw.items():
             # It would be easier on the eyes to write this using
@@ -1453,7 +1477,8 @@ def Clone(self, tools=[], toolpath=None, parse_flags = None, **kw):
         clone.Replace(**new)
 
         # Finally, apply any flags to be merged in
-        if parse_flags: clone.MergeFlags(parse_flags)
+        if parse_flags:
+            clone.MergeFlags(parse_flags)
 
         if SCons.Debug.track_instances: logInstanceCreation(self, 'Environment.EnvironmentClone')
         return clone
@@ -1610,7 +1635,7 @@ def FindIxes(self, paths, prefix, suffix):
             if name[:len(prefix)] == prefix and name[-len(suffix):] == suffix:
                 return path
 
-    def ParseConfig(self, command, function=None, unique=1):
+    def ParseConfig(self, command, function=None, unique=True):
         """
         Use the specified function to parse the output of the command
         in order to modify the current environment.  The 'command' can
@@ -1630,7 +1655,7 @@ def parse_conf(env, cmd, unique=unique):
         command = self.subst(command)
         return function(self, self.backtick(command))
 
-    def ParseDepends(self, filename, must_exist=None, only_one=0):
+    def ParseDepends(self, filename, must_exist=None, only_one=False):
         """
         Parse a mkdep-style file for explicit dependencies.  This is
         completely abusable, and should be unnecessary in the "normal"
diff --git a/SCons/EnvironmentTests.py b/SCons/EnvironmentTests.py
--- a/SCons/EnvironmentTests.py
+++ b/SCons/EnvironmentTests.py
@@ -787,33 +787,36 @@ def test_ParseFlags(self):
         d = env.ParseFlags([])
         assert d == empty, d
 
-        s = "-I/usr/include/fum -I bar -X\n" + \
-            '-I"C:\\Program Files\\ASCEND\\include" ' + \
-            "-L/usr/fax -L foo -lxxx -l yyy " + \
-            '-L"C:\\Program Files\\ASCEND" -lascend ' + \
-            "-Wa,-as -Wl,-link " + \
-            "-Wl,-rpath=rpath1 " + \
-            "-Wl,-R,rpath2 " + \
-            "-Wl,-Rrpath3 " + \
-            "-Wp,-cpp " + \
-            "-std=c99 " + \
-            "-std=c++0x " + \
-            "-framework Carbon " + \
-            "-frameworkdir=fwd1 " + \
-            "-Ffwd2 " + \
-            "-F fwd3 " + \
-            "-dylib_file foo-dylib " + \
-            "-pthread " + \
-            "-fmerge-all-constants " +\
-            "-fopenmp " + \
-            "-mno-cygwin -mwindows " + \
-            "-arch i386 -isysroot /tmp " + \
-            "-iquote /usr/include/foo1 " + \
-            "-isystem /usr/include/foo2 " + \
-            "-idirafter /usr/include/foo3 " + \
-            "-imacros /usr/include/foo4 " + \
-            "+DD64 " + \
+        s = (
+            "-I/usr/include/fum -I bar -X "
+            '-I"C:\\Program Files\\ASCEND\\include" '
+            "-L/usr/fax -L foo -lxxx -l yyy "
+            '-L"C:\\Program Files\\ASCEND" -lascend '
+            "-Wa,-as -Wl,-link "
+            "-Wl,-rpath=rpath1 "
+            "-Wl,-R,rpath2 "
+            "-Wl,-Rrpath3 "
+            "-Wp,-cpp "
+            "-std=c99 "
+            "-std=c++0x "
+            "-framework Carbon "
+            "-frameworkdir=fwd1 "
+            "-Ffwd2 "
+            "-F fwd3 "
+            "-dylib_file foo-dylib "
+            "-pthread "
+            "-fmerge-all-constants "
+            "-fopenmp "
+            "-mno-cygwin -mwindows "
+            "-arch i386 -isysroot /tmp "
+            "-iquote /usr/include/foo1 "
+            "-isystem /usr/include/foo2 "
+            "-idirafter /usr/include/foo3 "
+            "-imacros /usr/include/foo4 "
+            "--param l1-cache-size=32 --param l2-cache-size=6144 "
+            "+DD64 "
             "-DFOO -DBAR=value -D BAZ "
+        )
 
         d = env.ParseFlags(s)
 
@@ -827,6 +830,7 @@ def test_ParseFlags(self):
                                 ('-isystem', '/usr/include/foo2'),
                                 ('-idirafter', '/usr/include/foo3'),
                                 ('-imacros', env.fs.File('/usr/include/foo4')),
+                                ('--param', 'l1-cache-size=32'), ('--param', 'l2-cache-size=6144'),
                                 '+DD64'], repr(d['CCFLAGS'])
         assert d['CXXFLAGS'] == ['-std=c++0x'], repr(d['CXXFLAGS'])
         assert d['CPPDEFINES'] == ['FOO', ['BAR', 'value'], 'BAZ'], d['CPPDEFINES']
@@ -852,24 +856,40 @@ def test_ParseFlags(self):
 
 
     def test_MergeFlags(self):
-        """Test the MergeFlags() method
-        """
+        """Test the MergeFlags() method."""
+
         env = SubstitutionEnvironment()
+        # does not set flag if value empty
         env.MergeFlags('')
         assert 'CCFLAGS' not in env, env['CCFLAGS']
+        # merges value if flag did not exist
+        env.MergeFlags('-X')
+        assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
+
+        # avoid SubstitutionEnvironment for these, has no .Append method,
+        # which is needed for unique=False test
+        env = Environment(CCFLAGS=None)
+        # merge with existing but empty flag
         env.MergeFlags('-X')
         assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
+        # default Unique=True enforces no dupes
         env.MergeFlags('-X')
         assert env['CCFLAGS'] == ['-X'], env['CCFLAGS']
+        # Unique=False allows dupes
+        env.MergeFlags('-X', unique=False)
+        assert env['CCFLAGS'] == ['-X', '-X'], env['CCFLAGS']
 
-        env = SubstitutionEnvironment(CCFLAGS=None)
-        env.MergeFlags('-Y')
-        assert env['CCFLAGS'] == ['-Y'], env['CCFLAGS']
+        # merge from a dict with list values
+        env = SubstitutionEnvironment(B='b')
+        env.MergeFlags({'A': ['aaa'], 'B': ['bb', 'bbb']})
+        assert env['A'] == ['aaa'], env['A']
+        assert env['B'] == ['b', 'bb', 'bbb'], env['B']
 
-        env = SubstitutionEnvironment()
-        env.MergeFlags({'A':['aaa'], 'B':['bbb']})
+        # issue #2961: merge from a dict with string values
+        env = SubstitutionEnvironment(B='b')
+        env.MergeFlags({'A': 'aaa', 'B': 'bb bbb'})
         assert env['A'] == ['aaa'], env['A']
-        assert env['B'] == ['bbb'], env['B']
+        assert env['B'] == ['b', 'bb', 'bbb'], env['B']
 
         # issue #3665: if merging dict which is a compound object
         # (i.e. value can be lists, etc.), the value object should not
@@ -881,6 +901,7 @@ def test_MergeFlags(self):
             pass
         flags = {'CFLAGS': ['-pipe', '-pthread', '-g']}
         import copy
+
         saveflags = copy.deepcopy(flags)
         env.MergeFlags(flags)
         self.assertEqual(flags, saveflags)


Test patch: