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
19 changes: 6 additions & 13 deletions pymake/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,21 @@ class Call(FunctionWithArguments):
def eval(self, symbol_table):
var = "".join([a.eval(symbol_table) for a in self.args[0]])

symbol_table.push_layer()

arg_stack = []
for idx, arg_list in enumerate(self.args[1:]):
arg = "".join([a.eval(symbol_table) for a in arg_list])
varname = "%d" % (idx+1)
symbol_table.push(varname)
symbol_table.add(varname, arg)
# save the arg name so we can pop it from the symbol table
arg_stack.append(varname)


# all we need to do is fetch() from the symbol table and the expression
# will be eval'd
s = symbol_table.fetch(var)
# breakpoint()

# pop the args off the symbol table in reverse order because why not
while 1:
try:
symbol_table.pop(arg_stack.pop())
except IndexError:
break
symbol_table.pop_layer()

return s

Expand Down Expand Up @@ -183,16 +177,15 @@ def eval(self, symbol_table):
# array of strings
list_ = "".join([a.eval(symbol_table) for a in self.args[1]]).split()

# breakpoint()

out_str_list = []
symbol_table.push(var)
symbol_table.push_layer()

for item in list_:
symbol_table.add(var, item, self.args[0][0].get_pos())
text = "".join([a.eval(symbol_table) for a in self.args[2]])
out_str_list.append(text)

symbol_table.pop(var)
symbol_table.pop_layer()

return " ".join(out_str_list)

Expand Down
41 changes: 27 additions & 14 deletions pymake/pymake.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,26 +308,37 @@ def _is_recipe_comment(tok):
curr_rules.clear()

rule_expr = statement
m = rule_expr.makefile()

# Note a RuleExpression.eval() is very different from all other
# eval() methods (so far). A RuleExpression.eval() returns two
# arrays of strings: targets, prereqs
# All other eval() returns a string.
target_list, prereq_list = rule_expr.eval(symtable)


if rule_expr.assignment:
# Have a target-specific assignment expression.
# There will be no prereqs or attached recipes.
#
# The assignments will not be eval'd here but rather stored in
# the Rule and eval'd when the Rule is used.
assert not prereq_list

if target_list:
for t in target_list:
rule = rules.Rule(t, prereq_list, rule_expr.recipe_list, rule_expr.get_pos())
rulesdb.add(rule)
rule = rulesdb.add(t, prereq_list, rule_expr.recipe_list,
rule_expr.assignment, rule_expr.get_pos())
curr_rules.append(rule)
# we're in a big confusing loop so get rid of a name I re-use
del rule
else:
# "We accept and ignore rules without targets for
# compatibility with SunOS 4 make." -- GNU Make src/read.c
# grumble grumble grumble
# We need to have a Rule in curr_rules to correctly parse
# ambiguous statements with have a leading Recipe Prefix
# (aka <tab>)
rule = rules.Rule(None, prereq_list, rule_expr.recipe_list, rule_expr.get_pos())
rule = rules.Rule(None, prereq_list, rule_expr.recipe_list, rule_expr.assignment, rule_expr.get_pos())
# don't add this Rule to the DB but do let the world know we are in a Rule
curr_rules.append(rule)

Expand Down Expand Up @@ -432,18 +443,15 @@ def check_prefixes(s):
s = s[1:]

elif s[0] == '+':
raise NotImplementedError
raise NotImplementedError()

else:
break

return s, ignore_failure, silent

# TODO many more automatic variables
symtable.push("@")
symtable.push("^")
symtable.push("+")
symtable.push("<")
symtable.push_layer()
symtable.add_automatic("@", rule.target, recipe.get_pos())
symtable.add_automatic("^", " ".join(remove_duplicates(rule.prereq_list)), rule.get_pos())
symtable.add_automatic("+", " ".join(rule.prereq_list), rule.get_pos())
Expand All @@ -452,11 +460,6 @@ def check_prefixes(s):
cmd_s = recipe.eval(symtable)
# print("execute_recipe \"%r\"" % cmd_s)

symtable.pop("@")
symtable.pop("^")
symtable.pop("+")
symtable.pop("<")

# Defining Multi-Line Variables.
# "However, note that using two separate lines means make will invoke the shell twice, running
# an independent sub-shell for each line. See Section 5.3 [Recipe Execution], page 46."
Expand Down Expand Up @@ -522,6 +525,8 @@ def check_prefixes(s):
break
exit_code = 0

symtable.pop_layer()

return exit_status["error"] if exit_code else exit_status["success"]

def execute(makefile, args):
Expand Down Expand Up @@ -650,10 +655,18 @@ def execute(makefile, args):
# this warning catches where I fail to find an implicit rule
logger.warning("I didn't find a recipe to build target=\"%s\"", target)

# target specific variables
if rule.assignment_list:
symtable.push_layer()
for asn in rule.assignment_list:
asn.eval(symtable)

for recipe in rule.recipe_list:
exit_code = execute_recipe(rule, recipe, symtable, args)
if exit_code != 0:
break
if rule.assignment_list:
symtable.pop_layer()
if exit_code != 0:
break

Expand Down
72 changes: 58 additions & 14 deletions pymake/rules.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2014-2025 David Poole davep@mbuf.com david.poole@ericsson.com

import logging
import os
Expand All @@ -12,11 +13,19 @@

_debug = True

def _rule_sanity(pos, prereq_list, assignment):
# if we have a prereq list, we cannot have an assignment
# if we have an assignment, we cannot have a prereq list
if prereq_list:
assert not assignment, pos
elif assignment:
assert not prereq_list, pos

class Rule:
# A Rule contains a target (string) and the prereqs (array of string).
# Rule contains a RecipeList from the Symbol hierarchy but don't want to
# pull symbol.py in here (to keep pymake.py a little more clean).
def __init__(self, target, prereq_list, recipe_list, pos):
def __init__(self, target, prereq_list, recipe_list, assignment, pos):
# ha ha type checking
# target is string and prereq_list[] is array of strings
# target can be None to handle rules without a target which GNU Make allows
Expand All @@ -30,6 +39,9 @@ def __init__(self, target, prereq_list, recipe_list, pos):
self.target = target
self.prereq_list = prereq_list
self.recipe_list = recipe_list
self.assignment_list = [assignment] if assignment else []

_rule_sanity(pos, prereq_list, assignment)

# need to pass in an explict pos because the target is a string not a
# Symbol.
Expand All @@ -39,13 +51,22 @@ def __str__(self):
target = "" if self.target is None else self.target
return "%s <- %s" % (target, ",".join(self.prereq_list))

def makefile(self):
s = "".join([ "%s:%s\n" % (self.target,a.makefile()) for a in self.assignment_list])
s += "%s : %s\n%s" % (self.target, " ".join(self.prereq_list), self.recipe_list.makefile())
return s

def get_pos(self):
return self.pos

def add_recipe(self, recipe):
logger.debug("add recipe to rule=%r", self)
logger.debug("add recipe to rule=%r", self.target)
self.recipe_list.append(recipe)

def add_assignment(self, assignment):
logger.debug("add assignment to rule=%r", self.target)
self.assignment_list.append(assignment)

def graphviz_graph(self):
if self.target is None:
raise ValueError("cannot build a graphviz for rule without targets at pos=%r" % self.pos)
Expand All @@ -72,28 +93,51 @@ def __init__(self):
# first rule added becomes the default
self.default = None

def add(self, rule):
def add(self, target, prereq_list, recipe_list, assignment, pos):

# ha ha type checking
logger.debug("add rule=%s at %r", rule, rule.get_pos())
assert isinstance(rule,Rule), type(rule)
logger.debug("add rule target=%r at %r", target, pos)

if not rule.target:
if not target:
# TODO
breakpoint()
assert rule.target

if rule.target == ".PHONY":
# TODO
return
if target == ".PHONY":
raise NotImplementedError(target)

_rule_sanity(pos, prereq_list, assignment)

# do we currently have a rule already with this target?
rule = self.rules.get(target,None)
if not rule:
# create new
rule = Rule(target, prereq_list, recipe_list, assignment, pos)
else:
# could be an update or could be an overwrite
#
# If we don't have an assignment arg, and we've already seen some recipes,
# then we have a new rule.
if not assignment and len(rule.recipe_list):
# overwrite
warning_message(pos, "overriding recipe for target '%s'" % target )
warning_message(rule.get_pos(), "ignoring old recipe for target '%s'" % target)

rule = Rule(target, prereq_list, recipe_list, assignment, pos)
else:
# update existing rule
if recipe_list:
[rule.add_recipe(r) for r in recipe_list]
elif assignment:
rule.add_assignment(assignment)

self.rules[rule.target] = rule

if self.default is None:
self.default = rule.target

# GNU Make doesn't warn on this sort of thing but I want to see it.
if rule.target in self.rules and rule.prereq_list:
warning_message(rule.get_pos(), "overriding rule \"%s\"" % (rule.target, ))

self.rules[rule.target] = rule
return rule


def get(self, target):
# allow KeyError to propagate
Expand Down
50 changes: 32 additions & 18 deletions pymake/symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,21 +466,11 @@ def __init__(self, token_list):
# add sanity check in constructor
Symbol.validate(token_list)

# ha ha type checking
assert len(token_list)==3, len(token_list)

assert isinstance(token_list[0], TargetList), (type(token_list[0]),)
assert isinstance(token_list[1], RuleOp), (type(token_list[1]),)

if isinstance(token_list[2], PrerequisiteList) :
# all is well
pass
elif isinstance(token_list[2], AssignmentExpression) :
# target specific variable assignment
# see: 6.11 Target-specific Variable Values GNU Make V.4.3 Jan2020
raise NotImplementedError()
else:
assert 0, (type(token_list[2]),)

# Start with a default empty recipe list (so this object will always
# have a RecipeList instance)
self.recipe_list = RecipeList([])
Expand All @@ -489,7 +479,20 @@ def __init__(self, token_list):

self.targets = self.token_list[0]
self.rule_op = self.token_list[1]
self.prereqs = self.token_list[2]
self.prereqs = []

self.assignment = None

if isinstance(token_list[2], PrerequisiteList) :
# all is well
self.prereqs = self.token_list[2]
elif isinstance(token_list[2], AssignmentExpression) :
# target specific variable assignment
# see: 6.11 Target-specific Variable Values GNU Make V.4.3 Jan2020
self.assignment = token_list[2]
else:
assert 0, (type(token_list[2]),)


def makefile(self):
# rule-targets rule-op prereq-list <CR>
Expand All @@ -509,14 +512,21 @@ def makefile(self):
s += self.rule_op.makefile()

# prerequisite(s)
s += self.prereqs.makefile() + "\n"
if self.prereqs:
s += self.prereqs.makefile()
elif self.assignment:
s += self.assignment.makefile()

# s += "\n"

# recipe(s)
s += self.recipe_list.makefile()
return s

def add_recipe( self, recipe ) :
assert isinstance(recipe, Recipe)
# assignment Rule Expressions cannot have recipes or it's a parse error
assert self.assignment is None

logger.debug("add_recipe() rule=%s", str(self))
self.recipe_list.append(recipe)
Expand All @@ -537,11 +547,14 @@ def eval(self, symbol_table):
# time.

targets = self.targets.eval(symbol_table).split()
prereqs = self.prereqs.eval(symbol_table).split()

# throw away empty strings
targets = [t for t in targets if t]
prereqs = [p for p in prereqs if p]

prereqs = []
if self.prereqs:
prereqs = self.prereqs.eval(symbol_table).split()
# throw away empty strings
prereqs = [p for p in prereqs if p]

return targets, prereqs

Expand Down Expand Up @@ -614,6 +627,8 @@ def eval(self, symbol_table):
# e.g., "echo $$PATH" becomes "echo $PATH" sent to the shell
return s.replace("$$","$")

def makefile(self):
return "\t" + super().makefile()

class RecipeList( Expression ) :
# A collection of Recipe objects
Expand All @@ -630,8 +645,7 @@ def makefile(self):
# newline separated, tab prefixed
s = ""
if len(self.token_list):
s = "\t" + "\n\t".join([t.makefile() for t in self.token_list])
s += "\n"
s = "\n".join([t.makefile() for t in self.token_list])
return s

class Directive(Symbol):
Expand Down
Loading
Loading