diff --git a/.gitignore b/.gitignore index 8d19a12..439b970 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ __pycache__ build -*.egg-info \ No newline at end of file +*.egg-info +# Regenerated by the test/Makefile's `conditional` regression test fixture +test/gendep.mk \ No newline at end of file diff --git a/src/pymakeutils/common.py b/src/pymakeutils/common.py index 1078275..a910457 100644 --- a/src/pymakeutils/common.py +++ b/src/pymakeutils/common.py @@ -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 ` 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( @@ -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: diff --git a/test/Makefile b/test/Makefile index f1db02f..f49c98d 100644 --- a/test/Makefile +++ b/test/Makefile @@ -5,4 +5,19 @@ prereq: preprereq1 preprereq2 cp $< $@ cat preprereq2 >> $@ -test2: dirprereq \ No newline at end of file +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 $< $@ \ No newline at end of file diff --git a/test/standaloneprereq b/test/standaloneprereq new file mode 100644 index 0000000..6827d9f --- /dev/null +++ b/test/standaloneprereq @@ -0,0 +1 @@ +standalone diff --git a/test/test_cli.py b/test/test_cli.py index ab47902..fe2f07b 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -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']