]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-136097: Fix sysconfig._parse_makefile() (#136166)
authorSerhiy Storchaka <storchaka@gmail.com>
Sat, 4 Oct 2025 13:57:12 +0000 (16:57 +0300)
committerGitHub <noreply@github.com>
Sat, 4 Oct 2025 13:57:12 +0000 (14:57 +0100)
* Fix potential infinite recursion.
* Fix a bug when reference can cross boundaries of substitutions, e.g.
  a=$(
  b=$(a)a)
* Fix potential quadratic complexity.
* Fix KeyError for undefined CFLAGS, LDFLAGS, or CPPFLAGS.
* Fix infinite recursion when keep_unresolved=False.
* Unify behavior with keep_unresolved=False for bogus $ occurred before
  and after variable references.

Lib/sysconfig/__main__.py
Lib/test/test_sysconfig.py
Misc/NEWS.d/next/Library/2025-07-01-14-44-03.gh-issue-136097.bI1n14.rst [new file with mode: 0644]

index bc2197cfe794028ac6eeb09e5754ad7cc290d578..0cf0cf4dbb9ec72bbc5e68dcfe9bf19528f684bd 100644 (file)
@@ -21,8 +21,9 @@ from sysconfig import (
 # Regexes needed for parsing Makefile (and similar syntaxes,
 # like old-style Setup files).
 _variable_rx = r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)"
-_findvar1_rx = r"\$\(([A-Za-z][A-Za-z0-9_]*)\)"
-_findvar2_rx = r"\${([A-Za-z][A-Za-z0-9_]*)}"
+_findvar_rx = (r"\$(\([A-Za-z][A-Za-z0-9_]*\)"
+                 r"|\{[A-Za-z][A-Za-z0-9_]*\}"
+                 r"|\$?)")
 
 
 def _parse_makefile(filename, vars=None, keep_unresolved=True):
@@ -49,26 +50,7 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
         m = re.match(_variable_rx, line)
         if m:
             n, v = m.group(1, 2)
-            v = v.strip()
-            # `$$' is a literal `$' in make
-            tmpv = v.replace('$$', '')
-
-            if "$" in tmpv:
-                notdone[n] = v
-            else:
-                try:
-                    if n in _ALWAYS_STR:
-                        raise ValueError
-
-                    v = int(v)
-                except ValueError:
-                    # insert literal `$'
-                    done[n] = v.replace('$$', '$')
-                else:
-                    done[n] = v
-
-    # do variable interpolation here
-    variables = list(notdone.keys())
+            notdone[n] = v.strip()
 
     # Variables with a 'PY_' prefix in the makefile. These need to
     # be made available without that prefix through sysconfig.
@@ -76,72 +58,64 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
     # if the expansion uses the name without a prefix.
     renamed_variables = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS')
 
-    while len(variables) > 0:
-        for name in tuple(variables):
-            value = notdone[name]
-            m1 = re.search(_findvar1_rx, value)
-            m2 = re.search(_findvar2_rx, value)
-            if m1 and m2:
-                m = m1 if m1.start() < m2.start() else m2
-            else:
-                m = m1 if m1 else m2
-            if m is not None:
-                n = m.group(1)
-                found = True
-                if n in done:
-                    item = str(done[n])
-                elif n in notdone:
-                    # get it on a subsequent round
-                    found = False
-                elif n in os.environ:
-                    # do it like make: fall back to environment
-                    item = os.environ[n]
-
-                elif n in renamed_variables:
-                    if (name.startswith('PY_') and
-                        name[3:] in renamed_variables):
-                        item = ""
-
-                    elif 'PY_' + n in notdone:
-                        found = False
-
-                    else:
-                        item = str(done['PY_' + n])
-
+    def resolve_var(name):
+        def repl(m):
+            n = m[1]
+            if n == '$':
+                return '$'
+            elif n == '':
+                # bogus variable reference (e.g. "prefix=$/opt/python")
+                if keep_unresolved:
+                    return m[0]
+                raise ValueError
+            elif n[0] == '(' and n[-1] == ')':
+                n = n[1:-1]
+            elif n[0] == '{' and n[-1] == '}':
+                n = n[1:-1]
+
+            if n in done:
+                return str(done[n])
+            elif n in notdone:
+                return str(resolve_var(n))
+            elif n in os.environ:
+                # do it like make: fall back to environment
+                return os.environ[n]
+            elif n in renamed_variables:
+                if name.startswith('PY_') and name[3:] in renamed_variables:
+                    return ""
+                n = 'PY_' + n
+                if n in notdone:
+                    return str(resolve_var(n))
                 else:
-                    done[n] = item = ""
-
-                if found:
-                    after = value[m.end():]
-                    value = value[:m.start()] + item + after
-                    if "$" in after:
-                        notdone[name] = value
-                    else:
-                        try:
-                            if name in _ALWAYS_STR:
-                                raise ValueError
-                            value = int(value)
-                        except ValueError:
-                            done[name] = value.strip()
-                        else:
-                            done[name] = value
-                        variables.remove(name)
-
-                        if name.startswith('PY_') \
-                        and name[3:] in renamed_variables:
-
-                            name = name[3:]
-                            if name not in done:
-                                done[name] = value
-
+                    assert n not in done
+                    return ""
             else:
-                # Adds unresolved variables to the done dict.
-                # This is disabled when called from distutils.sysconfig
-                if keep_unresolved:
-                    done[name] = value
-                # bogus variable reference (e.g. "prefix=$/opt/python");
-                # just drop it since we can't deal
-                variables.remove(name)
+                done[n] = ""
+                return ""
+
+        assert name not in done
+        done[name] = ""
+        try:
+            value = re.sub(_findvar_rx, repl, notdone[name])
+        except ValueError:
+            del done[name]
+            return ""
+        value = value.strip()
+        if name not in _ALWAYS_STR:
+            try:
+                value = int(value)
+            except ValueError:
+                pass
+        done[name] = value
+        if name.startswith('PY_') and name[3:] in renamed_variables:
+            name = name[3:]
+            if name not in done:
+                done[name] = value
+        return value
+
+    for n in notdone:
+        if n not in done:
+            resolve_var(n)
 
     # strip spurious spaces
     for k, v in done.items():
index 6a0681a414888565e81cf5443caafc1dee3e7588..59ea623bb92610daec2be0df04aecbb6eb3fb27a 100644 (file)
@@ -764,8 +764,12 @@ class MakefileTests(unittest.TestCase):
             print("var3=42", file=makefile)
             print("var4=$/invalid", file=makefile)
             print("var5=dollar$$5", file=makefile)
-            print("var6=${var3}/lib/python3.5/config-$(VAR2)$(var5)"
+            print("var6=${var7}/lib/python3.5/config-$(VAR2)$(var5)"
                   "-x86_64-linux-gnu", file=makefile)
+            print("var7=${var3}", file=makefile)
+            print("var8=$$(var3)", file=makefile)
+            print("var9=$(var10)(var3)", file=makefile)
+            print("var10=$$", file=makefile)
         vars = _parse_makefile(TESTFN)
         self.assertEqual(vars, {
             'var1': 'ab42',
@@ -774,6 +778,71 @@ class MakefileTests(unittest.TestCase):
             'var4': '$/invalid',
             'var5': 'dollar$5',
             'var6': '42/lib/python3.5/config-b42dollar$5-x86_64-linux-gnu',
+            'var7': 42,
+            'var8': '$(var3)',
+            'var9': '$(var3)',
+            'var10': '$',
+        })
+
+    def _test_parse_makefile_recursion(self):
+        self.addCleanup(unlink, TESTFN)
+        with open(TESTFN, "w") as makefile:
+            print("var1=var1=$(var1)", file=makefile)
+            print("var2=var3=$(var3)", file=makefile)
+            print("var3=var2=$(var2)", file=makefile)
+        vars = _parse_makefile(TESTFN)
+        self.assertEqual(vars, {
+            'var1': 'var1=',
+            'var2': 'var3=var2=',
+            'var3': 'var2=',
+        })
+
+    def test_parse_makefile_renamed_vars(self):
+        self.addCleanup(unlink, TESTFN)
+        with open(TESTFN, "w") as makefile:
+            print("var1=$(CFLAGS)", file=makefile)
+            print("PY_CFLAGS=-Wall $(CPPFLAGS)", file=makefile)
+            print("PY_LDFLAGS=-lm", file=makefile)
+            print("var2=$(LDFLAGS)", file=makefile)
+            print("var3=$(CPPFLAGS)", file=makefile)
+        vars = _parse_makefile(TESTFN)
+        self.assertEqual(vars, {
+            'var1': '-Wall',
+            'CFLAGS': '-Wall',
+            'PY_CFLAGS': '-Wall',
+            'LDFLAGS': '-lm',
+            'PY_LDFLAGS': '-lm',
+            'var2': '-lm',
+            'var3': '',
+        })
+
+    def test_parse_makefile_keep_unresolved(self):
+        self.addCleanup(unlink, TESTFN)
+        with open(TESTFN, "w") as makefile:
+            print("var1=value", file=makefile)
+            print("var2=$/", file=makefile)
+            print("var3=$/$(var1)", file=makefile)
+            print("var4=var5=$(var5)", file=makefile)
+            print("var5=$/$(var1)", file=makefile)
+            print("var6=$(var1)$/", file=makefile)
+            print("var7=var8=$(var8)", file=makefile)
+            print("var8=$(var1)$/", file=makefile)
+        vars = _parse_makefile(TESTFN)
+        self.assertEqual(vars, {
+            'var1': 'value',
+            'var2': '$/',
+            'var3': '$/value',
+            'var4': 'var5=$/value',
+            'var5': '$/value',
+            'var6': 'value$/',
+            'var7': 'var8=value$/',
+            'var8': 'value$/',
+        })
+        vars = _parse_makefile(TESTFN, keep_unresolved=False)
+        self.assertEqual(vars, {
+            'var1': 'value',
+            'var4': 'var5=',
+            'var7': 'var8=',
         })
 
 
diff --git a/Misc/NEWS.d/next/Library/2025-07-01-14-44-03.gh-issue-136097.bI1n14.rst b/Misc/NEWS.d/next/Library/2025-07-01-14-44-03.gh-issue-136097.bI1n14.rst
new file mode 100644 (file)
index 0000000..209ca74
--- /dev/null
@@ -0,0 +1,2 @@
+Fix potential infinite recursion and KeyError in ``sysconfig
+--generate-posix-vars``.