by Matt Brennan

Sourcing a shell script in Make

Let’s say you have a makefile, and you already have a shell script (probably called something like env.sh) that you source before running your app to set environment variables in it. Now you’re writing tests (run by Make), and you need your environment variables there, too. “Aha”, you think, “I’ll source the env script in a makefile variable”:

DUMMY_ENV := $(shell source env.sh)

Well, that doesn’t work. Make runs its commands in a subshell, so the variables exported by source aren’t available to other commands.

You notice that Make and Bash have awfully similar syntaces for setting variables. So you try just includeing the script as if it were a makefile:

include env.sh

Nice! You’ve got the variables in your commands! Except, well, Make doesn’t parse quotes, so they’re part of the values. Not so nice. You need to munge the file into something closer to make: remove the quotes, maybe convert the = to a :=. You need to convert your env.sh into an env.mk. Sounds like a job for Make!

env.mk: env.sh
	cat $< | sed 's/"//g ; s/=/:=/' > $@

But when to make env.mk? It needs to run before any target. You could just append it to all of your prerequisites, but that’s kind of horrible, and besides, it’ll mess up anything that’s depending on the order of the prerequisites. Well, there’s one target you could use. Make can make itself. If there’s a target for makefile, and its prerequisites are new, the target will run before anything, because the makefile might change. So you add env.mk as a prerequisite of makefile:

makefile: env.mk

Now the file is created when you make any target! But it’s not being included. You need to do that after it’s been made, at runtime. eval can run any Make code, even an include, at runtime:

makefile: env.mk
	$(eval include env.mk)

And that works! The first time. Once you’ve made it, env.mk is up to date, so the eval never runs. You need to include env.mk at the top, too, but not care if it doesn’t exist yet (because it’s about to be made and included anyway). That’s what -include is for:

-include env.mk

So, put it all together, and you end up with:

-include env.mk

makefile: env.mk
	$(eval include env.mk)

env.mk: env.sh
	cat $< | sed 's/"//g ; s/=/:=/' > $@

And that’s how you source a shell script in Make.


Update

Maybe then you notice that the makefile rule isn’t necessary. Anything that’s included is listed as a makefile and remade before the makefile runs. And because Make restarts execution once it’s remade a makefile, you don’t need the eval include. All you need is:

-include env.mk

env.mk: env.sh
	cat $< | sed 's/"//g ; s/=/:=/' > $@

Update 2

That’s a useless use of cat, isn’t it.

-include env.mk

env.mk: env.sh
	sed 's/"//g ; s/=/:=/' < $< > $@