Skip to content
Open
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
104 changes: 98 additions & 6 deletions make_profiler/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,11 @@ def parse_target(token: Tuple[Tokens, str]):
rest = rest.strip()
if '|' in rest:
deps_part, order_part = rest.split('|', 1)
order_deps = sorted(order_part.strip().split()) if order_part.strip() else []
order_deps = order_part.strip().split() if order_part.strip() else []
else:
deps_part = rest
order_deps = []
deps = sorted(deps_part.strip().split()) if deps_part.strip() else []
deps = deps_part.strip().split() if deps_part.strip() else []

body = parse_body()
ast.append(
Expand Down Expand Up @@ -192,9 +192,22 @@ def parse_body() -> List[Tuple[Tokens, str]]:


def get_dependencies_influences(ast: List[Tuple[Tokens, Dict[str, Any]]]):
"""Build the dependency graph consumed by :func:`export_dot`.

Repeated explicit single-colon rules merge and deduplicate their
prerequisites in first-seen order. A prerequisite seen normally is
normal everywhere, even if another rule lists it as order-only. Pattern
rules remain alternatives, so their latest definition overwrites the
stored prerequisite lists rather than merging them. Both graph directions
describe that same stored alternative.

Returns ``(dependencies, influences, order_only, indirect_influences)``.
``order_only`` contains only prerequisites that were never normal;
``export_dot`` uses it to hide otherwise-unconnected order-only nodes.
"""
dependencies = {}
dependency_membership = {}
influences = collections.defaultdict(set)
order_only = set()
indirect_influences = collections.defaultdict(set)

alias_map = {}
Expand All @@ -209,6 +222,15 @@ def get_dependencies_influences(ast: List[Tuple[Tokens, Dict[str, Any]]]):
def alias(name: str) -> str:
return alias_map.get(name, name)

# A declared target remains a graph node even when a later pattern-rule
# alternative drops its last incoming edge. ``descendants`` below walks
# every node, so deleting such a node can mutate ``influences`` mid-walk.
declared_targets = {
alias(item['target'])
for item_t, item in ast
if item_t == Tokens.target and item['target'] != '.PHONY'
}

for item_t, item in ast:
if item_t != Tokens.target:
continue
Expand All @@ -220,15 +242,86 @@ def alias(name: str) -> str:
if target in ('.PHONY',):
continue

dependencies[target] = [deps, order_deps]
# Pattern rules are alternatives, not repeated explicit rules. Keep
# the base overwrite representation instead of applying explicit-rule
# prerequisite accumulation to them. Remove reverse edges from the
# superseded alternative so critical-path traversal never reaches a
# dependency absent from the forward graph.
is_pattern_rule = any('%' in name for name in item.get('all_targets', []))
if is_pattern_rule:
previous_deps, _previous_order_deps = dependencies.get(target, [[], []])
for dep in set(previous_deps) - set(deps):
influences[dep].discard(target)

normal_dep_names = set(deps)
order_deps = [dep for dep in order_deps if dep not in normal_dep_names]
dependencies[target] = [deps, order_deps]
Comment thread
Komzpa marked this conversation as resolved.
influences[target]
for dep in deps:
influences[dep].add(target)
for dep in order_deps:
influences[dep]
continue

# GNU Make combines prerequisites from repeated single-colon rules.
# Keep the reverse graph aligned with the forward influences below so
# critical-path traversal can return through every incoming edge.
existing_deps, existing_order_deps = dependencies.setdefault(target, [[], []])
Comment thread
Komzpa marked this conversation as resolved.
if target not in dependency_membership:
dependency_membership[target] = (set(existing_deps), set(existing_order_deps))
known_dep_names, known_order_dep_names = dependency_membership[target]
for dep in deps:
if dep in known_order_dep_names:
known_order_dep_names.remove(dep)
if dep not in known_dep_names:
existing_deps.append(dep)
known_dep_names.add(dep)
for dep in order_deps:
if dep in known_dep_names or dep in known_order_dep_names:
continue
existing_order_deps.append(dep)
known_order_dep_names.add(dep)

# influences
influences[target]
for k in deps:
influences[k].add(target)
for k in order_deps:
influences[k]
order_only.update(order_deps)

# Promotions above only update membership sets: removing an item from the
# ordered list for every promotion would repeatedly scan that list. Filter
# each target once instead, retaining the first-seen order of surviving
# order-only prerequisites.
for target, (_known_deps, known_order_deps) in dependency_membership.items():
existing_order_deps = dependencies[target][1]
dependencies[target][1] = [
dep for dep in existing_order_deps if dep in known_order_deps
]

# Classify prerequisites from the completed forward graph so a superseded
# pattern alternative cannot keep a stale normal or order-only node.
normal_dependencies = {
dependency
for normal_deps, _order_deps in dependencies.values()
for dependency in normal_deps
}
order_only = {
dependency
for _normal_deps, order_deps in dependencies.values()
for dependency in order_deps
}
order_only.difference_update(normal_dependencies)

# Pattern replacements leave empty reverse nodes behind until the complete
# forward graph is known. Clean them once here instead of repeatedly
# rescanning all dependencies for every replacement. Declared targets and
# live normal/order-only prerequisites must remain graph nodes even if they
# have no outgoing influence edges.
live_nodes = declared_targets | normal_dependencies | order_only
for node in list(influences):
if not influences[node] and node not in live_nodes:
del influences[node]

# Cache previously calculated descendants to avoid quadratic behaviour on
# large graphs. ``descendants(target)`` returns a set of all nodes reachable
Expand All @@ -250,4 +343,3 @@ def descendants(target):
)

return dependencies, influences, order_only, indirect_influences

Loading