Skip to content

Commit 9f05fd6

Browse files
authored
Merge pull request #29 from linuxlizard/dpoole/fix-multiple-targets
fix bug with multiple targets appending same recipe
2 parents ed214ed + 9e98b93 commit 9f05fd6

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)