Search code examples
makefilegnu-make

How to define a make target ONLY in the same directory as the Makefile?


I need a makefile with a wildcard rule that is only used for targets in the root of my codebase.

Let's say I have the following rules in my Makefile:

%.md:
    @echo "Running rule '%.md' for target '$@'."

md/%:
    @echo "Running rule 'md/%' for target '$@'."

When I run make ex1.md md/ex2.md unknown/ex3.md, I get this result:

Running rule '%.md' for target 'ex1.md'.
Running rule '%.md' for target 'md/ex2.md'.
Running rule '%.md' for target 'unknown/ex3.md'.

The expected result is:

Running rule '%.md' for target 'ex1.md'.
Running rule 'md/%' for target 'md/ex2.md'.
make: *** No rule to make target 'unknown/ex3.md'.  Stop.

To clarify, %.md should only run for md files in the same directory as the Makefile, and nowhere else.

For completeness, I am aware that the order of the rules in the Makefile matter, and flipping the order of the two rules above would "work" for md/ex2.md. This is not enough to solve the problem, as I also want make unknown/ex3.md to fail rather than running %.md.

Is there a way to do that with GNU Make?

The closest documentation pages I found about it are these:


Updates:

  • All the recipes in question are "PHONY". They are not really meant to create files, as make was designed to do.
  • None of the recipes have real prerequisites, but introducing some fake ones as a workaround would be acceptable.
  • The best solution so far was to (1) reorder rules, and (2) check the $(@D) (automatic variable)[https://www.gnu.org/software/make/manual/make.html#Automatic-Variables], like this:
md/%:
    @echo "Running rule 'md/%' for target '$@'."

%.md:
    @if [ "$(@D)" != '.' ]; then echo "Rule '%.md' is only meant for files on the current dir."; exit 1; fi
    @echo "Running rule '%.md' for target '$@'."

The result is this:

$ make ex1.md md/ex2.md unknown/ex3.md
Running rule '%.md' for target 'ex1.md'.
Running rule 'md/%' for target 'md/ex2.md'.
Rule '%.md' is only meant for files on the current dir.
make: *** [Makefile:127: unknown/ex3.md] Error 1

Solution

  • If recursive make is acceptable you could try:

    $ cat Makefile
    %.md:
    ifeq ($(MODE),)
        $(MAKE) MODE=$(@D) $@
    else ifeq ($(MODE),.)
        echo "Running rule '%.md' for target '$@'."
    endif
    
    md/%:
        echo "Running rule 'md/%' for target '$@'."
    
    $ make -s ex1.md md/ex2.md unknown/ex3.md
    Running rule '%.md' for target 'ex1.md'.
    Running rule 'md/%' for target 'md/ex2.md'.
    make[1]: *** No rule to make target 'unknown/ex3.md'.  Stop.
    make: *** [Makefile:3: unknown/ex3.md] Error 2
    

    The idea is to use conditionals to separate the recipes for %.md files:

    1. No mode defined: the recipe for %.md calls make again with the mode set to the directory part (. or other).
    2. . mode: the recipe for %.md is that for files in the root directory.
    3. Other mode: no recipe for %.md. If another rule applies (e.g. md/% for md/ex2.md), it is chosen, else make stops with an error (unknown/ex3.md).

    Note that the top make will fail and stop on the first encountered error, so:

    make -s unknown/ex3.md ex1.md md/ex2.md
    make[1]: *** No rule to make target 'unknown/ex3.md'.  Stop.
    make: *** [Makefile:3: unknown/ex3.md] Error 2
    

    If you prefer to continue building the other valid targets (and if you are OK with a 0 exit status) just add - on front of the recipe:

        -$(MAKE) MODE=$(@D) $@