Skip to content

Instantly share code, notes, and snippets.

@weshouman
Last active September 29, 2023 11:42
Show Gist options
  • Save weshouman/1d6c4a4555aba8f80db221b3b746fb9d to your computer and use it in GitHub Desktop.
Save weshouman/1d6c4a4555aba8f80db221b3b746fb9d to your computer and use it in GitHub Desktop.
tips and tricks in creating your own makefile
  1. Simple Assignment (=):

    • It assigns a value to a variable.

    • The value of the variable is evaluated when the variable is used, not when it's declared.

    • This means that if you reference other variables in the assignment, their values will be expanded at the time of use, not at the time of assignment.

      FOO = $(BAR)
      BAR = Hello
      all:
          @echo $(FOO)   # This will output "Hello"
  2. Immediate Assignment (:= or ::=):

    • It assigns a value to a variable immediately.

    • The right-hand side is evaluated at the time of assignment.

    • This is useful if you want to ensure that a variable's value is computed once and doesn't change.

      BAR = Hello
      FOO := $(BAR)
      BAR = World
      all:
          @echo $(FOO)   # This will output "Hello"
  3. Append Assignment (+=):

    • This operator appends the right-hand side to the current value of the variable.

    • It behaves like simple assignment if the variable was not previously defined.

      FOO = Hello
      FOO += World
      all:
          @echo $(FOO)   # This will output "Hello World"
  4. Conditional Assignment (?=):

    • This operator assigns a value to a variable only if the variable is not already defined.

    • If the variable has a value, the assignment is ignored.

      FOO ?= Hello
      FOO ?= World
      all:
          @echo $(FOO)   # This will output "Hello"
  5. Shell Assignment (!=):

    • This operator assigns the output of a shell command to a variable.

    • The shell command is executed immediately, and its output is assigned to the variable.

      FOO != echo Hello
      all:
          @echo $(FOO)   # This will output "Hello"
  6. Immediate with Escape Assignment (:::=):

    • Just like the := operator, the :::= operator also performs immediate assignment. The difference is in how the right-hand side is handled when it contains variable references.
    • With :::=, any $ characters in the assignment value are treated as literal dollar signs, unless they are escaped with another $.
    BAR = World
    # With := assignment
    FOO1 := $$BAR
    # With :::= assignment
    # We do not need to use another $ to escape by ourselves
    FOO2 :::= $BAR
    
    all:
        @echo FOO1: $(FOO1)   # This will output "FOO1: $BAR"
        @echo FOO2: $(FOO2)   # This will output "FOO2: $BAR"

Reference

Source won't work

source command won't work, but rather . will work, and ensure you have the file with executable permission set (ie: use first chmod 755 venv/bin/activate).
Note: source and . execute the command in the shell, while /path/to/exe execute in a child instance.

Dryrun is an option

To invoke a Dryrun for a target use make -n.
Ref: SO answer.

Autotools get us some verbosity

If make was created from autotool's ./configure, we may invoke make VERBOSE=1 for more verbosity.
Ref: SO answer.

Create our own help target

help:           ## Show this help.
	@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'

ref: Gist by prwhite.

List all default targets

make -p -f/dev/null | grep '^[^.%][-A-Za-z0-9_]*:' | cut -d: -f1

Accordingly we could list current targets also by comparison to the default list, may be something like that

# note: the output targets are not as expected
comm -23 <(make -p | grep '^[^\s]*:' | cut -d: -f1 | sort) <(make -p -f/dev/null | grep '^[^\s]*:' | cut -d: -f1 | sort)
  • make -p | grep '^[^\s]*:' | cut -d: -f1 | sort: list all the targets (including built-in ones) from the Makefile, sorts them, and then outputs them.
  • make -p -f/dev/null | grep '^[^\s]*:' | cut -d: -f1 | sort: list only the built-in targets, sorts them, and then outputs them.
  • comm -23: compare the two sorted lists and outputs only the lines that are unique to the first list (i.e., the targets unique to the current Makefile).

Multi-patterns are not an option

It's not possible to have 2 generic patterns % in the rule of a target, for example, we can't do

.PHONY: %_%

%_%:
	command --arg1 $* --arg2 $(word 2, $(subst _, ,$@)) $(ANY_ARG)

Only one pattern is allowed, so we could use instead

%:
	command --arg1 $(word 1, $(subst _, ,$@)) --arg2 $(word 2, $(subst _, ,$@)) $(ANY_ARG)

That's too generic, and usually not what we want, so we could make it more specific by adding a prefix, for example:

q_%:
	command --arg1 $(word 2, $(subst _, ,$@)) --arg2 $(word 3, $(subst _, ,$@)) $(ANY_ARG)

Failing trial

However, adding a prefix means we changed the rule name, which is usually undesireable, so the solution would be to define the values that the rule customization could look like, the following, but we'll notice that the target rule would be replacing the variable with all the content ...

ACTION ?= act1 act2
FORMAT ?= format1 format2 format3

${ACTION}_${FORMAT}:
	command ${ACTION} ${FORMAT} $(ANY_ARG)
  
# use it like that `make act1_format2`

Fix

To fix that we'll need to create a target template, in which we customize both the rule and the makefile content.
We'll do that using the macro TARGET_template, notice here that ANY_ARG's $ had to be escaped

ACTIONS := act1 act2
FORMATS := format1 format2 format3

define TARGET_template
$(1)_$(2):
	command $(1) $(2) $$(ANY_ARG)
endef  

$(foreach a,$(ACTIONS),$(foreach f,$(FORMATS),$(eval $(call TARGET_template,$(a),$(f)))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment