Bug 1250297 - Make python configure output config.status instead of old-configure doing it
authorMike Hommey <mh+mozilla@glandium.org>
Fri, 26 Feb 2016 04:59:53 +0900
changeset 703566 12ad2dac1f7c4a4ea9dc744e78790f9977447e00
parent 703553 0fc750c50fe2bb14929f1a5b076398c07178e0ac
child 703567 f412426cf50586e27f6bf3d5d9f5d3f6c6dbb8ab
child 703596 b3d2e406fe57c17be50b3ddd721ce37cff70b5ec
push id111430
push usermh@glandium.org
push dateThu, 25 Feb 2016 23:13:42 +0000
treeherdertry@f412426cf505 [default view] [failures only]
bugs1250297
milestone47.0a1
Bug 1250297 - Make python configure output config.status instead of old-configure doing it The nice side effect is that now we can have actual dicts for defines and substs from the start, which simplifies so things, although it requires adjustments to some unit tests.
CLOBBER
build/autoconf/config.status.m4
configure.py
js/src/old-configure.in
old-configure.in
python/mozbuild/mozbuild/backend/configenvironment.py
python/mozbuild/mozbuild/config_status.py
python/mozbuild/mozbuild/test/backend/common.py
python/mozbuild/mozbuild/test/backend/test_configenvironment.py
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,10 +17,10 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1246756 - Update Skia to m49 branch.
+Bug 1250297 - Doesn't actually need a clobber, but updating the tree to an older changeset does.
 
--- a/build/autoconf/config.status.m4
+++ b/build/autoconf/config.status.m4
@@ -78,142 +78,95 @@ define([AC_DEFINE_UNQUOTED],
 EOF
 ifelse($#, 2, _MOZ_AC_DEFINE_UNQUOTED($1, $2), $#, 3, _MOZ_AC_DEFINE_UNQUOTED($1, $2, $3),_MOZ_AC_DEFINE_UNQUOTED($1))dnl
 ])
 
 dnl Replace AC_OUTPUT to create and call a python config.status
 define([MOZ_CREATE_CONFIG_STATUS],
 [dnl Top source directory in Windows format (as opposed to msys format).
 WIN_TOP_SRC=
-encoding=utf-8
 case "$host_os" in
 mingw*)
     WIN_TOP_SRC=`cd $srcdir; pwd -W`
-    encoding=mbcs
     ;;
 esac
 AC_SUBST(WIN_TOP_SRC)
 
 dnl Used in all Makefile.in files
 top_srcdir=$srcdir
 AC_SUBST(top_srcdir)
 
 dnl Picked from autoconf 2.13
 trap '' 1 2 15
 AC_CACHE_SAVE
 
 trap 'rm -f $CONFIG_STATUS conftest*; exit 1' 1 2 15
-: ${CONFIG_STATUS=./config.status}
+: ${CONFIG_STATUS=./config.data}
 
 dnl We're going to need [ ] for python syntax.
 changequote(<<<, >>>)dnl
 echo creating $CONFIG_STATUS
 
 extra_python_path=${COMM_BUILD:+"'mozilla', "}
 
 cat > $CONFIG_STATUS <<EOF
-#!${PYTHON}
-# coding=$encoding
-
-import os
-import types
-dnl topsrcdir is the top source directory in native form, as opposed to a
-dnl form suitable for make.
-topsrcdir = '''${WIN_TOP_SRC:-$srcdir}'''
-if not os.path.isabs(topsrcdir):
-    rel = os.path.join(os.path.dirname(<<<__file__>>>), topsrcdir)
-    topsrcdir = os.path.abspath(rel)
-topsrcdir = os.path.normpath(topsrcdir)
-
-topobjdir = os.path.abspath(os.path.dirname(<<<__file__>>>))
-
 def unique_list(l):
     result = []
     for i in l:
         if l not in result:
             result.append(i)
     return result
 
 dnl All defines and substs are stored with an additional space at the beginning
 dnl and at the end of the string, to avoid any problem with values starting or
 dnl ending with quotes.
-defines = [(name[1:-1], value[1:-1]) for name, value in [
+defines = [
 EOF
 
 dnl confdefs.pytmp contains AC_DEFINEs, in the expected format, but
 dnl lacks the final comma (see above).
 sed 's/$/,/' confdefs.pytmp >> $CONFIG_STATUS
 rm confdefs.pytmp confdefs.h
 
 cat >> $CONFIG_STATUS <<\EOF
-] ]
+]
 
-substs = [(name[1:-1], value[1:-1] if isinstance(value, types.StringTypes) else value) for name, value in [
+substs = [
 EOF
 
 dnl The MOZ_DIVERSION_SUBST output diversion contains AC_SUBSTs, in the
 dnl expected format, but lacks the final comma (see above).
 sed 's/$/,/' >> $CONFIG_STATUS <<EOF
 undivert(MOZ_DIVERSION_SUBST)dnl
 EOF
 
 dnl Add in the output from the subconfigure script
 for ac_subst_arg in $_subconfigure_ac_subst_args; do
   variable='$'$ac_subst_arg
   echo "    (''' $ac_subst_arg ''', r''' `eval echo $variable` ''')," >> $CONFIG_STATUS
 done
 
 cat >> $CONFIG_STATUS <<\EOF
-] ]
+]
 
 dnl List of AC_DEFINEs that aren't to be exposed in ALLDEFINES
 non_global_defines = [
 EOF
 
 if test -n "$_NON_GLOBAL_ACDEFINES"; then
   for var in $_NON_GLOBAL_ACDEFINES; do
     echo "    '$var'," >> $CONFIG_STATUS
   done
 fi
 
 cat >> $CONFIG_STATUS <<EOF
 ]
-
-__all__ = ['topobjdir', 'topsrcdir', 'defines', 'non_global_defines', 'substs']
-EOF
-
-# We don't want js/src/config.status to do anything in gecko builds.
-if test -z "$BUILDING_JS" -o -n "$JS_STANDALONE"; then
-
-    cat >> $CONFIG_STATUS <<EOF
-dnl Do the actual work
-if __name__ == '__main__':
-    args = dict([(name, globals()[name]) for name in __all__])
-    from mozbuild.config_status import config_status
-    config_status(**args)
 EOF
 
-fi
-
 changequote([, ])
-
-chmod +x $CONFIG_STATUS
-])
-
-define([MOZ_RUN_CONFIG_STATUS],
-[
-
-MOZ_RUN_ALL_SUBCONFIGURES()
-
-rm -fr confdefs* $ac_clean_files
-dnl Execute config.status, unless --no-create was passed to configure.
-if test "$no_create" != yes && ! ${PYTHON} $CONFIG_STATUS; then
-    trap '' EXIT
-    exit 1
-fi
 ])
 
 define([m4_fatal],[
 errprint([$1
 ])
 m4exit(1)
 ])
 
--- a/configure.py
+++ b/configure.py
@@ -1,22 +1,25 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import print_function, unicode_literals
 
 import glob
 import itertools
+import json
 import os
 import subprocess
 import sys
 import re
+import types
 
-base_dir = os.path.dirname(__file__)
+
+base_dir = os.path.abspath(os.path.dirname(__file__))
 sys.path.append(os.path.join(base_dir, 'python', 'which'))
 sys.path.append(os.path.join(base_dir, 'python', 'mozbuild'))
 from which import which, WhichError
 from mozbuild.mozconfig import MozconfigLoader
 
 
 # If feel dirty replicating this from python/mozbuild/mozbuild/mozconfig.py,
 # but the end goal being that the configure script would go away...
@@ -111,13 +114,70 @@ def main(args):
     old_configure = os.path.abspath(old_configure).replace(os.sep, '/')
 
     try:
         autoconf_refresh(old_configure)
     except RuntimeError as e:
         print(e.message, file=sys.stderr)
         return 1
 
-    return subprocess.call([shell, old_configure] + args)
+    ret = subprocess.call([shell, old_configure] + args)
+    if ret:
+        return ret
+
+    raw_config = {}
+    with open('config.data') as fh:
+        encoding = 'mbcs' if sys.platform == 'win32' else 'utf-8'
+        code = compile(fh.read().decode(encoding), 'config.data', 'exec')
+        # Every variation of the exec() function I tried led to:
+        # SyntaxError: unqualified exec is not allowed in function 'main' it
+        # contains a nested function with free variables
+        exec code in raw_config
+
+    # Sanitize config data
+    config = {}
+    substs = config['substs'] = {
+        k[1:-1]: v[1:-1] if isinstance(v, types.StringTypes) else v
+        for k, v in raw_config['substs']
+    }
+    config['defines'] = {
+        k[1:-1]: v[1:-1]
+        for k, v in raw_config['defines']
+    }
+    config['non_global_defines'] = raw_config['non_global_defines']
+    config['topsrcdir'] = base_dir
+    config['topobjdir'] = os.path.abspath(os.getcwd())
 
+    # Create config.status. Eventually, we'll want to just do the work it does
+    # here, when we're able to skip configure tests/use cached results/not rely
+    # on autoconf.
+    print("Creating config.status", file=sys.stderr)
+    with open('config.status', 'w') as fh:
+        fh.write('#!%s\n' % config['substs']['PYTHON'])
+        fh.write('# coding=utf-8\n')
+        for k, v in config.iteritems():
+            fh.write('%s = ' % k)
+            json.dump(v, fh)
+            fh.write('\n')
+        fh.write("__all__ = ['topobjdir', 'topsrcdir', 'defines', "
+                 "'non_global_defines', 'substs']")
+
+        if not substs.get('BUILDING_JS') or substs.get('JS_STANDALONE'):
+            fh.write('''
+if __name__ == '__main__':
+    args = dict([(name, globals()[name]) for name in __all__])
+    from mozbuild.config_status import config_status
+    config_status(**args)
+''')
+
+    # Other things than us are going to run this file, so we need to give it
+    # executable permissions.
+    os.chmod('config.status', 0755)
+    if not substs.get('BUILDING_JS') or substs.get('JS_STANDALONE'):
+        if not substs.get('JS_STANDALONE'):
+            os.environ['WRITE_MOZINFO'] = '1'
+        # Until we have access to the virtualenv from this script, execute
+        # config.status externally, with the virtualenv python.
+        return subprocess.call([config['substs']['PYTHON'], 'config.status'])
+    return 0
 
 if __name__ == '__main__':
     sys.exit(main(sys.argv[1:]))
--- a/js/src/old-configure.in
+++ b/js/src/old-configure.in
@@ -163,16 +163,17 @@ if test "$JS_STANDALONE" = no; then
   #DIST is exported from top-level configure
 else
   JS_STANDALONE=1
   AC_DEFINE(JS_STANDALONE)
   DIST="$MOZ_BUILD_ROOT/dist"
 fi
 AC_SUBST(JS_STANDALONE)
 BUILDING_JS=1
+AC_SUBST(BUILDING_JS)
 AC_SUBST(autoconfmk)
 
 MOZ_ARG_WITH_STRING(gonk,
 [  --with-gonk=DIR
                location of gonk dir],
     gonkdir=$withval)
 
 MOZ_ARG_WITH_STRING(gonk-toolchain-prefix,
@@ -3663,10 +3664,12 @@ MOZ_SUBCONFIGURE_JEMALLOC()
 # Avoid using obsolete NSPR features
 AC_DEFINE(NO_NSPR_10_SUPPORT)
 
 dnl Spit out some output
 dnl ========================================================
 MOZ_CREATE_CONFIG_STATUS()
 
 if test "$JS_STANDALONE"; then
-  MOZ_RUN_CONFIG_STATUS()
+  MOZ_RUN_ALL_SUBCONFIGURES()
 fi
+
+rm -fr confdefs* $ac_clean_files
--- a/old-configure.in
+++ b/old-configure.in
@@ -9169,16 +9169,16 @@ if ! test -e js; then
     mkdir js
 fi
 
 AC_OUTPUT_SUBDIRS(js/src,$cache_file)
 ac_configure_args="$_SUBDIR_CONFIG_ARGS"
 
 fi # COMPILE_ENVIRONMENT
 
-export WRITE_MOZINFO=1
 dnl we need to run config.status after js/src subconfigure because we're
 dnl traversing its moz.build and we need its config.status for that.
 dnl However, writing our own config.status needs to happen before
 dnl subconfigures because the setup surrounding subconfigures alters
 dnl many AC_SUBSTed variables.
-MOZ_RUN_CONFIG_STATUS()
-unset WRITE_MOZINFO
+MOZ_RUN_ALL_SUBCONFIGURES()
+
+rm -fr confdefs* $ac_clean_files
--- a/python/mozbuild/mozbuild/backend/configenvironment.py
+++ b/python/mozbuild/mozbuild/backend/configenvironment.py
@@ -73,22 +73,21 @@ class ConfigEnvironment(object):
     and output results in the object directory.
 
     There are two types of files: config files and config headers,
     each treated through a different member function.
 
     Creating a ConfigEnvironment requires a few arguments:
       - topsrcdir and topobjdir are, respectively, the top source and
         the top object directory.
-      - defines is a list of (name, value) tuples. In autoconf, these are
-        set with AC_DEFINE and AC_DEFINE_UNQUOTED
+      - defines is a dict filled from AC_DEFINE and AC_DEFINE_UNQUOTED in
+        autoconf.
       - non_global_defines are a list of names appearing in defines above
         that are not meant to be exported in ACDEFINES (see below)
-      - substs is a list of (name, value) tuples. In autoconf, these are
-        set with AC_SUBST.
+      - substs is a dict filled from AC_SUBST in autoconf.
 
     ConfigEnvironment automatically defines one additional substs variable
     from all the defines not appearing in non_global_defines:
       - ACDEFINES contains the defines in the form -DNAME=VALUE, for use on
         preprocessor command lines. The order in which defines were given
         when creating the ConfigEnvironment is preserved.
     and two other additional subst variables from all the other substs:
       - ALLSUBSTS contains the substs in the form NAME = VALUE, in sorted
@@ -101,18 +100,18 @@ class ConfigEnvironment(object):
         NAME =.
 
     ConfigEnvironment expects a "top_srcdir" subst to be set with the top
     source directory, in msys format on windows. It is used to derive a
     "srcdir" subst when treating config files. It can either be an absolute
     path or a path relative to the topobjdir.
     """
 
-    def __init__(self, topsrcdir, topobjdir, defines=[], non_global_defines=[],
-        substs=[], source=None):
+    def __init__(self, topsrcdir, topobjdir, defines={}, non_global_defines=[],
+        substs={}, source=None):
 
         if not source:
             source = mozpath.join(topobjdir, 'config.status')
         self.source = source
         self.defines = ReadOnlyDict(defines)
         self.non_global_defines = non_global_defines
         self.substs = dict(substs)
         self.topsrcdir = mozpath.abspath(topsrcdir)
@@ -124,20 +123,21 @@ class ConfigEnvironment(object):
         self.dll_suffix = self.substs.get('DLL_SUFFIX', '')
         if self.substs.get('IMPORT_LIB_SUFFIX'):
             self.import_prefix = self.lib_prefix
             self.import_suffix = '.%s' % self.substs['IMPORT_LIB_SUFFIX']
         else:
             self.import_prefix = self.dll_prefix
             self.import_suffix = self.dll_suffix
 
-        global_defines = [name for name, value in defines
+        global_defines = [name for name, value in defines.iteritems()
             if not name in non_global_defines]
         self.substs['ACDEFINES'] = ' '.join(['-D%s=%s' % (name,
-            shell_quote(self.defines[name]).replace('$', '$$')) for name in global_defines])
+            shell_quote(self.defines[name]).replace('$', '$$'))
+            for name in sorted(global_defines)])
         def serialize(obj):
             if isinstance(obj, StringTypes):
                 return obj
             if isinstance(obj, Iterable):
                 return ' '.join(obj)
             raise Exception('Unhandled type %s', type(obj))
         self.substs['ALLSUBSTS'] = '\n'.join(sorted(['%s = %s' % (name,
             serialize(self.substs[name])) for name in self.substs if self.substs[name]]))
--- a/python/mozbuild/mozbuild/config_status.py
+++ b/python/mozbuild/mozbuild/config_status.py
@@ -58,17 +58,17 @@ following:
 
    mach build-backend --backend=VisualStudio
 
 ===============================
 '''.strip()
 
 
 def config_status(topobjdir='.', topsrcdir='.',
-        defines=[], non_global_defines=[], substs=[], source=None):
+        defines={}, non_global_defines=[], substs={}, source=None):
     '''Main function, providing config.status functionality.
 
     Contrary to config.status, it doesn't use CONFIG_FILES or CONFIG_HEADERS
     variables.
 
     Without the -n option, this program acts as config.status and considers
     the current directory as the top object directory, even when config.status
     is in a different directory. It will, however, treat the directory
@@ -90,23 +90,17 @@ def config_status(topobjdir='.', topsrcd
         raise Exception('Using the CONFIG_HEADERS environment variable is not '
             'supported.')
 
     if not os.path.isabs(topsrcdir):
         raise Exception('topsrcdir must be defined as an absolute directory: '
             '%s' % topsrcdir)
 
     default_backends = ['RecursiveMake']
-    # We have a chicken/egg problem, where we only have a dict for substs after
-    # creating the ConfigEnvironment, which requires argument parsing to have
-    # occurred.
-    for name, value in substs:
-        if name == 'BUILD_BACKENDS':
-            default_backends = value
-            break
+    default_backends = substs.get('BUILD_BACKENDS', ['RecursiveMake'])
 
     parser = ArgumentParser()
     parser.add_argument('--recheck', dest='recheck', action='store_true',
                         help='update config.status by reconfiguring in the same conditions')
     parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                         help='display verbose output')
     parser.add_argument('-n', dest='not_topobjdir', action='store_true',
                         help='do not consider current directory as top object directory')
--- a/python/mozbuild/mozbuild/test/backend/common.py
+++ b/python/mozbuild/mozbuild/test/backend/common.py
@@ -24,84 +24,84 @@ log_manager = LoggingManager()
 log_manager.add_terminal_logging()
 
 
 test_data_path = mozpath.abspath(mozpath.dirname(__file__))
 test_data_path = mozpath.join(test_data_path, 'data')
 
 
 CONFIGS = defaultdict(lambda: {
-    'defines': [],
+    'defines': {},
     'non_global_defines': [],
-    'substs': [('OS_TARGET', 'WINNT')],
+    'substs': {'OS_TARGET': 'WINNT'},
 }, {
     'android_eclipse': {
-        'defines': [
-            ('MOZ_ANDROID_MIN_SDK_VERSION', '15'),
-        ],
+        'defines': {
+            'MOZ_ANDROID_MIN_SDK_VERSION': '15',
+        },
         'non_global_defines': [],
-        'substs': [
-            ('ANDROID_TARGET_SDK', '16'),
-            ('MOZ_WIDGET_TOOLKIT', 'android'),
-        ],
+        'substs': {
+            'ANDROID_TARGET_SDK': '16',
+            'MOZ_WIDGET_TOOLKIT': 'android',
+        },
     },
     'binary-components': {
-        'defines': [],
+        'defines': {},
         'non_global_defines': [],
-        'substs': [
-            ('LIB_PREFIX', 'lib'),
-            ('LIB_SUFFIX', 'a'),
-        ],
+        'substs': {
+            'LIB_PREFIX': 'lib',
+            'LIB_SUFFIX': 'a',
+        },
     },
     'sources': {
-        'defines': [],
+        'defines': {},
         'non_global_defines': [],
-        'substs': [
-            ('LIB_PREFIX', 'lib'),
-            ('LIB_SUFFIX', 'a'),
-        ],
+        'substs': {
+            'LIB_PREFIX': 'lib',
+            'LIB_SUFFIX': 'a',
+        },
     },
     'stub0': {
-        'defines': [
-            ('MOZ_TRUE_1', '1'),
-            ('MOZ_TRUE_2', '1'),
-        ],
+        'defines': {
+            'MOZ_TRUE_1': '1',
+            'MOZ_TRUE_2': '1',
+        },
         'non_global_defines': [
-            ('MOZ_NONGLOBAL_1', '1'),
-            ('MOZ_NONGLOBAL_2', '1'),
+            'MOZ_NONGLOBAL_1',
+            'MOZ_NONGLOBAL_2',
         ],
-        'substs': [
-            ('MOZ_FOO', 'foo'),
-            ('MOZ_BAR', 'bar'),
-        ],
+        'substs': {
+            'MOZ_FOO': 'foo',
+            'MOZ_BAR': 'bar',
+        },
     },
     'substitute_config_files': {
-        'defines': [],
+        'defines': {},
         'non_global_defines': [],
-        'substs': [
-            ('MOZ_FOO', 'foo'),
-            ('MOZ_BAR', 'bar'),
-        ],
+        'substs': {
+            'MOZ_FOO': 'foo',
+            'MOZ_BAR': 'bar',
+        },
     },
     'test_config': {
-        'defines': [
-            ('foo', 'baz qux'),
-            ('baz', 1)
-        ],
+        'defines': {
+            'foo': 'baz qux',
+            'baz': 1,
+        },
         'non_global_defines': [],
-        'substs': [
-            ('foo', 'bar baz'),
-        ],
+        'substs': {
+            'foo': 'bar baz',
+        },
     },
     'visual-studio': {
-        'defines': [],
+        'defines': {},
         'non_global_defines': [],
-        'substs': [
-            ('MOZ_APP_NAME', 'my_app'),
-        ],
+        'substs': {
+            'MOZ_APP_NAME': 'my_app',
+        },
     },
 })
 
 
 class BackendTester(unittest.TestCase):
     def setUp(self):
         self._old_env = dict(os.environ)
         os.environ.pop('MOZ_OBJDIR', None)
@@ -117,17 +117,17 @@ class BackendTester(unittest.TestCase):
         environment is cleaned up automatically when the test finishes.
         """
         config = CONFIGS[name]
 
         objdir = mkdtemp()
         self.addCleanup(rmtree, objdir)
 
         srcdir = mozpath.join(test_data_path, name)
-        config['substs'].append(('top_srcdir', srcdir))
+        config['substs']['top_srcdir'] = srcdir
         return ConfigEnvironment(srcdir, objdir, **config)
 
     def _emit(self, name, env=None):
         env = env or self._get_environment(name)
         reader = BuildReader(env)
         emitter = TreeMetadataEmitter(env)
 
         return env, emitter.emit(reader.read_topsrcdir())
--- a/python/mozbuild/mozbuild/test/backend/test_configenvironment.py
+++ b/python/mozbuild/mozbuild/test/backend/test_configenvironment.py
@@ -34,28 +34,28 @@ class ConfigEnvironment(ConfigStatus.Con
 
 
 class TestEnvironment(unittest.TestCase):
     def test_auto_substs(self):
         '''Test the automatically set values of ACDEFINES, ALLSUBSTS
         and ALLEMPTYSUBSTS.
         '''
         env = ConfigEnvironment('.', '.',
-                  defines = [ ('foo', 'bar'), ('baz', 'qux 42'),
-                              ('abc', "d'e'f"), ('extra', 'foobar') ],
+                  defines = { 'foo': 'bar', 'baz': 'qux 42',
+                              'abc': "d'e'f", 'extra': 'foobar' },
                   non_global_defines = ['extra', 'ignore'],
-                  substs = [ ('FOO', 'bar'), ('FOOBAR', ''), ('ABC', 'def'),
-                             ('bar', 'baz qux'), ('zzz', '"abc def"'),
-                             ('qux', '') ])
+                  substs = { 'FOO': 'bar', 'FOOBAR': '', 'ABC': 'def',
+                             'bar': 'baz qux', 'zzz': '"abc def"',
+                             'qux': '' })
         # non_global_defines should be filtered out in ACDEFINES.
         # Original order of the defines need to be respected in ACDEFINES
-        self.assertEqual(env.substs['ACDEFINES'], """-Dfoo=bar -Dbaz='qux 42' -Dabc='d'\\''e'\\''f'""")
+        self.assertEqual(env.substs['ACDEFINES'], """-Dabc='d'\\''e'\\''f' -Dbaz='qux 42' -Dfoo=bar""")
         # Likewise for ALLSUBSTS, which also must contain ACDEFINES
         self.assertEqual(env.substs['ALLSUBSTS'], '''ABC = def
-ACDEFINES = -Dfoo=bar -Dbaz='qux 42' -Dabc='d'\\''e'\\''f'
+ACDEFINES = -Dabc='d'\\''e'\\''f' -Dbaz='qux 42' -Dfoo=bar
 FOO = bar
 bar = baz qux
 zzz = "abc def"''')
         # ALLEMPTYSUBSTS contains all substs with no value.
         self.assertEqual(env.substs['ALLEMPTYSUBSTS'], '''FOOBAR =
 qux =''')