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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__pycache__
build
*.egg-info
*.egg-info
# Regenerated by the test/Makefile's `conditional` regression test fixture
test/gendep.mk
15 changes: 11 additions & 4 deletions src/pymakeutils/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@
import sys


# Function to parse all rules from 'make -pq' output
# Function to parse all rules from 'make -pq' output.
# `goal` is passed as an actual Make goal (rather than only being used to look up its
# prerequisites in the returned dict), so that MAKECMDGOALS-conditional logic in the
# Makefile (e.g. `include`s gated on the requested goal) is exercised the same way it
# would be for a real `make <goal>` invocation, and any prerequisite it generates as a
# side effect (e.g. an auto-generated dependency file) is reflected in the database.
@lru_cache(maxsize=4)
def _parse_makefile(flags=''):
def _parse_makefile(flags='', goal=None):
# Run 'make -pq' and capture its output
cmd = ['make', '-pq']
if goal:
cmd.append(goal)
if flags:
cmd.extend(flags.split())
result = subprocess.run(
Expand Down Expand Up @@ -59,8 +66,8 @@ def _get_prerequisites_recursive(target, targets, recursive=False):
# Function to list prerequisites, optionally recursively
def list_prerequisites(target, recursive=False, debug=False):

# Parse the makefile
targets = _parse_makefile()
# Parse the makefile, passing `target` as the actual Make goal (see `_parse_makefile`)
targets = _parse_makefile(goal=target)

# Handle non-existing target
if target not in targets:
Expand Down
17 changes: 16 additions & 1 deletion test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,19 @@ prereq: preprereq1 preprereq2
cp $< $@
cat preprereq2 >> $@

test2: dirprereq
test2: dirprereq

# Regression test for passing `target` as an actual Make goal to `make -pq`
# (rather than only using it to look up its prerequisites in a goal-less
# database dump). `hiddenprereq` is only a prerequisite of `conditional` once
# `conditional` is the requested goal, mirroring a common pattern where an
# auto-generated dependency file is `-include`d only when relevant to
# MAKECMDGOALS. A tool that queries `make -pq` without passing the goal would
# never see `hiddenprereq`.
gendep.mk:
echo "conditional: hiddenprereq" > $@

-include $(if $(filter conditional,$(MAKECMDGOALS)),gendep.mk)

conditional: standaloneprereq
cp $< $@
1 change: 1 addition & 0 deletions test/standaloneprereq
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
standalone
10 changes: 10 additions & 0 deletions test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ def test_list_dependent_targets_non_recursive():
def test_list_dependent_targets_recursive():
targets = run_cli('list-dependent-make-targets', 'preprereq1', '--recursive').splitlines()
assert targets == ['.DEFAULT_GOAL', 'prereq', 'test1']


def test_list_prerequisites_with_makecmdgoals_conditional_prerequisite():
# `conditional`'s Makefile rule `-include`s a generated dependency file only when
# `conditional` itself is passed as an actual Make goal (see test/Makefile). This
# guards against a regression where the target was only used to look up its
# prerequisites in a goal-less `make -pq` database dump, silently hiding any
# prerequisite that depended on MAKECMDGOALS-conditional inclusion logic.
prerequisites = run_cli('list-make-prerequisites', 'conditional', '--recursive').splitlines()
assert prerequisites == ['hiddenprereq', 'standaloneprereq']
Loading