Skip to content
Merged
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
28 changes: 18 additions & 10 deletions pymake/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down
39 changes: 31 additions & 8 deletions pymake/pymake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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:])
Expand Down
22 changes: 15 additions & 7 deletions pymake/symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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 == "?=":
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
35 changes: 34 additions & 1 deletion tests/define.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
55 changes: 55 additions & 0 deletions tests/eval.mk
Original file line number Diff line number Diff line change
@@ -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))

@:;@:

20 changes: 20 additions & 0 deletions tests/test_define.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Loading
Loading