sshilovsky
sshilovsky

Reputation: 998

How to check existence of the target in a makefile

I want to run certain action in the shell depending on whether a default makefile in the current directory contains a certain target.

#!/bin/sh
make -q some_target
if test $? -le 1 ; then
    true # do something
else
    false # do something else     
fi

This works, because GNU make returns error code 2 if the target is absent, 0 or 1 otherwise. The problem is that is not documented this way. Here is a slice of the man:

-q, --question
        ``Question mode''.  Do not run any commands,  or  print  anything;
        just  return  an exit status that is zero if the specified targets
        are already up to date, nonzero otherwise.

Only zero/nonzero is distinguished. What is the right way to do that?

Upvotes: 8

Views: 14801

Answers (5)

Andrei
Andrei

Reputation: 884

Simple solution:

output=$(make -n some_target 2>&1 | head -1)
if [[ "$output" != *"No rule to make target"* ]]; then
    echo "target is present"
fi

The idea is to check output of make without running it. Not sure, it's 100% right way though.

Can be wrapped in function:

function check_make_target {
    output=$(make -n "$1" 2>&1 | head -1)
    [[ "$output" != *"No rule to make target"* ]]
}
if check_make_target some_target; then
    echo "target is present"
fi

Upvotes: 1

steinybot
steinybot

Reputation: 6104

I needed a solution which would work for multiple goals/targets and also not trigger the Makefile to be remade.

Here is what I ended up using:

#!/usr/bin/env bash

set -euo pipefail

dir="$1"
targets="$2"

# If there is no Makefile then return.
[[ ! -f "${dir}/Makefile" ]] && exit 1

# First check if this build will remake itself.
# If it does then add Makefile as a target to stop it.
if eval ${MAKE} --dry-run "--directory=${dir}" Makefile &> /dev/null; then
  db_target=Makefile
else
  db_target=
fi

# Search the Make internal database for the targets.
eval ${MAKE} --dry-run --print-data-base "--directory=${dir}" "${db_target}" | \
  awk -v targets="${targets}" '
    BEGIN {
      # Count the number of targets and put them in an array keyed by their own value.
      numRemaining = split(targets, temp)
      for (i in temp) remaining[temp[i]]
    }
    # If we found all the targets then consume all the input so the pipe does not fail.
    numRemaining == 0 { next }
    # Skip everything from the first line up to the start of the database comment inclusive.
    NR == 1, /\# Make data base/ { next }
    # Skip those that are not real targets.
    /\# Not a target:/, /^[^#]/ { next }
    {
      # Check if this line starts with any of the targets.
      for (target in remaining) {
        if ($0 ~ "^"target":") {
          delete remaining[target]
          numRemaining--
          next
        }
      }
    }
    END {
      # If we get to the end then make sure that we found all the targets.
      exit (numRemaining ? 1 : 0)
    }'

The caveats are:

  • The Make file must be called Makefile (or not exist at all).
  • You have an export MAKE statement in your Make file (you can hard code make into the script if you prefer).
  • You don't mind an evil eval (Yuck!).

I use it in my Make file to aggregate targets on sub-projects which may not exist like this:

top_targets := all setup build clean mostlyclean
sub_dirs := foo bar

.PHONY: $(top_targets)
$(top_targets): $(sub_dirs)

.PHONY: $(sub_dirs)
$(sub_dirs):
    @if ./bin/make-target-exists.sh '$@' '$(MAKECMDGOALS)'; then $(MAKE) --directory='$@' $(MAKECMDGOALS); fi

Upvotes: 0

MikeBergmann
MikeBergmann

Reputation: 419

Another possibility is to grep within the database that results from reading the makefile:

some_target:        
  @ if $(MAKE) -C subdir -npq --no-print-directory .DEFAULT 2>/dev/null | grep -q "^$@:" ;  \
    then                                                                                    \
              echo "do something" ;                                                         \
    fi

where

-n --just-print, --dry-run
     Print the commands that would be executed, but do not execute them.

-p, --print-data-base
     Print the data base (rules and variable values) that results from reading 
     the makefiles

-q, --question
     Question mode. Do not run any commands, or print anything; just return and 
     exit status that is zero if the specified targets are already up to date, 
     nonzero otherwise.

Upvotes: 3

wvdschel
wvdschel

Reputation: 11828

Late answer, but perhaps it will help people facing the same problem in the future.

I use the approach below, which does require you to modify the Makefile (you need to add a single %-rule-exists pattern rule). If you don't want to do that, you can just run make -n your-target-to-check &> /dev/null directly from your script.

I use this to have commands like autobuild and autoupload, which are included for context in the snippet.

%-rule-exists: 
    @$(MAKE) -n $* &> /dev/null


auto%: %-rule-exists
    $(info Press CTRL-C to stop $*ing)
    @while inotifywait -qq -r -e move -e create -e modify -e delete --exclude "\.#.*" src; do make $* ||: ; date ; done

Example output:

$ make build-rule-exists; echo Exit code: $?
Exit code: 0
$ make nonsense-rule-exists; echo Exit code: $?
Exit code: 2

Note that it does not actually build the target to find out if the rule exists, courtesy of the -n flag of make. I needed it to work even if a build would fail.

Upvotes: 4

MadScientist
MadScientist

Reputation: 100781

You should read the GNU make manual rather than the man page: the man page is merely a summary not a complete definition. The manual says:

The exit status of make is always one of three values:

0    The exit status is zero if make is successful

2    The exit status is two if make encounters any errors. It will print messages
     describing the particular errors.

1    The exit status is one if you use the ‘-q’ flag and make determines that
     some target is not already up to date.

Since trying to create a target that doesn't exists is an error, you'll always get an exit code of 2 in that situation.

Upvotes: 8

Related Questions