Skip to content

Commit 967214c

Browse files
gh-136097: Fix sysconfig._parse_makefile()
* 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 infinite recursion when keep_unresolved=False. * Unify behavior with keep_unresolved=False for bogus $ occured before and after variable references.
1 parent bd928a3 commit 967214c

File tree

2 files changed

+130
-87
lines changed

2 files changed

+130
-87
lines changed

Lib/sysconfig/__main__.py

Lines changed: 60 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
# Regexes needed for parsing Makefile (and similar syntaxes,
2222
# like old-style Setup files).
2323
_variable_rx = r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)"
24-
_findvar1_rx = r"\$\(([A-Za-z][A-Za-z0-9_]*)\)"
25-
_findvar2_rx = r"\${([A-Za-z][A-Za-z0-9_]*)}"
24+
_findvar_rx = (r"\$(\([A-Za-z][A-Za-z0-9_]*\)"
25+
r"|\{[A-Za-z][A-Za-z0-9_]*\}"
26+
r"|\$?)")
2627

2728

2829
def _parse_makefile(filename, vars=None, keep_unresolved=True):
@@ -49,99 +50,72 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
4950
m = re.match(_variable_rx, line)
5051
if m:
5152
n, v = m.group(1, 2)
52-
v = v.strip()
53-
# `$$' is a literal `$' in make
54-
tmpv = v.replace('$$', '')
55-
56-
if "$" in tmpv:
57-
notdone[n] = v
58-
else:
59-
try:
60-
if n in _ALWAYS_STR:
61-
raise ValueError
62-
63-
v = int(v)
64-
except ValueError:
65-
# insert literal `$'
66-
done[n] = v.replace('$$', '$')
67-
else:
68-
done[n] = v
69-
70-
# do variable interpolation here
71-
variables = list(notdone.keys())
53+
notdone[n] = v.strip()
7254

7355
# Variables with a 'PY_' prefix in the makefile. These need to
7456
# be made available without that prefix through sysconfig.
7557
# Special care is needed to ensure that variable expansion works, even
7658
# if the expansion uses the name without a prefix.
7759
renamed_variables = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS')
7860

79-
while len(variables) > 0:
80-
for name in tuple(variables):
81-
value = notdone[name]
82-
m1 = re.search(_findvar1_rx, value)
83-
m2 = re.search(_findvar2_rx, value)
84-
if m1 and m2:
85-
m = m1 if m1.start() < m2.start() else m2
86-
else:
87-
m = m1 if m1 else m2
88-
if m is not None:
89-
n = m.group(1)
90-
found = True
91-
if n in done:
92-
item = str(done[n])
93-
elif n in notdone:
94-
# get it on a subsequent round
95-
found = False
96-
elif n in os.environ:
97-
# do it like make: fall back to environment
98-
item = os.environ[n]
99-
100-
elif n in renamed_variables:
101-
if (name.startswith('PY_') and
102-
name[3:] in renamed_variables):
103-
item = ""
104-
105-
elif 'PY_' + n in notdone:
106-
found = False
107-
108-
else:
109-
item = str(done['PY_' + n])
110-
61+
def resolve_var(name):
62+
def repl(m):
63+
n = m[1]
64+
if n == '$':
65+
return '$'
66+
elif n == '':
67+
# bogus variable reference (e.g. "prefix=$/opt/python")
68+
if keep_unresolved:
69+
return m[0]
70+
raise ValueError
71+
elif n[0] == '(' and n[-1] == ')':
72+
n = n[1:-1]
73+
elif n[0] == '{' and n[-1] == '}':
74+
n = n[1:-1]
75+
76+
if n in done:
77+
return str(done[n])
78+
elif n in notdone:
79+
return str(resolve_var(n))
80+
elif n in os.environ:
81+
# do it like make: fall back to environment
82+
return os.environ[n]
83+
elif n in renamed_variables:
84+
if name.startswith('PY_') and name[3:] in renamed_variables:
85+
return ""
86+
n = 'PY_' + n
87+
if n in notdone:
88+
return str(resolve_var(n))
11189
else:
112-
done[n] = item = ""
113-
114-
if found:
115-
after = value[m.end():]
116-
value = value[:m.start()] + item + after
117-
if "$" in after:
118-
notdone[name] = value
119-
else:
120-
try:
121-
if name in _ALWAYS_STR:
122-
raise ValueError
123-
value = int(value)
124-
except ValueError:
125-
done[name] = value.strip()
126-
else:
127-
done[name] = value
128-
variables.remove(name)
129-
130-
if name.startswith('PY_') \
131-
and name[3:] in renamed_variables:
132-
133-
name = name[3:]
134-
if name not in done:
135-
done[name] = value
136-
90+
assert n not in done
91+
return ""
13792
else:
138-
# Adds unresolved variables to the done dict.
139-
# This is disabled when called from distutils.sysconfig
140-
if keep_unresolved:
141-
done[name] = value
142-
# bogus variable reference (e.g. "prefix=$/opt/python");
143-
# just drop it since we can't deal
144-
variables.remove(name)
93+
done[n] = ""
94+
return ""
95+
96+
assert name not in done
97+
done[name] = ""
98+
try:
99+
value = re.sub(_findvar_rx, repl, notdone[name])
100+
except ValueError:
101+
del done[name]
102+
return ""
103+
value = value.strip()
104+
if name not in _ALWAYS_STR:
105+
try:
106+
value = int(value)
107+
except ValueError:
108+
pass
109+
done[name] = value
110+
if name.startswith('PY_') and name[3:] in renamed_variables:
111+
name = name[3:]
112+
if name not in done:
113+
done[name] = value
114+
return value
115+
116+
for n in notdone:
117+
if n not in done:
118+
resolve_var(n)
145119

146120
# strip spurious spaces
147121
for k, v in done.items():

Lib/test/test_sysconfig.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,8 +757,12 @@ def test_parse_makefile(self):
757757
print("var3=42", file=makefile)
758758
print("var4=$/invalid", file=makefile)
759759
print("var5=dollar$$5", file=makefile)
760-
print("var6=${var3}/lib/python3.5/config-$(VAR2)$(var5)"
760+
print("var6=${var7}/lib/python3.5/config-$(VAR2)$(var5)"
761761
"-x86_64-linux-gnu", file=makefile)
762+
print("var7=${var3}", file=makefile)
763+
print("var8=$$(var3)", file=makefile)
764+
print("var9=$(var10)(var3)", file=makefile)
765+
print("var10=$$", file=makefile)
762766
vars = _parse_makefile(TESTFN)
763767
self.assertEqual(vars, {
764768
'var1': 'ab42',
@@ -767,6 +771,71 @@ def test_parse_makefile(self):
767771
'var4': '$/invalid',
768772
'var5': 'dollar$5',
769773
'var6': '42/lib/python3.5/config-b42dollar$5-x86_64-linux-gnu',
774+
'var7': 42,
775+
'var8': '$(var3)',
776+
'var9': '$(var3)',
777+
'var10': '$',
778+
})
779+
780+
def _test_parse_makefile_recursion(self):
781+
self.addCleanup(unlink, TESTFN)
782+
with open(TESTFN, "w") as makefile:
783+
print("var1=var1=$(var1)", file=makefile)
784+
print("var2=var3=$(var3)", file=makefile)
785+
print("var3=var2=$(var2)", file=makefile)
786+
vars = _parse_makefile(TESTFN)
787+
self.assertEqual(vars, {
788+
'var1': 'var1=',
789+
'var2': 'var3=var2=',
790+
'var3': 'var2=',
791+
})
792+
793+
def test_parse_makefile_renamed_vars(self):
794+
self.addCleanup(unlink, TESTFN)
795+
with open(TESTFN, "w") as makefile:
796+
print("var1=$(CFLAGS)", file=makefile)
797+
print("PY_CFLAGS=-Wall $(CPPFLAGS)", file=makefile)
798+
print("PY_LDFLAGS=-lm", file=makefile)
799+
print("var2=$(LDFLAGS)", file=makefile)
800+
print("var3=$(CPPFLAGS)", file=makefile)
801+
vars = _parse_makefile(TESTFN)
802+
self.assertEqual(vars, {
803+
'var1': '-Wall',
804+
'CFLAGS': '-Wall',
805+
'PY_CFLAGS': '-Wall',
806+
'LDFLAGS': '-lm',
807+
'PY_LDFLAGS': '-lm',
808+
'var2': '-lm',
809+
'var3': '',
810+
})
811+
812+
def test_parse_makefile_keep_unresolved(self):
813+
self.addCleanup(unlink, TESTFN)
814+
with open(TESTFN, "w") as makefile:
815+
print("var1=value", file=makefile)
816+
print("var2=$/", file=makefile)
817+
print("var3=$/$(var1)", file=makefile)
818+
print("var4=var5=$(var5)", file=makefile)
819+
print("var5=$/$(var1)", file=makefile)
820+
print("var6=$(var1)$/", file=makefile)
821+
print("var7=var8=$(var8)", file=makefile)
822+
print("var8=$(var1)$/", file=makefile)
823+
vars = _parse_makefile(TESTFN)
824+
self.assertEqual(vars, {
825+
'var1': 'value',
826+
'var2': '$/',
827+
'var3': '$/value',
828+
'var4': 'var5=$/value',
829+
'var5': '$/value',
830+
'var6': 'value$/',
831+
'var7': 'var8=value$/',
832+
'var8': 'value$/',
833+
})
834+
vars = _parse_makefile(TESTFN, keep_unresolved=False)
835+
self.assertEqual(vars, {
836+
'var1': 'value',
837+
'var4': 'var5=',
838+
'var7': 'var8=',
770839
})
771840

772841

0 commit comments

Comments
 (0)