Skip to content

Commit 57df321

Browse files
committed
fix symbol table add changing value of existing variable
Noticed my symbol table add() method was always creating a new entry vs updating the entry that existed. Now will update the Entry's value. Added some additional complex rules to handle Command Line variables. Also snuck in a change to remove some dead code that was confusing my greps.
1 parent 0a9f422 commit 57df321

9 files changed

Lines changed: 191 additions & 160 deletions

File tree

pymake/pargs.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,16 @@ def __init__(self):
113113

114114
def __str__(self):
115115
# useful for debugging the sub-make
116-
s = ""
117-
s += "-B " if self.always_make else ""
118-
s += "-C %s " % self.directory if self.directory else ""
119-
s += "-d " if self.debug else ""
120-
s += "-f %s " % self.filename if self.filename else ""
121-
s += "-n " if self.dry_run else ""
122-
s += "-s " if self.silent else ""
123-
s += " ".join(self.argslist)
124-
return s
116+
a = [
117+
"-B" if self.always_make else "",
118+
"-C %s" % self.directory if self.directory else "",
119+
"-d" if self.debug else "",
120+
"-f %s" % self.filename if self.filename else "",
121+
"-n" if self.dry_run else "",
122+
"-s" if self.silent else "",
123+
*self.argslist
124+
]
125+
return " ".join( [s for s in a if s] )
125126

126127
def _parse_debug_flags(s):
127128
flags = s.split(',')

pymake/parser.py

Lines changed: 0 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -752,116 +752,6 @@ def parse_directive(directive_vstr, virt_line, vline_iter):
752752

753753
return lut[str(directive_vstr)](directive_vstr, virt_line, vline_iter)
754754

755-
def parse_expression(expr, virt_line, vline_iter):
756-
# davep 20241123 ; obsolete
757-
assert 0, "TODO"
758-
759-
# This is a second pass through an Expression.
760-
# An Expression could be something like:
761-
# $(info blah blah) # fn call ; most are invalid in standalone context
762-
# export
763-
# export something
764-
# define foo # start of multi-line variable
765-
766-
logger.debug("parse %s", expr.__class__.__name__)
767-
# if get_line_number(expr) > 151:
768-
# breakpoint()
769-
assert isinstance(expr,Expression), type(expr)
770-
771-
# If we do find a directive, we'll wind up re-parsing the entire line as a
772-
# directive. Unnecessary and ugly but I first tried to handle directives
773-
# before assignment and rules which didn't work (too much confusion in
774-
# handling the case of directive names as as rule or assignments). So I'm
775-
# parsing the line first, determining if it's a rule or statement or
776-
# expression. If expression, look for a directive string, then connect
777-
# to the original Directive handling code here.
778-
779-
assign_expr = None
780-
if isinstance(expr, AssignmentExpression):
781-
# weird case showing GNU Make's lack of reserved words and the
782-
# sloppiness of my grammar tokenparser.
783-
# define xyzzy := <-- multi-line variable masquerading as an Assignment
784-
# ifdef := 123 <-- totally legal but I want to throw a warning
785-
# export CC=gcc <-- looks like an assignment
786-
# Need to dig into the assignment to get the LHS.
787-
assign_expr = expr
788-
expr = expr.token_list[0]
789-
790-
# We're only interested in Directives at this point. A Directive will be
791-
# inside string Literal.
792-
#
793-
# Leave anything else alone. Invalid context function calls will error out
794-
# during execute(). For example, $(subst ...) alone on a line will error
795-
# with "*** missing separator. Stop."
796-
797-
# If the first token isn't a Literal, we're done.
798-
tok = expr.token_list[0]
799-
if not isinstance(tok, Literal):
800-
return assign_expr if assign_expr else expr
801-
802-
# seek_directive() needs a character iterator
803-
viter = ScannerIterator(tok.string, tok.string.get_pos()[0])
804-
805-
# peek at the first character of the line. If it's a <tab> aka recipeprefix then
806-
# WOW does our life get complicated. GNU Make allows directives (with a
807-
# couple exceptions) to be recipeprefix'd.
808-
# TODO not handling recipeprefix yet
809-
first_vchar = viter.lookahead()
810-
811-
directive_vstr = seek_directive(viter)
812-
if not directive_vstr:
813-
# nope, not a directive. Ignore this expression and let execute figure it out
814-
return assign_expr if assign_expr else expr
815-
816-
# at this point, we definitely have some sort of directive.
817-
assert viter.is_empty()
818-
819-
# delete the whitespace token after the directive if it exists
820-
if len(expr.token_list) > 1 and expr.token_list[1].is_whitespace():
821-
del expr.token_list[1]
822-
823-
dir_str = str(directive_vstr)
824-
825-
if assign_expr:
826-
# If we've peeked into an assignment and decided this is re-using a
827-
# directive name as an assign, throw a warning about using directive
828-
# name in a weird context.
829-
#
830-
# yet another corner case of totally legit directive syntax:
831-
# define = foo
832-
# define = <nothing>
833-
# export CC=gcc
834-
# override CC=xcc
835-
# override define foo=
836-
837-
if dir_str in ("define", "export", "override") and len(expr.token_list) > 1:
838-
# These directives can validly be an assignment so parse them as
839-
# a directive. The len() above checks that there's multiple
840-
# symbols on the LHS before the assignment operator.
841-
842-
# extract the directive from the expression
843-
# We want to throw away the first Literal containing the directive
844-
# expr is a ref into assign_expr.token_list[0]
845-
# so I should be able to delete expr.token_list[0] and kill the directive literal
846-
assert assign_expr.token_list[0].token_list[0] is expr.token_list[0]
847-
848-
del expr.token_list[0]
849-
850-
dir_ = parse_directive(assign_expr, directive_vstr, virt_line, vline_iter)
851-
else:
852-
# GNU Make doesn't warn like this (pats self on back).
853-
logger.warning("re-using a directive name \"%s\" in an assignment at %r", dir_str, assign_expr.get_pos())
854-
return assign_expr
855-
else:
856-
# extract the directive from the expression
857-
# We want to throw away the first Literal containing the directive
858-
del expr.token_list[0]
859-
860-
dir_ = parse_directive(expr, directive_vstr, virt_line, vline_iter)
861-
862-
# print("parse directive=%s" % dir_.makefile())
863-
return dir_
864-
865755
def parse_rule(vchar_scanner):
866756
lhs = tokenizer.tokenize_rule(vchar_scanner)
867757
if lhs is None:

pymake/pymake.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,7 @@ def execute(makefile, args):
639639
return exit_status["error"] if exit_code else exit_status["success"]
640640

641641
def _run_it(args):
642+
logger.debug("run_it args=\"%s\"", args)
642643
# -C option
643644
if args.directory:
644645
os.chdir(os.path.join(*args.directory))

pymake/symbol.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ class Literal(Symbol):
144144
def __init__(self, vstring):
145145
# catch bugs where I create empty Literals
146146
if not len(vstring):
147-
breakpoint()
148-
assert len(vstring)
147+
raise InternalError("attempt to create empty vstring")
149148

150149
# cache the literal string
151150
self._str = str(vstring)
@@ -349,7 +348,12 @@ def assign(lhs, op, rhs, symbol_table, flags=0):
349348
pos = lhs.get_pos()
350349

351350
logger.debug("assignment rhs=%s", rhs)
352-
# breakpoint()
351+
352+
if flags & AssignmentExpression.FLAG_OVERRIDE:
353+
raise NotImplementedError("override")
354+
355+
if flags & AssignmentExpression.FLAG_PRIVATE:
356+
raise NotImplementedError("private")
353357

354358
if op_str == "?=":
355359
symbol_table.maybe_add(key, rhs, pos)
@@ -422,7 +426,6 @@ def add_modifiers(self, modifier_list):
422426
self.modifier_flags |= self.FLAG_OVERRIDE
423427
else:
424428
assert 0, s
425-
426429
self.sanity()
427430

428431
def __str__(self):

pymake/symtable.py

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def value(self):
8282

8383
def set_value(self, value, pos):
8484
# TODO add a stack where the values get changed
85+
logger.debug("overwrite value name=%s at pos=%r", self.name, pos)
8586
self.pos = pos
8687
self._value = value
8788

@@ -151,9 +152,21 @@ def sanity(self):
151152
# -- GNU Make manual Version 4.3 Jan 2020
152153
class DefaultEntry(Entry):
153154
origin = "default"
154-
never_export = True
155155

156-
class CallbackEntry(DefaultEntry):
156+
# An Immutable Entry is one where we cannot change the value BUT we can
157+
# completely change the entry. For example, .VARIABLES has a special meaning in
158+
# Make (a string containing names of all the defined variables) BUT we can do something bone-headed like:
159+
#
160+
# .VARIABLES:=foo # kill the .VARIABLES variable
161+
#
162+
# When am Immutable Entry is updated, the set_value() will throw an exception.
163+
# The Symbol Table add() method will then create a new entry instead.
164+
#
165+
class ImmutablemixIn:
166+
def set_value(self, value, pos):
167+
raise ValueError("cannot update CallbackEntry name=%s" % self.name)
168+
169+
class CallbackEntry(ImmutablemixIn, DefaultEntry):
157170
# Some built-in variables have complex requirements that can't be stored as
158171
# a simple string in the symbol table. For example .VARIABLES returns a
159172
# list of the variables currently defined.
@@ -167,12 +180,13 @@ def __init__(self, name, callback_fn):
167180
def eval(self, symbol_table):
168181
return self._value(symbol_table)
169182

183+
170184
# Environment Variables are higher precedence than File variables. But Command
171185
# Line args are higher precedence than Environment Variables.
172186
# "By default, only variables that came from the environment or the
173187
# command line are passed to recursive invocations."
174188
# -- GNU Make manual Version 4.3 Jan 2020
175-
class EnvVarEntry(Entry):
189+
class EnvVarEntry(ImmutablemixIn, Entry):
176190
origin = "environment"
177191

178192
def __init__(self, name, value):
@@ -188,16 +202,13 @@ def __init__(self, name, value):
188202
# "By default, only variables that came from the environment or the
189203
# command line are passed to recursive invocations."
190204
# -- GNU Make manual Version 4.3 Jan 2020
191-
class CommandLineEntry(Entry):
205+
class CommandLineEntry(ImmutablemixIn, Entry):
192206
origin = "command line"
193207

194208
def __init__(self, name, value):
195209
super().__init__(name,value)
196210
self.set_export(Export.SUBMAKE)
197211

198-
def set_value(self, value, pos):
199-
pass
200-
201212

202213
class AutomaticEntry(Entry):
203214
origin = "automatic"
@@ -312,29 +323,49 @@ def add(self, name, value, pos=None):
312323

313324
try:
314325
entry = self.symbols[name]
315-
logger.debug("overwrite value name=%s at pos=%r", entry.name, pos)
316326
except KeyError:
317-
entry = None
318-
319-
# if we didn't find it, make it
320-
if self.command_line_flag:
321-
# this flag is a weird hack to support vars created by eval()'ing
322-
# expressions from the command line
323-
new_entry = CommandLineEntry(name, value)
324-
# command line vars are always exported until explicitly unexported
327+
entry = None
328+
329+
if entry is None:
330+
# easy case: if we didn't find it, make it
331+
if self.command_line_flag:
332+
# this flag is a weird hack to support vars created by eval()'ing
333+
# expressions from the command line
334+
# - command line vars are always exported until explicitly
335+
# marked 'unexport'
336+
# - command line vars cannot be overwritten by file variables
337+
# except by the 'override' directive (not yet implemented)
338+
entry = CommandLineEntry(name, value)
339+
else:
340+
if pos and pos[0] == "@defaults":
341+
entry = DefaultEntry(name, value, pos)
342+
else:
343+
entry = FileEntry(name, value, pos)
344+
entry.set_export(self.export_default_value)
345+
346+
self._add_entry(entry)
325347
else:
326-
# command line > file
327-
if isinstance(entry,CommandLineEntry):
328-
return
329-
330-
new_entry = FileEntry(name, value, pos)
331-
new_entry.set_export(self.export_default_value)
332-
333-
# XXX not sure read-only is a good idea yet
334-
# if entry.read_only:
335-
# raise PermissionDenied(name)
336-
337-
self._add_entry(new_entry)
348+
overwrite = False
349+
try:
350+
entry.set_value(value, pos)
351+
except ValueError:
352+
overwrite = True
353+
354+
if overwrite:
355+
# Found an immutable variable that refuses to be overwritten.
356+
# So (maybe) create a new one.
357+
overwrite_entry = None
358+
if self.command_line_flag:
359+
# command line entry can happily override command line entry
360+
overwrite_entry = CommandLineEntry(name, value)
361+
else:
362+
# do not overwrite a command line entry when we're
363+
# no longer processing command line variables
364+
if not isinstance(entry, CommandLineEntry):
365+
overwrite_entry = FileEntry(name, value, pos)
366+
367+
if overwrite_entry:
368+
self._add_entry(overwrite_entry)
338369

339370
def add_automatic(self, name, value, pos):
340371
entry = AutomaticEntry(name, value, pos)

tests/test_assign.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Copyright (C) 2014-2024 David Poole davep@mbuf.com david.poole@ericsson.com
3+
14
import run
25

36
# Test all the ways we can assign a variable
@@ -37,3 +40,14 @@ def test_add():
3740
expect = "FOO=foo bar"
3841
run_test(makefile, expect)
3942

43+
def test_update():
44+
makefile = """
45+
FOO:=foo
46+
FOO:=$(FOO) foo
47+
FOO:=bar $(FOO)
48+
49+
@:; @echo $(FOO)
50+
"""
51+
expect = "bar foo foo"
52+
run_test(makefile, expect)
53+

tests/test_export.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,12 +264,35 @@ def test_specific_unexport():
264264
expect=("gcc","no CFLAGS for you")
265265
run_test(makefile,expect)
266266

267-
@pytest.mark.skip(reason="FIXME")
268-
def test_circular_export_bug():
267+
def test_circular_export():
269268
makefile="""
270-
VERSION=$(shell cat /etc/os-release)
271-
export VERSION
272-
@:;@:
269+
FOO=$(shell echo foo)
270+
export FOO
271+
@:; @printenv FOO
273272
"""
274-
expect = ("",)
273+
expect = ("foo",)
275274
run_test(makefile,expect)
275+
276+
def test_export_commandline_var():
277+
# command line variable assignments always override export status of file
278+
# variables
279+
makefile="""
280+
export FOO:=foo
281+
@:; @echo $(FOO)
282+
"""
283+
expect = ("bar",)
284+
run_test(makefile, expect, extra_args=("FOO=bar",))
285+
286+
287+
def test_export_commandline_var_explicit():
288+
# command line variable assignments always override export status of file
289+
# variables
290+
makefile="""
291+
export FOO:=foo
292+
@:; @echo $(FOO)
293+
"""
294+
expect = ("bar",)
295+
# use explicity assign instead of recursive assign
296+
run_test(makefile, expect, extra_args=("FOO:=bar",))
297+
298+

tests/test_origin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,13 @@ def test_command_line():
8787
output = run.gnumake_string(makefile, extra_args=("FOO=BAR",))
8888
assert output.strip() == "command line", output
8989

90+
def test_default():
91+
makefile="""
92+
ifneq ($(origin CC),default)
93+
$(error CC origin should be 'default' not '$(origin CC)')
94+
endif
95+
96+
@:;@:
97+
"""
98+
run.simple_test(makefile)
99+

0 commit comments

Comments
 (0)