Skip to content

Commit 9e98b93

Browse files
committed
fix bug with multiple targets appending same recipe
Python references rose up to bite me. Each Rule has the same RecipeList instance but I was adding recipes to each RecipeList. So there were duplicate recipes for each rule.
1 parent ed214ed commit 9e98b93

4 files changed

Lines changed: 56 additions & 10 deletions

File tree

pymake/pymake.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,15 @@ def _is_recipe_comment(tok):
294294
# corner case to handle a line with leading <tab> and a comment
295295
# which will be a Recipe if we're inside a Rule but is ignored
296296
# before we've seen a Rule
297+
tok = tok.token_list[0]
297298
try:
298-
tok = tok.token_list[0]
299299
lit = tok.literal
300300
except AttributeError:
301301
# not a literal therefore definitely can't be a comment
302302
return False
303-
return tokenizer.seek_comment(iter(tok.string))
303+
s = tok.makefile().lstrip()
304+
return s[0] == '#'
305+
# return tokenizer.seek_comment(iter(tok.string))
304306

305307
for statement in stmt_list:
306308
# sanity check; everything has to have a successful get_pos()
@@ -314,18 +316,46 @@ def _is_recipe_comment(tok):
314316
# If this is just a comment line, ignore it
315317
# But if we haven't seen a rule, throw the infamous error.
316318

317-
if not curr_rules and not _is_recipe_comment(statement):
319+
# use a better name
320+
recipe = statement
321+
322+
if not curr_rules and not _is_recipe_comment(recipe):
318323
# So We're confused.
319324
raise RecipeCommencesBeforeFirstTarget(pos=statement.get_pos())
320325

321-
[rule.add_recipe(statement) for rule in curr_rules]
326+
# The RuleDB contains a Rule instance for each target. The RuleDB
327+
# is key'd by the target string. Given a rule with multiple targets
328+
# such as
329+
# a b c d: ; @echo $@
330+
# there will be four separate Rule instances in the RuleDB.
331+
#
332+
# Each Rule instance has a RecipeList instance (which is basically
333+
# just a wrapper around a python list to automatically support the
334+
# .makefile() method). The Rule's RecipeList is created from the
335+
# RuleExpression.
336+
#
337+
# Here's the punchline:
338+
# The Symbol class hierarchy creates one RuleExpression and one
339+
# RecipeList even if there are multiple targets. But the RuleDB
340+
# uses a separate Rule instance for each target. The RecipeList
341+
# instance is therefore shared between each Rule (basically, all
342+
# Rule instances point to the exact same RecipeList instance)
343+
#
344+
if curr_rules:
345+
if len(curr_rules) > 1:
346+
# sanity clause
347+
id_ = id(curr_rules[0].recipe_list)
348+
assert all( id_==id(r.recipe_list) for r in curr_rules)
349+
curr_rules[0].add_recipe(recipe)
350+
# [rule.add_recipe(recipe) for rule in curr_rules]
322351

323352
elif isinstance(statement,RuleExpression):
324353
# restart the rules list but maintain the same ref!
325354
# (need the same ref because this array is passed by the caller and
326355
# we need to track the values across calls to this function)
327356
curr_rules.clear()
328357

358+
# use a better name
329359
rule_expr = statement
330360
m = rule_expr.makefile()
331361

@@ -602,7 +632,7 @@ def execute(makefile, args):
602632
# target" error.
603633
#
604634
# Basically, we have context sensitive evaluation.
605-
curr_rules = []
635+
curr_rules = [] # array of Rule instances
606636

607637
# For handling $(eval) This is not my proudest moment. I originally
608638
# designed my make function implementations to be truly functional (no side

pymake/rules.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ def __init__(self, target, prereq_list, recipe_list, assignment, pos):
3434
assert '\t' not in target
3535
assert target
3636
assert all( (isinstance(s,str) for s in prereq_list) )
37+
_ = recipe_list.makefile
3738

3839
logger.debug("create rule target=%r at %r", target, pos)
3940
self.target = target
4041
self.prereq_list = list(prereq_list)
41-
self.recipe_list = list(recipe_list)
42+
self.recipe_list = recipe_list
4243
self.assignment_list = [assignment] if assignment else []
4344

4445
_rule_sanity(pos, prereq_list, assignment)
@@ -88,6 +89,8 @@ def graphviz_graph(self):
8889

8990
class RuleDB:
9091
def __init__(self):
92+
# key: target (python string)
93+
# value: instance of class Rule
9194
self.rules = {}
9295

9396
# first rule added becomes the default
@@ -96,6 +99,8 @@ def __init__(self):
9699
def add(self, target, prereq_list, recipe_list, assignment, pos):
97100

98101
# ha ha type checking
102+
_ = recipe_list.makefile
103+
99104
logger.debug("add rule target=%r at %r", target, pos)
100105

101106
if not target:

pymake/symbol.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,9 @@ def __init__(self, recipe_list):
639639
super().__init__(recipe_list)
640640

641641
def append(self, recipe):
642+
# ha ha type checking
643+
assert isinstance(recipe, Recipe), recipe
644+
642645
self.token_list.append(recipe)
643646

644647
def makefile(self):

tests/test_makefile.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import re
99

1010
import pytest
11-
#import pymake
11+
12+
import run
1213

1314
# Find file relative to tests location
1415
test_dir = os.path.dirname(__file__)
@@ -134,10 +135,17 @@ def test_makefile(infilename):
134135
outfile.write(ground_truth)
135136
assert ground_truth == test_output
136137

138+
@pytest.mark.skip(reason="run manually because slow")
139+
@pytest.mark.parametrize("infilename", infilename_list)
140+
def test_pymake_options(infilename):
141+
# try some pymake args on the test makefiles
142+
# (sanity test code paths)
143+
filepath = os.path.join(example_dir, infilename)
144+
s = run.run_pymake(filepath, extra_args=("--print-rule",))
145+
s = run.run_pymake(filepath, extra_args=("-S",))
146+
s = run.run_pymake(filepath, extra_args=("-n",))
147+
s = run.run_pymake(filepath, extra_args=("--output", "/dev/null"))
137148

138-
# can I run pymake w/i pytest w/o spawning an additional python?
139-
# print(os.getcwd())
140-
# makefile = pymake.parse_makefile(infilename)
141149

142150
def test_value():
143151
# TODO need some way of testing value() function w/o running afoul of my $(P) problem

0 commit comments

Comments
 (0)