diff --git a/pymake/functions.py b/pymake/functions.py index 4026623..3d127da 100644 --- a/pymake/functions.py +++ b/pymake/functions.py @@ -16,17 +16,12 @@ from pymake.functions_fs import * from pymake.functions_cond import * from pymake.functions_str import * -from pymake.todo import TODOMixIn - import pymake.shell as shell +from pymake.source import SourceString -__all__ = [ "Info", - "WarningClass", - "Error", - "Shell", - - "make_function", - ] +# FIXME ugly hack dependency injection to solve problems with circular imports +parse_makefile_from_src = None +execute_statement_list = None class PrintingFunction(Function): fmt = None @@ -151,9 +146,22 @@ def eval(self, symbol_table): return s -class Eval(TODOMixIn, Function): +class Eval(Function): name = "eval" + def eval(self, symbol_table): + # call responsible to re-interpret this result + s = "".join([a.eval(symbol_table) for a in self.token_list]) + src = SourceString(s) + makefile = parse_makefile_from_src(src) + + exit_code = execute_statement_list(makefile.token_list, symbol_table.curr_rules, symbol_table.rulesdb, symbol_table) + + # TODO what should I do about exit_code ? + assert exit_code==0, exit_code + + return "" + class Flavor(Function): name = "flavor" diff --git a/pymake/pymake.py b/pymake/pymake.py index 3b913bb..b82aece 100644 --- a/pymake/pymake.py +++ b/pymake/pymake.py @@ -33,6 +33,7 @@ import pymake.submake as submake from pymake.debug import * import pymake.constants as constants +import pymake.functions as functions _debug = False @@ -266,7 +267,7 @@ def _add_internal_db(symtable): # TODO parse automatics -def _execute_statement_list(stmt_list, curr_rules, rulesdb, symtable): +def execute_statement_list(stmt_list, curr_rules, rulesdb, symtable): exit_code = 0 @@ -345,12 +346,16 @@ def _is_recipe_comment(tok): # makefile rules, statements, etc. # GNU Make itself seems to interpret raw text as a rule and # will print a "missing separator" error + # + # Update 20260101 NOPE. The $(eval) function is + # completely different and cannot be handled in the way + # I originally hoped. raise MissingSeparator(statement.get_pos()) else: # A conditional block or include eval returns an array # of parsed Symbols ready for eval. assert isinstance(result,list), type(result) - exit_code = _execute_statement_list(result, curr_rules, rulesdb, symtable) + exit_code = execute_statement_list(result, curr_rules, rulesdb, symtable) except MakeError as err: # Catch our own Error exceptions. Report, break out of our execute loop and leave. @@ -365,10 +370,10 @@ def _is_recipe_comment(tok): except Exception as err: # My code crashed. For shame! logger.exception(err) - logger.error("INTERNAL ERROR eval exception during token makefile=\"\"\"\n%s\n\"\"\"", statement.makefile()) - logger.error("INTERNAL ERROR eval exception during token string=%s", str(statement)) + logger.error("INTERNAL ERROR exception during token makefile=\"\"\"\n%s\n\"\"\"", statement.makefile()) + logger.error("INTERNAL ERROR exception during token string=%s", str(statement)) filename,pos = statement.get_pos() - logger.error("eval failed statement=%r file=%s pos=%s", statement, filename, pos) + logger.error("execute failed pos=%r file=%s statement=%r", pos, filename, statement) exit_code = 1 # leave early on error @@ -523,7 +528,6 @@ def execute(makefile, args): # ha ha type checking assert isinstance(args, pargs.Args) - # tinkering with how to evaluate symtable = SymbolTable(warn_undefined_variables=args.warn_undefined_variables) if not args.no_builtin_rules: @@ -555,7 +559,7 @@ def execute(makefile, args): # so the arglist must be parsed and assignment statements saved. Anything # not an Assignment is likely a target. for onearg in args.argslist: - v = vline.VirtualLine([onearg], (0,0), "@commandline") + v = vline.VirtualLine([onearg], (-1,-1), "@commandline") vchar_scanner = iter(v) stmt = tokenizer.tokenize_assignment_statement(vchar_scanner) if isinstance(stmt,AssignmentExpression): @@ -576,7 +580,24 @@ def execute(makefile, args): # Basically, we have context sensitive evaluation. curr_rules = [] - exit_code = _execute_statement_list(makefile.token_list, curr_rules, rulesdb, symtable) + # For handling $(eval) This is not my proudest moment. I originally + # designed my make function implementations to be truly functional (no side + # effects). My plan was for $(eval) to return a string of Make code that + # would be re-interpretted. But now I'm deep enough into implementation to + # understand that won't work. The $(eval) function is entirely a side + # effect. The $(eval) function can add rules, execute other functions, + # anything. And the $(eval) has to happen exactly in the place where it's + # called. + # $(info $(eval foo:bar)) # add a rule; $(info) would consume the string + # if I simply returned "foo:bar" from $(eval) + # + # I need a way to send down the current state of the make (specifically, + # rules). The symbol table is the only argument passed between make + # functions. + symtable.curr_rules = curr_rules + symtable.rulesdb = rulesdb + + exit_code = execute_statement_list(makefile.token_list, curr_rules, rulesdb, symtable) if exit_code: return exit_code @@ -677,6 +698,8 @@ def _run_it(args): parser.parse_vline = parse_vline symbol.parse_vline = parse_vline symbol.tokenize_line = tokenizer.tokenize_line +functions.parse_makefile_from_src = parse_makefile_from_src +functions.execute_statement_list = execute_statement_list def main(): args = pargs.parse_args(sys.argv[1:]) diff --git a/pymake/symbol.py b/pymake/symbol.py index d1597f2..6961800 100644 --- a/pymake/symbol.py +++ b/pymake/symbol.py @@ -281,6 +281,7 @@ class AssignmentExpression(Expression): FLAG_EXPORT = 1<<1 FLAG_PRIVATE = 1<<2 FLAG_OVERRIDE = 1<<3 + FLAG_DEFINE_BLOCK = 1<<31 def __init__(self, token_list, modifier_list=None): super().__init__(token_list) @@ -316,7 +317,8 @@ def assign(lhs, op, rhs, symbol_table, flags=0): if op_str == ":=" or op_str == "::=": # simply expanded - rhs = rhs.eval(symbol_table) + if not (flags & AssignmentExpression.FLAG_DEFINE_BLOCK): + rhs = rhs.eval(symbol_table) elif op_str == "=": # recursively expanded # store the expression in the symbol table without evaluating @@ -326,6 +328,10 @@ def assign(lhs, op, rhs, symbol_table, flags=0): if Version.major < 4: raise VersionError("!= not in this version of make") + if flags & AssignmentExpression.FLAG_DEFINE_BLOCK: + assert isinstance(rhs,DefineBlock), type(rhs) + rhs = [rhs,] + # execute RHS as shell rhs = shell.execute_tokens(rhs, symbol_table ) elif op_str == "?=": @@ -375,10 +381,6 @@ def eval(self, symbol_table): # pyfiles := $(wildcard foo*.py) $(wildcard bar*.py) $(wildcard baz*.py) assert len(self.token_list) == 3 -# lhs = self.token_list[0] -# op = self.token_list[1] -# rhs = self.token_list[2] -# return self.assign(self.lhs, self.assign_op, self.rhs, symbol_table, self.modifier_flags) @property @@ -392,7 +394,7 @@ def assign_op(self): # convenience method to get the assignment operator assert isinstance(self.token_list[1], AssignOp), type(self.token_list[1]) return self.token_list[1] - + @property def rhs(self): # convenience method to get the RHS (right hand side) @@ -1246,13 +1248,19 @@ def makefile(self): def eval(self, symbol_table): # self.expression contains the directive's name + # TODO 'define' variable modifier flags + + # silly hack + flags = AssignmentExpression.FLAG_DEFINE_BLOCK + # use AssignmentExpression so all the variable type and assignment type # special cases are in one place return AssignmentExpression.assign( self.expression.token_list[0], # LHS self.expression.token_list[1], # assignment operator self.block, - symbol_table) + symbol_table, + flags ) class UnDefineDirective(Directive): diff --git a/tests/define.mk b/tests/define.mk index 3bd8b26..6869ffa 100644 --- a/tests/define.mk +++ b/tests/define.mk @@ -7,7 +7,7 @@ define foo = endef # foo foo foo # The following are from the GNU Make manual -define two-lines +define two-lines := @echo two-lines foo @echo two-lines $(bar) endef @@ -147,6 +147,39 @@ $(info cdr=$a) # override previous bar so two-lines should now have qux bar=qux +define shell_example != + echo `date +%N` + echo bar +endef +# output should be identical (only evaluated once) +$(info 1 shell_example=$(shell_example)) +$(info 2 shell_example=$(shell_example)) + +define silly_example := + FOO:=foo + BAR:=bar +endef +ifdef FOO +$(error dave code is stupid) +endif + +$(eval $(silly_example)) +$(info FOO=$(FOO) BAR=$(BAR)) + +ifneq ($(FOO),foo) +$(error FOO missing) +endif + +define shell_example != + echo `date +%N` + echo bar +endef + +$(info $(shell_example)) +$(info $(shell_example)) + +@:;@: + .PHONY: all all : $(call two-lines) diff --git a/tests/eval.mk b/tests/eval.mk new file mode 100644 index 0000000..73a277e --- /dev/null +++ b/tests/eval.mk @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2024-2025 David Poole david.poole@ericsson.com +# +# test the eval function + +$(eval FOO:=foo) +$(info a FOO=$(FOO)) +$(eval $$(info b FOO=$$(FOO))) + +$(eval $$(info hello world 1)) + +HELLO:=$$(info hello world 2) +$(eval $(HELLO)) + +FOO:=BAZ:=$$(shell echo baz) +$(eval $(FOO)) +$(info BAZ=$(BAZ)) + +FOO=$(1)=$$(shell echo $(1)) + +$(info $(call FOO,foo)) +$(eval $(call FOO,foo)) +$(eval $(call FOO,bar)) +$(info foo=$(foo)) +$(info bar=$(bar)) + +define large_comment + $(info this is a contrived example) + $(info showing a multi-line variable) +endef + +$(eval $(large_comment)) + +FOO:=FOO error if you see this FOO +BAR:=BAR error if you see this BAR + +define silly_example + FOO:=foo + BAR:=bar +endef + +$(eval $(silly_example)) + +ifneq ($(FOO),foo) +$(error FOO fail) +endif + +ifneq ($(BAR),bar) +$(error BAR fail) +endif + +$(info FOO=$(FOO) BAR=$(BAR)) + +@:;@: + diff --git a/tests/test_define.py b/tests/test_define.py index e9d851a..3969f8d 100644 --- a/tests/test_define.py +++ b/tests/test_define.py @@ -14,6 +14,8 @@ import pymake.source as source from pymake.error import * +import run + # turn on some extra code paths that allow normally incorrect types to work pymake.symbol._testing = True pymake.vline._testing = True @@ -225,3 +227,21 @@ def test_nested_define(): with pytest.raises(StopIteration): s = next(vline_iter) +@pytest.mark.skip(reason="spaces handling in multi-line shell assign is broken") +def test_shell_define(): + makefile = """ +define shell_example != + echo foo + echo bar +endef +$(info >>$(shell_example)<<) +@:;@: +""" + s1 = run.gnumake_string(makefile) + print("make=",s1) + + s2 = run.pymake_string(makefile) + print("pymake=",s2) + + assert s1==s2 + diff --git a/tests/test_eval.py b/tests/test_eval.py new file mode 100644 index 0000000..47602dd --- /dev/null +++ b/tests/test_eval.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2024-2025 David Poole david.poole@ericsson.com +# +# test the $(eval) function + +import run + +def test1(): + makefile=""" +$(eval FOO:=foo) +ifndef FOO +$(error FOO is missing +endif +ifneq ($(FOO),foo) +$(error FOO is wrong) +endif +@:;@: +""" + run.simple_test(makefile) + +def test_rule(): + makefile=""" +$(eval @:;@:) +""" + run.simple_test(makefile) + +def test_two_eval(): + makefile=""" +$(eval BAR:=bar) +$(eval FOO:=$(BAR)) +ifndef FOO +$(error FOO is missing) +endif +ifneq ($(FOO),bar) +$(error foo is wrong) +endif +@:;@: +""" + run.simple_test(makefile) + +def test_eval_return(): + makefile=""" +$(info >>$(eval FOO:=foo)<<) +ifneq ($(FOO),foo) +$(error foo is wrong) +endif +@:;@: +""" + run.simple_test(makefile) diff --git a/tests/test_makefile.py b/tests/test_makefile.py index ffd4c25..299b141 100644 --- a/tests/test_makefile.py +++ b/tests/test_makefile.py @@ -93,7 +93,7 @@ def _run_pymake(infilename): # "file.mk", "call.mk", # "value.mk", # FIXME my .makefile() surrounds single letter varrefs with () even if not in the original - # "eval.mk", + "eval.mk", # "origin.mk", # "flavor.mk", # "shell.mk",