bridging shell and make


Recently at work I was tasked with updating our build system to be more stable and maintainable in the future. We had previously built a cobbled together Makefile based project that worked for a few specific types of builds but it was very fragile and interdependant. For example in order to build the version of our product that worked with the eclipse plugin we would have to run:

make ECLIPSE=true

In order to build for the visualstudio extension:

make VS=true WINDOWS=true

The problem came when we wanted to support windows versions of the eclipse platform. This required that we be able to run:

make ECLIPSE=true WINDOWS=true

But there was a snag, since some of the functionality for the visual studio build was flagged as being “windows” and some of it as being “vs”, this did not mix well with the “eclipse” functionality. At the time we hacked together a new target:

make ECLIPSE_ON_WINDOWS=true

And we felt dirty ever since.

Come around to the next time we needed a new target (this time would be a CMDLINE_ON_WINDOWS=true abomination) and I said no. I am going to fix this situation. I went for orthogonal as much as I could, so that it would be possible to build all different configurations, such as a x64-Linux-VS build. I then wrote some error checking and the makefile was happy. In addition we have some shell scripts which are affected by the behaviour of the makefile, and so I thought it would be pertinent to try to make them both speak the same language.

In order achieve this I needed to figure out the semantics of Makefile variables/arguments.

  • If a variable is defined in the environment, you need to use make -e to get it into a makefile variable
  • If a variable is defined on the commandline, then it will overwrite the definition in the makefile.
  • If a variable is not defined in either of the above, and it is in the file, use that definition.

So the desired order of evaluation goes:

  • in file
  • environment
  • commandline

I start off by processing all the default values for the given variables:

for line in `cat defaults`; do
  if [[ "$line" == \#* ]]; then
    continue # skip comments
  fi

the defaults file is a series of lines which look like VAR=val. These variable value pairs are extracted below

  LINE_VAR=`echo $line | sed -e "s/=.*//"`
  LINE_VAL=`echo $line | sed -e "s/$LINE_VAR=//"`

I need to be able to see if the variable has been set before, so here I use an eval to see what is in the current variable, and if it has, use that value and export the pair

  eval "LINE_VAR_VAL=\$$LINE_VAR"
  # previously exported values overwrite defaults
  if [ "$LINE_VAR_VAL" != "" ]; then
    LINE_VAL=$LINE_VAR_VAL
  fi
  export $LINE_VAR=$LINE_VAL
done

Finally I go through the arguments array and overwrite any environment variables that are specified. The script that uses this one will have to refer to ALL_ARGS after they have been processed here.

# arguments that are VAR=something overwrite
NEW_ARGS=""
for ARG in "$@"
do
  if [[ "$ARG" =~ ^[A-Z].*=.* ]]; then
    eval $ARG
  else
    NEW_ARGS="$NEW_ARGS $ARG"
  fi
done
ALL_ARGS="$NEW_ARGS"

This simplish hack has allowed me to standardise on a set of variables which control the logic of building, while maintaining orthogonality.