ChatGPT on Makefiles

In this exchange I needed to make a Makefile. I didn't have much experience with them, so I used ChatGPT and had a functional (and optimized) Makefile in 15 minutes. This probably saved me a couple of hours of reading documentation and trial and error.

Me

Show me a hello world example of a makefile. Explain all the different parts of it.

ChatGPT

Here is a simple "Hello, World!" example of a makefile:

# Define the target name and the dependencies
hello: main.c

# Define the recipe for building the target
	gcc -o hello main.c

Explanation:

  • The first line defines the target name (hello) and its dependencies (main.c). The dependencies are files that the target depends on and must be built first if they are changed.
  • The second line starts with a tab character, indicating that it's a recipe for building the target. The recipe is executed by the make command to build the target. In this case, it's a shell command to compile the main.c file into an executable named hello.

This makefile can be executed by running the make command in the same directory as the makefile. If the target is already up-to-date (i.e., the dependencies haven't changed), then make will simply return without doing anything. If the dependencies have changed, then make will execute the recipe and build the target.

Me

Does the target correspond to an output file? For example, if the file hello exists, then make knows it can skip that target?

ChatGPT

Yes, the target typically corresponds to an output file that is generated by executing the recipe. In the example makefile, the target hello corresponds to the executable file hello. If the file hello exists, then make checks the modification time of the target and its dependencies. If the target is newer than its dependencies, make assumes that it's up-to-date and doesn't execute the recipe. If the target is older than its dependencies or doesn't exist, then make executes the recipe to build the target.

Me

What about make clean? The file clean clearly does not exist, so make clean would therefore run every time?

ChatGPT

In a makefile, you can define multiple targets, each with its own recipe. The target name specified on the command line is the default target that make will build if no target is specified.

For example, you can add a clean target to the makefile that removes the generated files:

hello: main.c
	gcc -o hello main.c

clean:
	rm -f hello

Now, you can run make clean to remove the generated files, and make to build the hello target. When you run make clean, the clean target will be built, and its recipe will be executed, even though the file clean doesn't exist.

In this way, you can use make to build multiple targets and manage different aspects of your build process, not just building output files.

Me

all: hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz

hsd-gs-p-rtr.tar.gz:
	tar -cvzf hsd-gs-p-rtr.tar.gz hsd-gs-p-rtr

hsd-core-1.tar.gz:
	tar -cvzf hsd-core-1.tar.gz hsd-core-1

hsd-core-2.tar.gz:
	tar -cvzf hsd-core-2.tar.gz hsd-core-2

hsd-cust-pe-rtr.tar.gz:
	tar -cvzf hsd-cust-pe-rtr.tar.gz hsd-cust-pe-rtr

keys:

	umask 077
	wg genkey | tee hsd-gs-p-rtr/wireguard/private.key | wg pubkey > hsd-gs-p-rtr/wireguard/public.key
	umask 077
	wg genpsk > hsd-gs-p-rtr/wireguard/psk

	umask 077
	wg genkey | tee hsd-core-1/wireguard/private.key | wg pubkey > hsd-core-1/wireguard/public.key
	umask 077
	wg genpsk > hsd-core-1/wireguard/psk

	umask 077
	wg genkey | tee hsd-core-2/wireguard/private.key | wg pubkey > hsd-core-2/wireguard/public.key
	umask 077
	wg genpsk > hsd-core-2/wireguard/psk

	umask 077
	wg genkey | tee hsd-cust-pe-rtr/wireguard/private.key | wg pubkey > hsd-cust-pe-rtr/wireguard/public.key
	umask 077
	wg genpsk > hsd-cust-pe-rtr/wireguard/psk

deploy: hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz
	scp hsd-gs-p-rtr.tar.gz hsd-gs-p-rtr:~/hsd-gs-p-rtr.tar.gz
	scp hsd-core-1.tar.gz hsd-core-1:~/hsd-core-1.tar.gz
	scp hsd-core-2.tar.gz hsd-core-2:~/hsd-core-2.tar.gz
	scp hsd-cust-pe-rtr.tar.gz hsd-cust-pe-rtr:~/hsd-cust-pe-rtr.tar.gz

clean:
	rm -f hsd-gs-p-rtr.tar.gz
	rm -f hsd-core-1.tar.gz
	rm -f hsd-core-2.tar.gz
	rm -f hsd-cust-pe-rtr.tar.gz

How can I optimize this makefile to reduce redundant commands and text?

ChatGPT

You can reduce redundant commands and text in this makefile by using variables and pattern rules.

Variables can be used to store commonly used commands and strings, which can then be referenced throughout the makefile. For example, you can define a variable TARS to store the names of the tar archive files:

TARS = hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz

all: $(TARS)

Pattern rules can be used to define a recipe that applies to multiple targets that match a certain pattern. For example, you can define a pattern rule to generate the tar archive files:

%.tar.gz: %
	tar -cvzf $@ $<

This pattern rule defines a recipe for targets that match the pattern %.tar.gz, where % matches any string. The $@ variable refers to the target name, and the $< variable refers to the first dependency.

With these changes, the makefile becomes:

TARS = hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz

all: $(TARS)

%.tar.gz: %
	tar -cvzf $@ $<

keys:
	umask 077
	wg genkey | tee hsd-gs-p-rtr/wireguard/private.key | wg pubkey > hsd-gs-p-rtr/wireguard/public.key
	umask 077
	wg genpsk > hsd-gs-p-rtr/wireguard/psk

	umask 077
	wg genkey | tee hsd-core-1/wireguard/private.key | wg pubkey > hsd-core-1/wireguard/public.key
	umask 077
	wg genpsk > hsd-core-1/wireguard/psk

	umask 077
	wg genkey | tee hsd-core-2/wireguard/private.key | wg pubkey > hsd-core-2/wireguard/public.key
	umask 077
	wg genpsk > hsd-core-2/wireguard/psk

	umask 077
	wg genkey | tee hsd-cust-pe-rtr/wireguard/private.key | wg pubkey > hsd-cust-pe-rtr/wireguard/public.key
	umask 077
	wg genpsk > hsd-cust-pe-rtr/wireguard/psk

deploy: $(TARS)
	scp hsd-gs-p-rtr.tar.gz hsd-gs-p-rtr:~/hsd-gs-p-rtr.tar.gz
	scp hsd-core-1.tar.gz hsd-core-1:~/hsd-core-1.tar.gz
	scp hsd-core-2.tar.gz hsd-core-2:~/hsd-core-2.

Me

This is a good start, but I think the same can be done for the device hostnames (i.e. hsd-core-1, etc.). Can you try to optimize the keys target as well?

ChatGPT

Yes, you can also use variables and pattern rules to reduce redundant commands in the keys target.

Here's an example of how you can use variables to store the device hostnames:

DEVICES = hsd-gs-p-rtr hsd-core-1 hsd-core-2 hsd-cust-pe-rtr

keys:
	$(foreach device,$(DEVICES), \
		umask 077; \
		wg genkey | tee $(device)/wireguard/private.key | wg pubkey > $(device)/wireguard/public.key; \
		umask 077; \
		wg genpsk > $(device)/wireguard/psk;)

In this example, the $(foreach ...) function is used to generate a series of commands for each device hostname in the DEVICES variable. The \ character at the end of each line is used to indicate that the command continues on the next line. The ; character at the end of each line is used to separate commands.

This change results in a much more concise and readable keys target, without sacrificing any functionality.

Me

Ok, put both examples back together into a single makefile.

ChatGPT

Here's the complete makefile with both optimizations:

TARS = hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz
DEVICES = hsd-gs-p-rtr hsd-core-1 hsd-core-2 hsd-cust-pe-rtr

all: $(TARS)

%.tar.gz: %
	tar -cvzf $@ $<

keys:
	$(foreach device,$(DEVICES), \
		umask 077; \
		wg genkey | tee $(device)/wireguard/private.key | wg pubkey > $(device)/wireguard/public.key; \
		umask 077; \
		wg genpsk > $(device)/wireguard/psk;)

deploy: $(TARS)
	scp hsd-gs-p-rtr.tar.gz hsd-gs-p-rtr:~/hsd-gs-p-rtr.tar.gz
	scp hsd-core-1.tar.gz hsd-core-1:~/hsd-core-1.tar.gz
	scp hsd-core-2.tar.gz hsd-core-2:~/hsd-core-2.tar.gz
	scp hsd-cust-pe-rtr.tar.gz hsd-cust-pe-rtr:~/hsd-cust-pe-rtr.tar.gz

clean:
	rm -f $(TARS)

This makefile uses variables to store the names of the tar archive files and the device hostnames, and pattern rules to generate the tar archive files and generate keys for the devices. The result is a much more concise and readable makefile that is easier to maintain and modify in the future.

Me

I want to ensure that if the wireguard private.key, public.key, and psk files already exist, that it does not re-generate them. How can this be added into this makefile?

ChatGPT

To ensure that the wireguard private.key, public.key, and psk files are not re-generated if they already exist, you can add a check for their existence in the keys target. Here's an example:

TARS = hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz
DEVICES = hsd-gs-p-rtr hsd-core-1 hsd-core-2 hsd-cust-pe-rtr

all: $(TARS)

%.tar.gz: %
	tar -cvzf $@ $<

keys:
	$(foreach device,$(DEVICES), \
		if [ ! -f $(device)/wireguard/private.key ]; then \
			umask 077; \
			wg genkey | tee $(device)/wireguard/private.key | wg pubkey > $(device)/wireguard/public.key; \
		fi; \
		if [ ! -f $(device)/wireguard/psk ]; then \
			umask 077; \
			wg genpsk > $(device)/wireguard/psk; \
		fi;)

deploy: $(TARS)
	scp hsd-gs-p-rtr.tar.gz hsd-gs-p-rtr:~/hsd-gs-p-rtr.tar.gz
	scp hsd-core-1.tar.gz hsd-core-1:~/hsd-core-1.tar.gz
	scp hsd-core-2.tar.gz hsd-core-2:~/hsd-core-2.tar.gz
	scp hsd-cust-pe-rtr.tar.gz hsd-cust-pe-rtr:~/hsd-cust-pe-rtr.tar.gz

clean:
	rm -f $(TARS)

In this makefile, the keys target uses the if [ ! -f $(device)/wireguard/private.key ]; then ... fi; and if [ ! -f $(device)/wireguard/psk ]; then ... fi; constructs to check if the private.key and psk files exist, respectively. If they do not exist, the keys are generated. If they do exist, the keys are not re-generated.

Me

Take the last makefile code you returned to me and further optimize the deploy target like you did with the keys target.

ChatGPT

You can use a similar approach to optimize the deploy target as you did with the keys target. Here's an example of how you can use variables and a loop to reduce redundant commands in the deploy target:

TARS = hsd-gs-p-rtr.tar.gz hsd-core-1.tar.gz hsd-core-2.tar.gz hsd-cust-pe-rtr.tar.gz
DEVICES = hsd-gs-p-rtr hsd-core-1 hsd-core-2 hsd-cust-pe-rtr

all: $(TARS)

%.tar.gz: %
	tar -cvzf $@ $<

keys:
	$(foreach device,$(DEVICES), \
		if [ ! -f $(device)/wireguard/private.key ]; then \
			umask 077; \
			wg genkey | tee $(device)/wireguard/private.key | wg pubkey > $(device)/wireguard/public.key; \
		fi; \
		if [ ! -f $(device)/wireguard/psk ]; then \
			umask 077; \
			wg genpsk > $(device)/wireguard/psk; \
		fi;)

deploy: $(TARS)
	$(foreach tar,$(TARS), \
		scp $(tar) $(basename $(tar)):~/$(tar);)

clean:
	rm -f $(TARS)

In this makefile, the deploy target uses the $(foreach ...) function to generate a series of scp commands for each tar archive file in the TARS variable. The $(basename ...) function is used to extract the base name of each tar archive file, which is used as the hostname for the scp command.

This change results in a much more concise and readable deploy target, without sacrificing any functionality.