Skip to content

gh-136097: Fix sysconfig._parse_makefile() #136166

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 60 additions & 86 deletions Lib/sysconfig/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
# 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):
Expand All @@ -49,99 +50,72 @@ 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.
# Special care is needed to ensure that variable expansion works, even
# 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():
Expand Down
71 changes: 70 additions & 1 deletion Lib/test/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,8 +757,12 @@ def test_parse_makefile(self):
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',
Expand All @@ -767,6 +771,71 @@ def test_parse_makefile(self):
'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=',
})


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix potential infinite recursion and KeyError in ``sysconfig
--generate-posix-vars``.
Loading