skip to Main Content

I’m trying to adapt the Example from the Official Documentation of GNU make to my Use Case:
GNU make – Example of a Conditional

libs_for_gcc = -lgnu
normal_libs =

ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif

foo: $(objects)
        $(CC) -o foo $(objects) $(libs)

So, I created this Prototype:

libs_for_gcc="gcc libs"
normal_libs="normal libs"
libs=


all: do_nothing
    @echo "all: done."


do_nothing:
    @echo "do_nothing: done."


example:
    @echo "example: go ..."
    @echo "example - cc: '$(CC)'"
    @echo "libs_for_gcc: $(libs_for_gcc)"
    @echo "normal_libs: $(normal_libs)"
    @echo "libs: $(libs)"
ifeq ($(CC),gcc)
    libs=$(libs_for_gcc) && echo "libs: '$$libs'"
    @echo "example - libs: '$(libs)'"
else
    libs=$(normal_libs) && echo "libs: '$$libs'"
    @echo example - libs: $(libs)
endif
    @echo "example - libs: '$(libs)'"
    @echo "example: done."


test: libs += " -Ddebug"
test: example foo


bar:
    @echo "bar: go ..."
ifeq (${libs}, "")
    @echo "bar - libs: empty"
    @echo "assigning libs"
    libs=$(libs_for_gcc)
else
    @echo "bar - libs: not empty"
    @echo "bar - libs: '${libs}'"
endif
    @echo "bar - libs: '${libs}'"
    @echo "bar: done."



foo: 
    @echo "foo: go ..."
ifneq ("$(libs)", "")
    @echo "foo - libs: not empty"
    @echo "foo - libs: '$(libs)'"
else
    @echo "foo - libs: empty"
endif
    @echo "foo: done."

Now when I run the Default Target with $ make
it just produces:

$ make
example: go ...
example - cc: 'cc'
libs_for_gcc: gcc libs
normal_libs: normal libs
libs: 
libs="normal libs"
example - libs:
example - libs: ''
example: done.

I see that the value of libs was not changed as intended.

When I run the make bar Target it produces:

$ make bar
bar: go ...
bar - libs: not empty
bar - libs: ''
bar - libs: ''
bar: done.

Here libs is not empty but it has nothing inside.

And when I run the make foo target it produces:

$ make foo
foo: go ...
foo - libs: empty
foo: done.

Here libs is understood as empty

As I see that libs is not changed correctly I tried to change the syntax to:

example:
    # [...]
ifeq ($(CC),gcc)
    libs := $(libs_for_gcc)
    @echo "example - libs: '$(libs)'"
else
    libs := $(normal_libs)
    @echo example - libs: $(libs)
endif

But then I get the GNU make Error:

$ make
example: go ...
example - cc: 'cc'
libs_for_gcc: gcc libs
normal_libs: normal libs
libs: 
libs := "normal libs"
/bin/sh: 1: libs: not found
Makefile:7: recipe for target 'example' failed
make: *** [example] Error 127

I couldn’t find any documentation about this behaviour so I appreciate any advise.


Edit:

  • Added all and test Targets.

Background:

The GNU make Command is an important part of the toolchain for packaging and deploying software and thus important in the daily work of System Administrators and DevOps Engineers.

  • The Debian and RPM Packaging uses GNU make to package software.
    Makefile Driven Packaging
    It runs the ./configure && make && make install command sequence.
  • The Travis CI Workflow uses GNU make for running testsuites.
    C Language Automated Testing
    It runs the ./configure && make && make test sequence.

All the completely different Use Case are managed by the same Makefile.
Now for my concrete Use Case I’m working on setting up the Travis CI Workflow Sequence to enable Automated Testing for my static linked Source Code Library.
So, contrary to the Packaging Sequence the Automated Testing Sequence requires Debug Features and advanced Output Evaluation to produce a meaningful Test.
I want the Test to check the Error Report and also the Memory Usage Report to alert me of any hidden errors.

Using the Advice about setting the Variable at the Target Declaration Line I was able to change libs for test, example and foo targets.
I also saw the important hint about the Bash Variable libs which is only valid on the same line:

$ make
do_nothing: done.
all: done.

$ make test
example: go ...
example - cc: 'cc'
libs_for_gcc: gcc libs
normal_libs: normal libs
libs:  -Ddebug
libs="normal libs" && echo "libs: '$libs'" ;
libs: 'normal libs'
example - libs:  -Ddebug
example - libs: ' -Ddebug'
example: done.
foo: go ...
foo - libs: empty
foo - libs: ' -Ddebug'
foo: done.

The Recipe libs=$(libs_for_gcc) && echo "libs: '$$libs'" shows that a new Bash Variable libs was created and it did not affect the GNU make Variable.

Still the Conditional ifneq ($(libs),) cannot detect that libs was already set for the test target.

2

Answers


  1. Chosen as BEST ANSWER

    Finally I achieved to get my Makefile make test Target working.
    make test Test Suite with GNU make

    The key was to understand that there are 3 different contexts where a GNU make Variable can be assigned.
    And that GNU make often needs helper functions because of some strange issues with Conditionals
    These issues are mostly undocumented and only can be solved by Try and Error and searching on StackOverflow.com

    1. The first, easiest and best documented context to assign a Variable is in Global Context as seen in the official documentation and the first answer.
    GNU make - Example of a Conditional

    ifeq ($(CC),gcc)
      libs=$(libs_for_gcc)
    else
      libs=$(normal_libs)
    endif
    

    The Global Variable ${libs} has global visibility.

    2. The next context which is not that obvious but still documented in the Official Documentation is in the Target Definition Line
    GNU make - Target-specific Variable Values

    test: libs += " -Ddebug"
    test: example foo
    

    Here ${libs} is valid for the test Target and all dependent Targets that it calls.
    But the only Condition is the Target itself.

    3. To assign dynamically a Variable in the context of a Target is to use the $(eval ) Function in combination with the $(shell ) Function as seen in this StackOverflow.com Post:
    Assign a Variable as Result of a Command

    test: examples_debug
            # [...]
            $(eval error := $(shell cat ./demo_error.log))
    ifeq ($(strip $(error)),)
            @$(ECHO) "NO Error: '${error}'"
    else
            @$(ECHO) "Error detected: '${error}'"
            $(error "Demo failed with Error [Code: '${error_code}']")
            @exit 1
    endif
    

    Here ${error} is read from a File. Additionally the $(strip ) Function is required to be able to check if it is empty, which is some of the undocumented issues that GNU make has and are wierd to Bash Developers.

    4. Another method that works but does not use a Makefile Variable and is somewhat bulky is evaluating the Variable entirely in Bash in 1 single Line which can be found at: Check Variable with Bash Command Recipe and was also hinted by the previous Answer.
    Which would look like:

    test: test_debug
            # [...]
            leak=`cat ./test_heap.log | grep -i "unfreed memory blocks" | awk '{print $$1}'` ; if [ $$leak -gt 1 ]; then echo "Memory Leaks: $$leak" && exit 1; else echo "Memory Leaks: NONE"; fi ;
    

    Here $$leak is a Bash Variable and only valid within the same Line. An approach that is somewhat similar to the GitHub Workflow Command Logic. (Actually is was directly ported from the GitHub Workflow for the same Project)

    As about Evaluation of Conditionals there are many undocumented issues in GNU make that require Bash Workarounds to achieve the goal.
    As documented at:
    Preprocessing Numerical Values for the Makefile Evaluation
    There are issues with comparing Numbers and it is impossible to compare against 0 which is extremely important Exit Code for Command Line Applications. So the Workaround looked somewhat like:

    test: test_debug
            # Run the Testsuite Application
            # [...]
            $(eval error_code := $(shell export HEAPTRC="log=test_heap.log" ; echo -n "" >./test_heap.log ; ${wrkdir}/tests_hash-lists.dbg.run 2>./tests_error.log 1>./tests_exec.log ; echo "$$?"))
            @$(ECHO) Application Tests: Execution finished with [${error_code}]
            #Application Tests Execution Report
            # [...]
            $(eval is_error := $(shell if [ "${error_code}" = "0" ]; then echo false ; else echo true ; fi ;))
    ifeq (${is_error}, true)
            @$(ECHO) Exit Code non cero: '${error_code}'
            @$(ECHO) "Tests failed with Error [Code: '${error_code}']"
            @exit ${error_code}
    endif
    

    In this use case ${error_code} is tested with a Bash Conditional to populate the Helper Variable ${is_error} with true or false which then can be checked in GNU make.


    Discussion:

    The test Target cannot just exit on error.
    For troubleshooting a failed Automated Test it is crucial to see the Exit Code, the Error Message and the Heap Report.


  2. The difference between your example and the one in the GNU make manual is that in the GNU make manual they are setting a make variable named libs (they are assigning the variable outside of any recipe).

    In your usage you are assigning a shell variable named libs (you are assigning it inside the recipe, indented with a TAB).

    That’s why you get an error when you try to use :=, because that’s a make assignment and is not a valid shell assignment.

    Then in your echo you print the make variable $(libs) which has not been set at all. Further, every logical line of the recipe is run in inside it’s own shell. So you are running the equivalent of:

    /bin/sh -c 'libs="gcc libs"'
    /bin/sh -c 'echo '
    

    so even if you DID change your echo command to print the shell variable (via $$libs) it would be empty because the previous shell, where it was set, has exited.

    You want to use the same syntax as in the example: take the assignment of the variable OUT of the recipe and set the make variable instead:

    libs_for_gcc = gcc libs
    normal_libs = normal libs
    
    ifeq ($(CC),gcc)
      libs = $(libs_for_gcc)
    else
      libs = $(normal_libs)
    endif
    
    example:
            @echo "example: go ..."
            @echo "example - cc: '$(CC)'"
            @echo "libs_for_gcc: $(libs_for_gcc)"
            @echo "normal_libs: $(normal_libs)"
            @echo "libs: $(libs)"
            @echo "example - libs: '$(libs)'"
            @echo "example: done."
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search